代表性面试主题

系统设计面试:如何设计安全的 Leader Election 服务?

系统设计困难
OfferCC 编辑团队发布 更新

题干

多个无状态 worker 需要在任意时刻由一个实例执行定时结算任务。请设计 Leader Election 服务,说明租约、续期、fencing token、故障转移、网络分区和观测指标。

题干与适用场景

多个无状态 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 只用于加速重选,安全性仍由协调器和资源端校验保证;监控任期、续期延迟、重复拒绝和转移时间。”

分步骤深入解答

先定义任期记录

记录可包含 election_name、leader_id、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,并核对转移耗时和拒绝记录。

公开来源

同类题目

相关面试工具

用 Solve 整理系统设计回答

从澄清需求开始,展开规模、架构、组件选择和取舍。

查看工具