不能只靠Election.Campaign()成功就认为稳了,因其仅表示抢到初始leader key,而lease需显式KeepAlive续租,否则过期后key被自动删除,服务仍误当leader;必须监听Observe()通道关闭、独立运行KeepAlive协程、避免缓存isLeader状态。

直接用 concurrency.Election 能跑通,但线上 90% 的选主失效都源于续租失败后没及时退场——lease 过期了,key 被 etcd 自动删了,你的服务还在当 leader 处理请求。
为什么不能只靠 Election.Campaign() 成功就认为稳了
成功调一次 Campaign() 只代表你抢到了初始 leader key,不代表你能一直 hold 住。etcd 的 lease 是有 TTL 的,客户端不会自动续租,必须显式调用 Lease.KeepAlive()。一旦网络抖动、etcd 压力大、或 goroutine 被 GC 阻塞超过 TTL,lease 就会过期,key 被自动删除,而你的程序可能毫无感知。
-
context.DeadlineExceeded或lease expired错误出现后,Election.IsLeader()仍可能返回true(因为本地没刷新) - 不监听
Election.Observe(),等于放弃 etcd 主动推送的“你已被踢出”信号 - 多个实例共用同一个
concurrency.Election对象,CAS 冲突率飙升,leader 切换像抽风
必须监听 Election.Observe() 并处理 channel 关闭
Election.Observe() 返回一个 chan *concurrency.ElectionResponse,它会在 leader 身份变更(被抢占、lease 过期、key 被删)时推一条消息。但它不是永不停止的 stream:一旦底层 lease 彻底失效或连接断开,这个 channel 会被 close。
- 必须用
select+case +case 三重判断,不能只读不判空 - 收到
nil响应或 channel 关闭,立刻退出 leader 工作循环(比如停止消费任务队列、关闭定时器) - 别在
Observe()的 goroutine 里做耗时操作,否则会卡住后续通知
续租 goroutine 必须独立运行,并监控 Lease.KeepAlive() 返回流
Lease.KeepAlive() 返回一个 chan *clientv3.LeaseKeepAliveResponse,它每秒发一次心跳响应。但这个 channel 一旦底层连接断开,就会被 close,你得立刻感知。
- 续租逻辑必须单独起 goroutine,和 leader 主循环解耦
- 用
for range读这个 channel,一旦 range 结束,说明 keepalive 流已断,应立即触发Election.Resign()并退出 - 不要依赖
Lease.TimeToLive()定期轮询判断是否过期——它只是快照,无法替代 keepalive 流式保活
Key 路径和 Session 必须隔离,避免跨实例干扰
每个服务进程(或每个独立部署单元)应有且仅有一个 concurrency.Election 实例,对应唯一 key,比如 /services/api-leader-001。
- key 路径里必须带上实例标识,如
hostname或pod UID,避免 K8s 下多个副本写同一个 key - 不要复用
clientv3.Client连接池里的同一个concurrency.Session,每个 election 应配独立 session,否则 lease 续期互相干扰 - 启动时加日志输出
election.Key()和session.Lease(),方便排查是否 key 冲突或 lease 重复
最常被忽略的点是:所有关键业务操作前,必须调一次 election.IsLeader() 或 Get(ctx, key) 检查真实状态——绝不能缓存 isLeader 到全局变量里。etcd 状态和本地布尔值之间,永远存在竞态窗口。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











