题干与适用场景
多个无状态 worker 需要在任意时刻由一个实例执行定时结算任务。实例会崩溃、重启或发生网络分区,系统不能让两个 worker 长时间同时写入。请设计 Leader Election 服务,说明协调存储、租约、续期、fencing token、故障转移和运维边界。题目核心是安全的单主协调,不等同于普通互斥锁。
面试官考察点
是否定义了安全与活性
安全性是同一任期内最多一个有效 leader 被下游接受;活性是在旧 leader 失效并确认租约过期后,健康候选最终能接任。网络分区下不能为了可用性无条件宣布新 leader。
是否选择了有共识语义的协调器
leader 记录必须存放在具备线性化 CAS、租约和 watch 语义的系统中,例如 etcd。仅靠 Redis TTL 或本地时钟无法独立证明旧 leader 已失效。
是否防止旧 leader 写入
租约过期后,旧进程可能暂时恢复并继续工作。每次下游写入都应携带递增 fencing token,并由资源端拒绝过期 token,避免双主造成数据破坏。
回答前需要澄清的问题
- 任务是否允许重复执行,还是必须由下游严格串行接受?
- 选主范围是全局、租户、分片还是单个任务?
- 可以接受的故障转移时间和停顿窗口是多少?
- 协调器部署了几个故障域,法定人数和备份策略是什么?
- 下游能否校验 fencing token,任务是否支持幂等恢复?
- 需要 watch 事件、审计记录、告警和人工强制转移吗?
30 秒回答框架
“我会把选主范围和故障预算先定义清楚,用具备共识与线性化条件写入的协调器保存 leader 记录。候选通过事务创建或比较更新租约,成功者获得递增 fencing token,并在 TTL 内续期;续期失败就停止承接新任务。所有下游写入都校验 token,旧 leader 即使恢复也不能写。watch 只用于加速重选,安全性仍由协调器和资源端校验保证;监控任期、续期延迟、重复拒绝和转移时间。”
分步骤深入解答
先定义任期记录
记录可包含 electionname、leaderid、lease_id、term、候选元数据和更新时间。term 或 fencing token 必须单调递增,并由协调器在原子事务中分配,不能由客户端本地时钟生成。
选择协调器与写入条件
候选先创建带租约的临时记录;若记录不存在,线性化事务才允许写入。已有 leader 时,候选 watch 记录变化并重试。etcd 的 election API 用同一 election name 竞争,并由集群保证同一时刻只有一个成功者。
续期与主动降级
leader 使用独立 keepalive 定期续租,续期超时、连接失效、进程暂停或本地时钟异常都应进入 suspect,停止新任务和下游写入。不要在无法联系协调器时继续“自信”地工作。
处理故障转移
候选不能只根据本地 TTL 判断旧 leader 已死亡;必须看到协调器确认租约过期或记录被删除,再通过 CAS 竞争。转移时间由 TTL、检测间隔和调度延迟共同决定,应设上限并留出时钟与网络抖动余量。
加入 fencing token
新 leader 获得更大的 term,把 token 附在数据库更新、消息发布或外部 API 请求上。资源端保存已接受的最大 token,拒绝更小 token;这一步把“不能保证旧进程立刻消失”转化为可验证的写入规则。
面对网络分区与脑裂
少数派分区不能继续发放新任期。若候选无法与法定人数协调器通信,就只能停止或只读。恢复连接后,旧 leader 仍需重新观察当前任期并重新竞争,不能凭缓存状态恢复写入。
伪代码
~~~text campaign(): lease = coordinator.grant(ttl) result = coordinator.txn(key absent -> put(candidate, lease, next_term)) if result.succeeded: token = result.term keepalive(lease) runwithfencing(token) else: watch(key)
onkeepalivefailureorexpiry: stopnewwork() stopdownstreamwrites() ~~~
复杂度、恢复与观测
每次选举与续期都依赖协调器往返,候选数增加会带来 watch 和重试负载;应使用指数退避和抖动。记录当前 leader、term、续期 RTT、租约过期次数、选举耗时、fencing 拒绝、重复任务和协调器法定人数,支持回放任期变化。
| 机制 | 解决的问题 | 仍需补强 |
|---|---|---|
| 线性化 CAS | 避免两个候选同时成功 | 协调器必须有法定人数 |
| 租约 keepalive | 检测进程失效 | 暂停与网络分区仍可能留下旧进程 |
| fencing token | 拒绝旧 leader 写入 | 下游必须持久化并校验 token |
| watch 与退避 | 加速重选并降压 | 不能替代安全性证明 |
高质量示范回答
“我会用具备共识和线性化事务的协调器保存一个带租约的 leader 记录。候选通过‘记录不存在才写入’的 CAS 竞争,成功者拿到递增 term,并在 TTL 内 keepalive;无法续租就立即停止新任务和下游写入。每个数据库写入、消息或外部调用都带 fencing token,资源端拒绝小于已接受 token 的请求,因此旧 leader 即使从暂停或分区中恢复也不能继续写。少数派不能发放新 term,watch 只负责降低重选等待。上线监控续期 RTT、任期、转移耗时、fencing 拒绝、重复任务和协调器 quorum,并保留人工停用开关。”
常见错误
只使用 Redis TTL
TTL 到期与客户端看到到期不是同一件事,网络延迟和暂停可能让两个客户端都认为自己可以工作。需要协调器的线性化语义与下游 fencing。
把租约当作写入保护
租约只能帮助检测失效,不能让旧进程瞬间停止。没有 token 校验,旧 leader 仍可能覆盖新 leader 的结果。
用本地时间生成任期
机器时钟可能漂移、回拨或暂停。任期必须由协调器原子分配和持久化。
网络分区时强行自动接管
少数派无法确认旧 leader 状态,强行接管会制造 split-brain。安全设计应允许短暂不可用。
让 watch 事件直接决定安全状态
watch 可能丢失、延迟或重连。它只能触发重新读取和 CAS,不能替代线性化读写。
没有任务级幂等
即使选主正确,崩溃恢复和消息重投仍可能重复执行。任务需要幂等键、进度记录或可重放事务。
追问及应对
Leader Election 和分布式锁有什么不同?
锁通常保护一次临界区;选主维护较长生命周期的协调角色,并需要任期、续期、watch、fencing 和故障转移语义。两者可共享协调器,但安全问题范围不同。
TTL 应该设置多长?
它要覆盖正常续期 RTT、GC 或调度暂停、网络抖动和可接受故障转移时间。太短会频繁重选,太长会延迟接管,应通过故障注入和 SLO 校准。
为什么 fencing token 要由资源端检查?
协调器无法让所有旧进程立即停止。资源端拒绝旧 token,才能在旧进程仍存活时阻断危险写入。
etcd 集群失去 quorum 时怎么办?
不能提交新的任期或租约状态;现有 leader 也应在无法续期后停止写入。恢复 quorum 后再重新竞争。
如何处理 leader 长时间暂停?
暂停期间续期失败,协调器可让新候选接任。旧进程恢复后必须携带旧 token,被下游拒绝并重新参加选举。
如何验证没有双主?
注入进程暂停、网络分区、时钟跳变和协调器节点故障,检查同一任期的接受写入是否始终只有一个 token,并核对转移耗时和拒绝记录。