etcd watch 必须用长轮询而非单次 get,需 for range 持续读 watchchan;须用 context 控制生命周期、禁用 progress notify、直接解析 event.kv 而非重新 get;变更通知走独立 goroutine,启动时先 get 初始配置再 watch;断连需手动重连并用 withrev 从断点续听。

etcd Watch 机制必须用 long polling,不能只调一次 Get
etcd 的配置监听不是靠轮询 Get 实现的,而是依赖 Watch API 的长连接。如果只调一次 clientv3.NewWatcher 然后不持续接收事件,会立刻退出,根本收不到后续变更。
常见错误是把 watchChan := watcher.Watch(ctx, "/config/") 当成一次性操作,没做 for range watchChan 循环读取 —— 这会导致监听在建立后几秒就静默失效,且无任何报错。
- Watch 必须配合
context.WithCancel控制生命周期,避免 goroutine 泄漏 - 每次
WatchResponse的Events字段可能含多个事件(比如批量更新),不能只取[0] - etcd v3.5+ 默认启用
progress notify,需在WatchOption中显式禁用(clientv3.WithProgressNotify(false)),否则空事件会干扰判断
配置反序列化要绑定 revision,避免并发覆盖
监听到 etcd key 变更后,直接 Get 拿最新值再反序列化,看似合理,但存在竞态:两次变更间隔极短时,第二次 Get 可能覆盖第一次解析结果,导致配置“回退”或错乱。
正确做法是在 WatchResponse 中直接读 Event.Kv,它已包含该次变更对应的 Version 和 ModRevision,且数据与事件原子一致。
- 不要用
clientv3.NewKV(c).Get(...)重新查 —— 多一次网络往返,还破坏事件时序 - 若需校验结构体字段是否真实变化,比对
Event.Kv.ModRevision而非内容字符串(避免浮点精度、字段顺序等干扰) - 反序列化失败时,应丢弃该事件并记录 warn 日志,不能 panic 或阻塞 watch loop
Go 框架集成需隔离 Watch goroutine,禁止阻塞主流程
把 etcd Watch 逻辑直接塞进 HTTP handler 或 init 函数里,会导致服务启动卡住、无法响应健康检查,甚至被 k8s readiness probe 杀掉。
Watch 必须运行在独立 goroutine,并通过 channel 向业务层广播变更,主流程只负责注册回调和初始化。
- 推荐用
sync.Map存储当前配置快照,供 handler 直接读取,避免每次请求都加锁 - 变更通知 channel 容量建议设为 1(
make(chan Config, 1)),防止突发大量变更堆积导致 OOM - 框架启动时先
Get一次兜底加载初始配置,再启动 Watch,避免服务起来时配置为空
etcd 连接断开后 Watch 不会自动重连,必须手动处理
etcd clientv3 的 Watcher 在网络抖动或 leader 切换时会关闭 watchChan,但不会自动重建 —— 这是最常被忽略的生产问题。现象是:配置改了,服务完全无反应,日志也安静如鸡。
必须检测 watchChan 关闭,并在循环中重建 Watcher,同时利用 WithRev 从断点续听(传入上一次事件的 Header.Revision + 1)。
- 不要用固定 sleep 重试,应结合
backoff.Retry或指数退避(如time.Second * 1,*2,*4) - 首次 Watch 应用
clientv3.WithPrefix,断线重连时改用clientv3.WithRev(lastRev + 1),两者不可混用 - etcd server 端默认保留 1000 个历史 revision,若断连太久(>1000 次写入),需降级为全量
Get再续听
revision 对齐和重连状态管理,才是动态配置真正难绷的地方。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











