Fiber需自研etcd服务发现:用Watch监听+租约续期保障实时性,RWMutex线程安全更新backends,DynamicClient封装热更新逻辑,并实现带revision断点续传与指数退避的Watch重连机制。

Fiber 本身不内置服务发现能力,要让它支持从 etcd 动态获取上游服务地址(比如负载均衡到多个 user-service 实例),必须自己实现 Resolver + Watch 逻辑。这不是加个中间件就能跑通的事,核心难点在「如何把 etcd 的键值变更实时转成 Fiber 能理解的路由目标列表」。
为什么不能直接用 etcd client 做轮询查服务?
轮询 Get 每秒查一次 /services/user-service/ 下的所有 key,看似简单,但会快速暴露问题:
- etcd 的
Get是读请求,不带通知机制,查不到变化就只能等下一轮——延迟不可控 - 频繁
Get会压高 etcd 的 QPS,尤其当服务实例多、客户端多时,容易触发限流或 Raft 日志堆积 - 没处理租约过期(
LeaseID)时,已下线但未及时删除的 key 会残留,导致 Fiber 转发到不可达地址 - Fiber 的
fasthttp.Client不支持运行时热替换后端列表,你得自己维护一个可原子更新的[]string并配合RoundRobin或自定义 LB
必须用 Watch 监听变更,且要处理租约续期
etcd 的 Watch 是服务发现的正确起点,但它只告诉你「某个 key 被删了/改了/新增了」,不告诉你「这个 key 对应的服务是否还活着」。所以你得结合租约(Lease)来判断:
- 注册服务时,必须用
Put+LeaseGrant绑定租约,例如leaseID = 12345,然后Put("/services/user-service/10.0.1.10:8080", "alive", WithLease(leaseID)) - 客户端 Watch
/services/user-service/前缀时,收到事件后,要检查该 key 是否仍关联有效租约(可通过LeaseTimeToLive查) - 服务端需定期调用
LeaseKeepAlive续约,否则租约到期,key 自动被 etcd 删除,Watch 会收到Delete事件 - Fiber 后端列表更新必须是线程安全的:用
sync.RWMutex包裹backends []string,写时加写锁,读(如 LB 选节点)时加读锁
Fiber 中实际转发时怎么用动态后端?
你不能指望 Fiber 自动 reload Client 配置。常见做法是封装一个可热更新的 HTTP 客户端:
- 不直接 new
fasthttp.Client,而是封装一层DynamicClient,内部持有backends切片和读写锁 - 提供
GetBackend()方法,用RoundRobin或随机策略返回一个可用地址,如"http://10.0.1.10:8080" - 在 Fiber handler 中,每次请求都调用
client.Do(req, resp),其中req.SetRequestURI(...)动态拼接目标地址 - 别忘了设置超时和重试:etcd 变更后,新 backend 可能还没 ready,
fasthttp默认无重试,得自己 wrap 一层
示例片段(非完整):
func (dc *DynamicClient) GetBackend() string {
dc.mu.RLock()
defer dc.mu.RUnlock()
if len(dc.backends) == 0 {
return ""
}
idx := atomic.AddUint64(&dc.rrCounter, 1) % uint64(len(dc.backends))
return "http://" + dc.backends[idx]
}
// 在 Fiber handler 中:
backend := client.GetBackend()
if backend == "" {
c.Status(fasthttp.StatusServiceUnavailable).SendString("no backend available")
return
}
req.SetRequestURI(backend + c.Path())
最容易被忽略的点:Watch 连接断开后怎么恢复?
etcd.Watcher 是长连接,网络抖动或 etcd 重启都会导致 Watch channel 关闭。很多人只写一次 Watch,没做重连逻辑,结果服务上线后几分钟内发现不了新实例。
必须实现带退避的重连:
- 监听
watchChan的err字段,一旦非 nil 就关闭旧 Watch,sleep 后重试 - 重试间隔建议从 100ms 开始,指数退避到最大 5s,避免雪崩式重连
- 重连时要用
WithRev(rev)从上次中断的 revision 继续,否则会漏掉中间变更 - Watch 启动后第一件事是
Get全量 key 填充初始backends,再开始监听增量事件
这一步不做,整个服务发现链路就是半残的——它只在第一次启动时“看起来”正常。


















