clientv3是go操作etcd唯一推荐客户端,服务注册必须绑定lease并续租,watch需用withprefix且处理断连续接,get列表须带withprefix和withsort,grpc场景需实现自定义resolver。

clientv3 是当前 Go 操作 etcd 的唯一推荐客户端,旧版 v2 API 已被弃用且不支持服务发现所需的核心语义(如租约、Watch 机制)。直接用 clientv3 实现服务发现,关键不在“能不能”,而在“怎么避免掉进 Raft 和租约的坑”。
服务注册必须绑定 Lease,否则实例下线后键不会自动清理
etcd 本身不感知服务生死,只靠租约(Lease)控制 key 生命周期。如果注册时没带 LeaseID,服务进程挂了,key 会永远残留,导致服务发现拿到僵尸地址。
- 注册前先创建租约:
lease, err := cli.Grant(ctx, 15)(单位秒),返回的lease.ID必须传给Put请求 -
Put时显式指定租约:cli.Put(ctx, "/services/user/1001", "127.0.0.1:8080", clientv3.WithLease(lease.ID)) - 必须定期续租(KeepAlive):启动一个 goroutine 调用
cli.KeepAlive(ctx, lease.ID),否则租约到期 key 就被删 - 服务退出前应主动撤销租约:
cli.Revoke(ctx, lease.ID),避免等待租约自然过期
Watch 目录而非单 key,但要注意 prefix 匹配行为
服务发现需要监听整个服务目录(如 /services/user/)下的所有变更,但 clientv3 的 WithPrefix 不是“子目录递归”,而是“字节前缀匹配”。这意味着 /services/user/1001 和 /services/user-dev/2001 都会被匹配到——如果你没做命名隔离,就会误收无关服务事件。
- Watch 时用
clientv3.WithPrefix,路径末尾不加斜杠:cli.Watch(ctx, "/services/user/", clientv3.WithPrefix) - 收到事件后,检查
ev.Kv.Key是否以目标路径开头,再按需解析(比如截取最后一位作为实例 ID) - 不要依赖 Watch 事件顺序绝对可靠:Raft 日志可能有短暂延迟,客户端需容忍乱序或重复事件
- Watch 连接断开时,
ev.Err()会返回非 nil,此时应重建 Watch(带clientv3.WithRev(ev.Header.Revision + 1)续接)
服务发现查列表时,Get 带 WithPrefix + WithSort,别漏掉排序参数
首次拉取服务列表不能只靠 Get,否则返回结果无序,且可能因并发写入导致中间态(比如某实例刚注册但租约还没续上)。必须用 WithPrefix 加 WithSort 确保一致性视图。
- 查全部实例:
resp, err := cli.Get(ctx, "/services/user/", clientv3.WithPrefix, clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend)) - 遍历
resp.Kvs时,逐个检查对应租约是否 still alive(调用cli.TimeToLive获取剩余 TTL,为 0 表示已过期) - 不要缓存 Get 结果太久:etcd 不保证长期一致性,建议每次调用前都重新 Get + 过滤,或结合 Watch 增量更新本地缓存
- 如果服务实例数超千,注意
Get默认限制 1000 条,需加clientv3.WithLimit(5000)显式扩容
GRPC 场景下必须实现自定义 resolver,不能只靠轮询 Get
GRPC 内置的 DNS resolver 不认识 etcd,你不能在 grpc.Dial("etcd:///user") 里幻想它自动干活。必须实现 resolver.Builder 和 resolver.Resolver,把 etcd Watch 事件映射成 GRPC 的 Address 列表。
- Builder 的
scheme()返回自定义名,比如"etcd",然后 Dial 用"etcd:///user" - Resolver 启动后立即
Get全量 +Watch增量,把有效地址塞进cc.UpdateState() - Watch 事件来临时,要合并新旧地址、剔除失效项,再调用
UpdateState—— GRPC 会据此触发连接重建或负载均衡器刷新 - 别在 Resolver 里做阻塞操作:Watch goroutine 和 UpdateState 必须异步,否则 Dial 卡死
lease.TimeToLive 返回值和 WatchChan 是否持续收包,而不是猛看业务逻辑。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











