newelection成功后需显式续租lease,否则ttl过期导致幽灵leader;isleader()仅本地缓存不可靠;observe()必须select监听nil以感知channel关闭。

直接用 clientv3.NewElection 能跑通,但线上 90% 的故障切换失败,是因为续租中断后程序没退场,还在继续当 leader 处理任务。
为什么 NewElection 成功后不能认为稳了
调用 Election.Campaign() 返回 nil 只表示你抢到了初始 key,并不等于 lease 持续有效。etcd 的 lease 是有 TTL 的(比如 10 秒),过期后 key 自动被删,而 Go 客户端不会自动续租——必须显式启动 Lease.KeepAlive() 并持续消费其返回的 channel。
- 网络抖动、goroutine 阻塞或 channel 满,都会导致
KeepAlive()流中断,lease 静默过期 -
Election.IsLeader()是本地缓存判断,lease 过期后它仍可能返回true,造成“幽灵 leader” - 不监听
Election.Observe(),就收不到 etcd 主动推送的 “你已被踢出” 事件
Observe() 必须用 select + case nil 处理关闭
Election.Observe() 返回一个 chan *concurrency.ElectionResponse,它会在 leader 身份变更(被抢占、lease 过期、key 被删)时推送一条消息。但它不是无限流:底层 lease 彻底失效或连接断开时,这个 channel 会被 close。
- 不能只写
for range ch—— range 结束后无法感知 channel 已关,后续逻辑会卡死或误判 - 必须用
select { case resp := ,并在 <code>resp == nil或ok == false时立刻退出 leader 循环 - 收到
nil响应或 channel 关闭,要立即调用Election.Resign()、停止任务消费、关闭定时器、清空本地状态 - 别在 Observe goroutine 里做耗时操作(如写磁盘日志),否则会堵住后续通知
KeepAlive goroutine 必须独立且可感知失败
Lease.KeepAlive() 返回的 channel 每秒发一次心跳响应,但一旦连接断开,channel 就会被 close。如果你没消费它,或者消费逻辑阻塞,续租就静默失败。
- 必须起独立 goroutine 运行 KeepAlive,和 leader 主循环解耦
- 用
for循环读取该 channel,一旦range结束(即 channel 关闭),立刻触发Resign() - 不要轮询
Lease.TimeToLive()判断是否过期——它只返回快照,且频繁调用增加 etcd 压力 - 错误类型要区分:
context.DeadlineExceeded是超时未抢到,clientv3.ErrNoLeader是 etcd 不可用,不能一概重试
手写选举比 NewElection 更容易出错
不用 concurrency.Election 而是手撸,核心是两个原子操作组合:先 Lease.Grant() 拿租约,再 KV.Put() 写 key,且必须带 WithLease(leaseID) 和 cmp.Version("key") == 0 条件。
- 漏掉
WithLease→ key 不绑定租约,节点宕机后 key 永久残留 - 不用
cmp.Version("key") == 0→ 后来者 Put 直接覆盖前人,多个节点同时认为自己是 leader,脑裂 - Watch 断连后没重试 → 错过 key 删除事件,假活
-
NewElection底层用事务封装了 Lease 创建、CAS Put、Watch 删除、自动重试四步,避免上述漏点
真正难的不是抢到 leader,而是随时准备被踢出;所有续租、监听、退场逻辑都得闭环,少一个环节,集群就可能双主或假活。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











