NewElection成功后必须显式续租lease并监听Observe通道,否则TTL过期会导致幽灵leader;Campaign返回nil仅表示初始抢占成功,不代表lease持续有效,需独立goroutine运行KeepAlive并处理channel关闭,同时用select监听Observe的nil响应以及时Resign。

clientv3.NewElection 是最稳妥的起点,但直接调用 Campaign 成功不等于稳坐 leader 位——90% 的线上故障来自续租失败后未退场。
为什么不能只信 Campaign 返回 nil 就万事大吉
Campaign 成功只代表你抢到了初始 key,不代表 lease 持有稳定。etcd 不会自动续租,客户端必须显式调用 Lease.KeepAlive 并消费其返回的 chan *clientv3.LeaseKeepAliveResponse。
- 如果这个 channel 因网络抖动被关闭,而你的代码没监听或忽略它,lease 就静默过期,key 被删,但程序还在处理请求
- context.DeadlineExceeded 或 lease expired 错误出现后,IsLeader() 可能仍返回 true(本地缓存未刷新)
- 续租 goroutine 必须独立运行,不能和业务主循环耦合;一旦 channel 关闭,应立刻触发 Resign
Election.Observe() 不是可选功能,是退出开关
Election.Observe() 返回的 chan *concurrency.ElectionResponse 是 etcd 主动推送“你已被踢出”的唯一可靠通道。
- 它会在 leader 身份变更(lease 过期、key 被删、被抢占)时发消息,但 channel 本身可能关闭(连接断开、lease 彻底失效)
- 必须用 select + case + <code>case (双重判断),不能只读不判空
- 收到 nil 响应或 channel 关闭,就要立即停止任务消费、关闭定时器、清空本地状态
- 别在 observe goroutine 里做耗时操作(如写 DB、发 HTTP),否则会卡住后续通知
<h3>Key 路径和 Lease TTL 的实操陷阱</h3>
每个服务实例必须用唯一 key,且 lease TTL 要兼顾故障发现速度与系统稳定性:
- key 路径建议包含实例标识,例如 <code>/leader/api-service-pod-abc123,避免 K8s 多副本写同一个 key
- Lease.TTL 设为 10–15 秒:太短(如 3 秒)易因 GC 停顿或调度延迟误触发重选;太长(如 60 秒)则故障恢复慢
- 不要复用同一个 concurrency.Election 实例给多个 goroutine 或进程,CAS 冲突率会飙升,leader 切换像抽风
- 每次关键业务操作前,仍需调 Get(ctx, election.Key()) 校验当前 key 状态,别依赖缓存的 isLeader bool 变量
真正难的不是抢到 leader,而是随时准备交棒——lease 过期是 server 单方面行为,客户端无感;watch 断连不会自动重试;Observe channel 关闭不是异常,是信号。











