如何设计基于租约的 Leader 选举?
题目与使用场景
多个副本共享一个协调存储,需要在任意时刻只有一个实例执行定时结算任务。请设计租约记录、竞选、续租、交接和观测,并说明网络分区、进程暂停、时钟漂移及存储故障时的行为。租约只控制“谁可以开始工作”,业务写入仍需幂等和条件校验。
面试官考察什么
- 能否把租约安全边界与业务副作用分开。
- 是否理解原子比较并更新、任期 fencing token 和法定人数。
- 能否说明暂停、分区、旧 Leader 复活时为什么不会双主写入。
- 是否给出可验证的指标、故障注入和恢复策略。
作答前的澄清问题
先确认任务是否允许短暂停顿、最多容忍多久的重复执行、协调存储的线性一致性能力、跨区域延迟、实例数量和时钟同步质量。若必须跨区域强一致,选举延迟和可用性要明确取舍。
30 秒回答框架
我会使用具备线性一致性和条件写入的协调存储。每个候选者用唯一身份和递增任期竞争租约,只有成功的原子创建或更新者成为 Leader;Leader 在租约过期前续租。每次业务写入携带任期 token,由下游拒绝旧 token。候选者只有在确认读到新状态后再接管。网络分区或长时间暂停会让实例主动停止工作,宁可短暂不可用也不冒险双主。
分步骤深入解答
1. 数据模型与原子操作
租约记录包含 holder identity、期限、任期 token、版本号和最后续租时间。竞争使用 compare-and-set:记录不存在时创建,或只有版本未变且已过期时更新。Kubernetes 的 Lease 对象就是把持有者、续租时间和租期作为协调数据;实现必须确认底层读写语义,而不是依赖普通最终一致缓存。
2. 续租与自我降级
续租间隔应明显小于租期,并为网络抖动和调度暂停留出余量。续租失败、读不到确认或进程暂停超过安全窗口时,实例立即停止产生副作用;恢复后重新竞选。不要用本机墙上时钟单独判断他人是否过期。
3. Fencing 与业务幂等
每次成功竞选生成单调递增 token。任务执行器、数据库条件更新或下游服务必须拒绝小于当前 token 的请求。这样即使旧 Leader 在暂停后恢复,也不能覆盖新 Leader 的写入。结算任务仍需幂等键、事务边界和可重试设计。
高质量示范回答
我先定义安全目标:同一资源不能同时接受两个有效 Leader 的写入;可用目标则允许租约过期后短暂暂停。协调存储提供线性一致的 CAS,竞选者写入自己的身份和递增任期,Leader 以心跳续租。每次副作用携带任期 token,下游用条件更新拒绝旧 token。若网络分区、GC 停顿或续租确认丢失,实例停止工作并在重新确认后竞选;不能靠睡眠几秒继续执行。Raft 用任期和多数派投票解决日志领导权,Kubernetes Lease 更像轻量协调记录;两者都要求把存储一致性、超时比例和故障恢复作为设计的一部分。验收会注入 Leader 崩溃、分区、时钟偏移、长暂停和协调存储不可用,检查是否出现双写、旧 token 写入和恢复时间超标。
常见错误
- 只在内存中设置锁或依赖 Redis 最终一致读,却宣称不会双主。
- 只比较时间戳,没有递增任期和 fencing token。
- 续租失败后继续处理当前批次,给旧 Leader 留下写入窗口。
- 把“同一时刻一个 Leader”误写成“永远没有重复执行”。
- 只测正常选举,不测试 GC 暂停、分区、读写延迟和存储故障。
追问及应对
租约过期是不是等于旧 Leader 一定停止?
不是。进程可能暂停、网络隔离后仍运行,所以必须在业务入口验证 fencing token;租约过期只表示协调层不再承认它。
如何选择租期和续租间隔?
用故障检测目标、跨区 p99 延迟、调度暂停和存储抖动预算推导,并保留多个续租周期的余量。上线后持续观察续租失败率和选举抖动,不能照搬固定毫秒数。
协调存储不可用时怎么办?
停止启动新的副作用,保留只读或已安全提交的结果;恢复后重新读取任期并竞选。若业务必须持续运行,就需要明确降级为分片、多活或人工接管,并重新定义安全证明。