etcd选主必须用Lease+CompareAndSwap:先申请带TTL的Lease,再以WithLease和WithIgnoreValue原子写入key;需后台KeepAlive续期,Watch监听且从rev=0开始,避免竞态与漏事件。

etcd 选主必须用 Lease + CompareAndSwap 原语
单纯往某个 key 写入自己的 ID 并不构成可靠选主——竞态下多个节点可能同时写成功,且没有自动过期机制。etcd 的选主本质是「抢占式租约持有」:每个候选节点先申请一个带 TTL 的 Lease,再用该 Lease 关联的 key 执行原子性的 CompareAndSwap(即 Put 带 leaseID 和 IgnoreValue 条件)。只有第一个成功绑定 lease 到目标 key 的节点才算当选。
常见错误是漏掉 lease 续期逻辑——一旦 lease 过期,key 自动删除,但节点可能还在自认为是 leader。必须在后台 goroutine 中持续调用 KeepAlive,且要处理 KeepAlive 返回的 channel 关闭或 error(比如网络抖动导致 lease 被 server 撤销)。
-
Lease.TTL建议设为 10–15 秒,太短会频繁续期增加 etcd 压力,太长则故障转移延迟高 - 执行
Put时必须传clientv3.WithLease(leaseID)和clientv3.WithIgnoreValue(),后者确保不覆盖已有值(只在 key 不存在时写入) - 不要依赖
Get后判断 key 是否存在再Put——这中间有竞态窗口
监听 leader key 变更要用 Watch 而非轮询
非 leader 节点不能靠定时 Get leader key 来感知变更,这样既低效又容易漏事件。正确做法是用 clientv3.Watcher 监听该 key 的创建、删除和修改事件。注意 Watch 必须从历史 revision 开始(用 clientv3.WithRev(0)),否则可能错过 leader 刚上任时的初始写入。
Watch channel 可能因连接断开而关闭,必须重连并重新 Watch。典型模式是用 for-select 循环,在 case 分支里检查 ev.Type == clientv3.EventTypeDelete ——这意味着当前 leader 租约过期或主动让位,本节点应立即尝试争抢。
- Watch 不需要指定
WithPrefix,leader key 是固定路径(如/leader),避免误监听其他 key - Watch 返回的
Event.Kv.ModRevision可用于做轻量级 fencing(例如拒绝比当前 revision 更旧的请求),但需配合业务逻辑实现 - 不要在 Watch 回调里做耗时操作,建议只发 signal 到另一个 goroutine 处理
session.NewSession 封装了大部分底层细节,但别盲目信任
etcd 官方 clientv3 提供了 session 包,用 session.NewSession 可快速获得一个带自动续租、自动重建 lease 的 session 对象,其 Leadership 方法返回的 session.LeaderElection 支持 TryLock 和 Resign。这确实省去了手动管理 lease 和 watch 的样板代码。
但要注意:它默认的 lease TTL 是 10 秒,且 TryLock 在失败时不提供详细的竞态原因(比如是 lease 冲突还是权限不足),调试困难;另外,它的 Watch 是内部启动的,无法定制 event 过滤或 revision 起点。生产环境若需精细控制(如分级降级、指标上报、fencing token 透传),建议绕过 session,直接用 raw clientv3 API。
- 使用
session时务必设置session.WithTTL(15)和session.WithContext(ctx),否则 cancel 上层 context 不会释放 lease -
TryLock返回 nil error 只代表“当前拿到 leader”,不代表“一直会是 leader”——仍需监听LeaderElection.Observe()channel - 调用
Resign后,该 session 的 lease 会被主动 revoke,但 key 不会立刻删除(etcd 异步清理),其他节点可能短暂看到 stale leader
选主后必须做 fencing,否则可能引发双写
etcd 本身不提供 fencing token 机制。当旧 leader 因网络分区未及时感知自己已失权,却继续处理请求,就可能和新 leader 同时写入下游存储,造成数据不一致。解决方案是在每次成功 TryLock 或 Watch 到 leader 变更后,生成一个单调递增的 epoch(比如用当前 lease ID 的高位 + 自增 counter),并将该 epoch 作为 header 或 metadata 透传给所有后端操作。
下游服务(如数据库、缓存)需校验该 epoch:仅接受大于等于本地记录 epoch 的请求,拒绝更旧的。这个 epoch 不能只存在内存里——必须持久化到 etcd 的 leader key value 中(例如把 {"epoch": 123, "node": "a"} 作为 value 写入),这样新 leader 通过 Get 就能读到最新 epoch 并在此基础上加一。
- 不要用时间戳做 epoch,NTP 漂移会导致乱序
- value 中的 epoch 字段必须用 JSON number 类型,避免字符串比较陷阱
- 如果下游不支持 epoch 校验,至少要在 leader 切换时强制清空共享缓存,并标记为“不可信窗口期”
真正难的不是抢到 leader,而是让整个系统相信“此刻只有一个权威”。租约、watch、fencing 三者缺一不可,少一个环节,分布式一致性就只剩运气。











