Leader-Follower 领导者追随者模式
周一早上 9 点,运维群里炸了:ZooKeeper 集群的 Leader 节点宕机了。紧接着,所有依赖 ZooKeeper 进行服务发现的微服务开始出现大量超时错误。业务方纷纷打电话过来询问:「服务怎么又不稳定了?」
但老员工却很淡定:「没事,ZooKeeper 会自动选新的 Leader,等一会儿就好。」
果然,3 秒后,选举出了新的 Leader,服务恢复正常。
这个「自动选新 Leader」的能力,正是 Leader-Follower 模式的核心价值。
为什么需要领导者选举
在没有领导者的情况下,多个节点同时处理写操作会导致数据冲突。比如两个节点同时修改同一个配置项,谁的版本是正确的?这种「多主」场景如果没有协调机制,就会陷入「写冲突」的泥潭。
Leader-Follower 模式的核心思想是:所有写操作都交给一个 Leader 处理,其他 Follower 只负责同步数据和处理只读请求。这样就避免了多主冲突,同时可以通过增加 Follower 数量来提高系统的读取吞吐量和容灾能力。
flowchart TB
subgraph 多主冲突["多主(无协调)"]
W1["写请求"] --> N1["节点 A"]
W2["写请求"] --> N2["节点 B"]
N1 -->|"数据冲突"| CONFLICT["数据不一致"]
N2 -->|"数据冲突"| CONFLICT
end
subgraph LeaderFollower["Leader-Follower"]
W3["写请求"] --> L["Leader"]
L -->|"同步"| F1["Follower 1"]
L -->|"同步"| F2["Follower 2"]
L -->|"同步"| F3["Follower 3"]
R1["读请求"] --> F1
R2["读请求"] --> F2
end
Raft 领导者选举过程
Raft 是目前最流行的领导者选举算法之一,它将问题分解为三个子问题:领导者选举、日志复制、安全性。
选举过程
Raft 使用任期(Term)的概念来区分不同的时间段。每个任期以一次选举开始,如果选举成功,当选者将在该任期内担任 Leader。
sequenceDiagram
participant N1 as Node 1
participant N2 as Node 2
participant N3 as Node 3
Note over N1,N3: 初始状态:所有节点都是 Follower
N1->>N1: Leader心跳超时(150ms)
N1->>N2: RequestVote(term=1, candidateId=1)
N1->>N3: RequestVote(term=1, candidateId=1)
Note over N2,N3: 节点 2、3 尚未收到 Leader 心跳
N2->>N1: VoteGranted(term=1)
N3->>N1: VoteGranted(term=1)
Note over N1: 获得多数票,当选为 Leader
N1->>N2: AppendEntries(心跳 + 日志复制)
N1->>N3: AppendEntries(心跳 + 日志复制)
选举规则
Raft 的选举遵循以下规则:
public class RaftNode {
private volatile State state = State.FOLLOWER;
private volatile long currentTerm = 0;
private volatile String votedFor = null;
private volatile List<LogEntry> log = new ArrayList<>();
public RequestVoteResponse handleRequestVote(RequestVoteRequest request) {
// 规则1:如果请求任期小于当前任期,拒绝
if (request.getTerm() < currentTerm) {
return RequestVoteResponse.builder()
.term(currentTerm)
.voteGranted(false)
.build();
}
// 规则2:如果当前节点未投票,或投给了请求者
// 且请求者的日志至少和当前节点一样新
if (votedFor == null || votedFor.equals(request.getCandidateId())) {
if (isLogUpToDate(request)) {
currentTerm = request.getTerm();
state = State.FOLLOWER;
votedFor = request.getCandidateId();
return RequestVoteResponse.builder()
.term(currentTerm)
.voteGranted(true)
.build();
}
}
return RequestVoteResponse.builder()
.term(currentTerm)
.voteGranted(false)
.build();
}
// 日志完整性判断:最后一条日志的任期越大越新,任期相同则日志越长越新
private boolean isLogUpToDate(RequestVoteRequest request) {
int lastLogIndex = log.size() - 1;
long lastLogTerm = lastLogIndex >= 0 ? log.get(lastLogIndex).getTerm() : 0;
return request.getLastLogTerm() > lastLogTerm ||
(request.getLastLogTerm() == lastLogTerm &&
request.getLastLogIndex() >= lastLogIndex);
}
}
超时随机化
Raft 使用随机化的选举超时时间来避免「split vote」(平票)问题。每个节点的选举超时时间在一个固定范围内随机选择(如 150-300ms),这样大部分情况下只有一个节点会先超时并发起选举。
private int electionTimeout() {
// 随机选择 150-300ms 之间的超时时间
return 150 + new Random().nextInt(150);
}
随机化的设计保证了:即使所有节点同时启动,也很少会出现多次选举都无法选出 Leader 的情况。
ZooKeeper 的领导者选举:FastLeaderElection
Apache ZooKeeper 使用 ZAB 协议(ZooKeeper Atomic Broadcast)实现领导者选举和状态同步。FastLeaderElection 是 ZAB 的默认实现,核心流程如下:
- 发现(Discovery):节点收集其他节点的投票,找出拥有最新事务的节点
- 同步(Synchronization):新 Leader 将自己的事务日志同步给 Follower
- 广播(Broadcast):Leader 开始接收并广播事务请求
sequenceDiagram
participant F1 as Follower 1
participant C as Candidate
participant F2 as Follower 2
Note over C: 成为 Candidate,开始选举
C->>F1: 发送 LOOKING 消息(包含 myId, zxid, epoch)
C->>F2: 发送 LOOKING 消息
F1->>C: 返回投票(leaderId=z1, zxid=100, epoch=5)
F2->>C: 返回投票(leaderId=z2, zxid=99, epoch=5)
Note over C: 比较 zxid:100 > 99,z1 更优
C->>F1: 发送 FOLLOW 消息,宣布 z1 为 Leader
C->>F2: 发送 FOLLOW 消息
ZooKeeper 的选举中,zxid(事务 ID)越大表示数据越新,优先选择拥有最大 zxid 的节点作为 Leader。这保证了新 Leader 拥有最完整的事务历史。
Etcd 的 Raft 实现
etcd 是云原生基础设施中常用的分布式键值存储,它直接使用了 Raft 协议实现领导者选举。etcd 的 Raft 实现具有以下特点:
状态机分离:etcd 将 Raft 共识逻辑和网络、存储等模块解耦,通过 Node 接口与状态机交互。
type Node interface {
Tick()
Campaign(ctx context.Context) error
Propose(ctx context.Context, data []byte) error
Step(ctx context.Context, msg pb.Message) error
ApplyConfChange(cc pb.ConfChangeI) *pb.ConfChangeC
// ...
}
线性一致性读:etcd 支持线性一致性读取,保证读取到的是最新 Leader 确认的数据,而非 stale read。
// etcd 客户端的线性一致性读取
resp, err := client.Get(ctx, "key", clientv3.WithSerializable())
// WithSerializable 会走 Leader 确认路径
应用场景
Kafka Broker 协调
Kafka 使用控制器(Controller)机制实现领导者选举。每个 Broker 都有机会成为控制器,但同一时间只有一个 Broker 是活跃的。控制器负责管理分区的 Leader 选举、主题创建删除等操作。
flowchart TB
subgraph Kafka["Kafka 集群"]
C["Controller Broker\n(Leader 角色)"]
B1["Broker 1\n ISR={0,1,2}"]
B2["Broker 2\n ISR={0,1,2}"]
B3["Broker 3\n ISR={0,1,2}"]
end
C -->|"管理分区\nLeader 选举"| B1
C -->|"管理分区\nLeader 选举"| B2
C -->|"管理分区\nLeader 选举"| B3
Note over C: Controller 宕机后,ZooKeeper 触发重新选举
当 Controller 所在的 Broker 宕机后,ZooKeeper 会通知其他 Broker 重新竞选 Controller。新的 Controller 会接管所有分区的元数据,并触发必要的分区 Leader 选举。
Redis Sentinel
Redis Sentinel 是 Redis 的高可用解决方案,它监控主从复制集群的健康状态,并在主节点故障时自动进行故障转移。
flowchart TB
subgraph RedisSentinel["Redis Sentinel 集群"]
S1["Sentinel 1"]
S2["Sentinel 2"]
S3["Sentinel 3"]
end
subgraph RedisCluster["Redis 主从集群"]
M["Master\n(可读写)"]
R1["Replica 1"]
R2["Replica 2"]
end
S1 -->|"监控"| M
S2 -->|"监控"| M
S3 -->|"监控"| M
M -->|"主从同步"| R1
M -->|"主从同步"| R2
Note over S1,S3: Sentinel 使用 Raft 协议选举领导者
S1 -->|"Leader 发起\n故障转移"| M
Sentinel 集群内部使用 Raft 协议选举 Leader,确保只有 Leader 才能发起故障转移,避免「脑裂」(split brain)。
领导者故障转移时间
Leader-Follower 模式的一个关键指标是故障转移时间(Failover Time),即从 Leader 宕机到新 Leader 选举完成的时间间隔。
故障转移期间,系统无法处理写请求。对于延迟敏感型业务,这个「不可用窗口」是需要重点关注的指标。
减少故障转移时间
如果对可用性要求极高,可以考虑以下优化:
- 减少选举超时下限:将超时范围从 150-300ms 调整为 100-200ms(但会增加误判风险)
- Pre-Vote 机制:在真正发起选举前先探查是否会获得多数票,避免不必要的任期增加
- 领导者静默:Leader 定期发送心跳,让 Follower 知道 Leader 仍然存活
思考题
问题 1:Raft 协议中,为什么 Follower 在一个任期内只能投一票?
参考答案
这是为了避免「票数瓜分」问题。如果一个 Follower 可以多次投票,在同一个任期内投给多个候选者,可能导致多个候选者都获得部分票数但都无法达到多数,最终无法选出 Leader。限制一任期一票后,选举只有两种结果:某个候选者获得多数票当选,或没有任何候选者获得多数票(进入下一任期重新选举)。这种设计简化了选举的安全性证明。
问题 2:为什么 Raft 使用随机化的选举超时时间,而不是固定超时?
参考答案
固定超时会引发「split vote」问题:如果所有 Follower 同时超时,它们会同时发起选举,各自获得部分票数,形成僵局。随机化设计打破了这种对称性——通常只有一个节点会先超时并发起选举,获得多数票后成功当选。随机化的范围通常是一个合理的区间(如 150-300ms),既能避免频繁的无效选举,又能保证故障时快速响应。
问题 3:ZooKeeper 和 etcd 在领导者选举上有什么主要区别?
参考答案
主要区别在于协议和一致性模型:ZooKeeper 使用 ZAB 协议,提供线性一致性(linearizable)的写入,但不保证读取的线性一致性(可以通过 sync() 强制读取最新数据);etcd 使用 Raft 协议,默认提供线性一致性读写。在选举机制上,ZooKeeper 依赖 ZooKeeper 自身做选主(自举),etcd 则通过 Raft 协议内置的选举机制。此外,ZooKeeper 是通用协调服务,etcd 更专注于键值存储和配置管理。