6.3.2 日志复制
前面的介绍中,笔者已经阐述过:Raft 的本质就是多个副本的日志数据达成一致的解决方案。
理解日志复制的问题之前,我们得先搞清楚 Raft 中的日志和日志项是什么。
分布式系统中有一种常见的复制状态机的抽象,就是把具有一定顺序的一系列动作抽象成一条日志(log),每个动作都是日志中的一个日志项(log entry)。我们可以把 Raft 中的日志项理解为包含以下几个关键数据的数据格式:
- 指令: 一条由客户端请求转换成状态机需要执行的指令。
- 索引值:日志项对应的整数索引值,用于标识日志项,是一个连续、单调递增的整数。如此,Raft 可以不用关注空洞日志,也可以通过最大日志索引定位缺失的数据。
- 任期编号:创建这个日志项的 Leader 任期编号。
图 6-17 日志项概念
1. 日志复制
Raft 是强 Leader 模型的算法,日志项只能由 Leader 复制给其他成员,这意味着日志复制是单向的,Leader 从来不会覆盖本地的日志项,即所有的日志项以 Leader 为准。
当 Raft 收到客户端的指令时(x<-3):
- 如果当前节点不是 Leader,则把指令转发至 Leader;
- Leader 收到指令后先写入本地的日志仓库,然后向所有的 Follower 广播日志复制消息。
- 一旦 Leader 确认该指令充分复制(即集群半数以上的节点确认它们的日志仓库已经成功复制了该指令),该指令就会被 commit。在 raft 集中中 commit 的日志即被认为安全的日志。
- Leader 更新 commit 水位,在随后的心跳或者日志复制请求消息中携带 commit 水位。Follower 收到心跳后更新本地的 commit 水位。
图 6-17 日志项概念
选择节点的数量
Raft 的日志复制需要等待多数派节点确认。节点越多,复制延迟也相应增加。所以说,以 Raft 构建的分布式系统并不是节点越多越好。如 ETCD 推荐使用 3 个节点。对高可用性要求较高,且能容忍稍高的性能开销,可增加至 5 个节点。如果超出 5 个节点,则得不偿失。
通过心跳或者下一次日志复制请求消息来通知 Follower 提交(committed)日志项。这种做法可以使协商优化成一个阶段,降低处理客户端请求一半的延迟。
2. 实现日志的一致性
当一个 Follower 新加入集群或 Leader 刚晋升时,Leader 并不清楚需要同步哪些日志给 Follower。此外,当旧的 Leader 转变为 Follower 时,可能携带上一任期(term)中仅在本地提交的日志项,而这些日志项在当前新的 Leader 上并不存在。
Raft 算法中,通过 Leader 强制 Follower 复制自己的日志项,来处理不一致的日志。具体包括两个步骤:
- Leader 通过日志复制 RPC 的一致性检查,找到 Follower 与自己相同日志项的最大索引值。即在该索引值之前的日志,Leader 和 Follower 是一致的,之后的日志就不一致了;
- Leader 强制将 Follower 该索引值之后的所有日志项删除,并将 Leader 该索引值之后的所有日志项同步至 Follower,以实现日志的一致。
因此,处理 Leader 与 Follower 日志不一致的关键是找出上述的最大索引值。
Raft 引入两个变量,来方便找出这一最大索引值:
- prevLogIndex:表示 Leader 当前需要复制的日志项,前面那一个日志项的索引值。例如,下图,如果领导者需要将索引值为 8 的日志项复制到 Follower,那么 prevLogIndex 为 7
- prevLogTerm:表示 Leader 当前需要复制的日志项,前面一个日志项的任期编号。例如,下图,如果领导者需要将索引值为 8 的日志项复制到 Follower ,那么 prevLogTerm 为 4
{
"term": 5,
"leaderId": "leader-123",
"prevLogIndex": 8,
"prevLogTerm": 4,
"entries": [
{ "index": 9, "term": 5, "command": "set x=42" },
],
"leaderCommit": 7
}
图 6-19 领导者处理不一致日志
Leader 处理不一致的具体过程分析如下:
- Leader 通过日志复制 RPC 消息,发送当前自己最新日志项给 Follower,该消息的 prevLogIndex 为 7,prevLogTerm 为 4。
- 由于 Follower 在其日志中,无法找到索引值为 7,任期编号为 4 的日志项,即 Follower 的日志和 Leader 的不一致,故 Follower 会拒绝接收新的日志项,返回失败。
- 此时,Follower 在其日志中,找到了索引值为 6,任期编号为 3 的日志项,故 Follower 返回成功。
- Leader 收到 Follower 成功返回后,知道在索引值为 6 的位置之前的所有日志项,均与自己的相同。于是通过日志复制 RPC ,复制并覆盖索引值为 6 之后的日志项,以达到 Follower 的日志与 Leader 的日志一致。
图 6-20 Leader 处理不一致日志过程
从上面的步骤看到,Leader 通过日志复制 RPC 消息的一致性检查,比较 index 和 term,从而找到 Follower 节点上与自己相同日志项的最大索引值,然后复制并更新该索引值之后的日志项,实现各个节点日志自动趋于一致。