etcd + clientv3 是最务实的高性能配置中心选择,需配置 grpc.withblock()、dialtimeout、autosyncinterval,watch 前先 get 且路径严格匹配,用 rwmutex 细粒度锁,敏感配置绑定 lease 并续期。

直接上 etcd + clientv3 是最务实的高性能选择,别自己封装 HTTP 服务或轮询数据库——延迟高、一致性弱、重试逻辑难写全。
etcd clientv3 初始化必须带 grpc.WithBlock() 和 DialTimeout
不设这些参数,clientv3.New 可能卡死在 DNS 解析或连接建立阶段,且不报错;生产环境常见现象是服务启动一半就 hang 住,日志里没线索。
-
DialTimeout建议设为3 * time.Second,太长会拖慢故障发现速度 - 必须显式传
grpc.WithBlock(),否则底层连接可能异步失败,err被吞掉 - 加
AutoSyncInterval: 30 * time.Second,应对 etcd 集群节点变更导致的 endpoint 失效
Watch 必须先 Get 再监听,且路径要严格匹配 WithPrefix
只 Watch 不 Get,服务重启后首次配置值就丢了;路径结尾少个 / 或漏掉 clientv3.WithPrefix(),会导致子 key 变更收不到事件。
- 读取用:
cli.Get(ctx, "/config/app/", clientv3.WithPrefix()) - 监听用:
cli.Watch(ctx, "/config/app/", clientv3.WithPrefix()),两个路径完全一致 - Watch goroutine 里必须检查
ok := ,channel 关闭后不能继续读,否则 panic - 每次收到事件,先判断
ev.Type == clientv3.EventTypePut,跳过Delete和Unknown
配置更新不能全量覆盖结构体,要用 RWMutex 细粒度控制
直接 json.Unmarshal 到全局 struct 后赋值,会引发竞态;高频读场景下,写锁阻塞所有读请求,QPS 断崖下跌。
- 用
sync.RWMutex,读操作全程持RLock(),写操作只锁变更字段所在字段组 - 例如 DB 连接池参数单独一组,超时时间单独一组,避免改一个 timeout 导致整个 config struct 被锁
- Watch 回调里不要做耗时操作(如发 HTTP 请求、写磁盘),起新 goroutine 处理,防止阻塞 watch 流
- 解析失败(如 JSON 格式错)要打 warn 日志并 continue,不能
panic或 return,否则整个 watch 协程退出
敏感配置必须走 lease 绑定,且 Put 操作要显式传 lease ID
不绑 lease,配置写入后进程崩溃或网络断开,key 会永久残留,下次启动时读到脏数据;手动删又容易误删其他服务的配置。
- 初始化 lease:
resp, err := cli.Grant(ctx, 30),拿到resp.ID - 写配置:
cli.Put(ctx, "/config/app/db.url", "mysql://...", clientv3.WithLease(resp.ID)) - 另起 goroutine 调
cli.KeepAlive,定期续期,避免 lease 过期自动删除 - 读操作(
Get/Watch)不需要 lease,传了也无效
真正难的不是写完 Watch 循环,而是 lease 续期是否稳定、watch channel 关闭后能否无损重连、以及并发读写时字段级锁的边界是否被意外扩大——这些地方一出问题,表现就是配置“有时生效有时不”,查起来极费时间。











