关于Raft的一些学习
主要看:MIT6.824 6.3 Raft 初探 和 MIT6.824 6.4 Raft Log同步时序
讲的其实相对于详细,同时前面的 6.1 也讲了脑裂问题,这里可以和之前 Redis 的分布式部署可能会导致脑裂的问题一起看,能更好的理解相关的场景和问题,也能更直观的了解 Raft 作为一个去中心化的分布式共识协议是如何通过自身特性避免相关问题的。
如果想看更详细的话,建议把第 7 章节也看了,会有深入理解。
看完之后,再去看相关于分布式的技术栈(例如 etcd),就会容易理解的多。
补充
这里就简单说一下上面 Redis 脑裂问题吧。
Redis 的脑裂问题
- 主从复制和哨兵机制
Redis 使用主从复制和哨兵机制来实现高可用性。在主从复制模式下,主节点将数据同步到从节点。哨兵节点负责监控主从复制的状态,并在主节点出现故障时自动进行故障转移。
- 脑裂的原因
- 网络分区:当网络分区发生时,哨兵节点可能会错误地认为主节点已经故障,从而触发故障转移。如果此时主节点仍然正常工作,就会出现两个主节点同时对外提供服务的情况,这就是脑裂。
- 哨兵的决策机制:哨兵的决策机制是基于多数投票的。如果哨兵节点之间也发生网络分区,可能会导致多个哨兵集群分别认为主节点已经故障,从而各自选举新的主节点,导致脑裂。
- Redis 的脑裂过程
- 主节点与从节点心跳不一致:当主节点与从节点之间的网络连接中断时,从节点会检测到心跳超时。
- 哨兵触发故障转移:哨兵节点会检测到主节点不可达,从而触发故障转移。哨兵会选举一个新的从节点作为新的主节点。
- 脑裂发生:如果原主节点仍然正常工作,并且与部分从节点保持连接,而新的主节点也与部分从节点保持连接,就会出现两个主节点同时对外提供服务的情况,导致脑裂。
etcd 的无脑裂机制
- Raft 协议
etcd 使用 Raft 协议来保证数据的一致性和可靠性。Raft 协议的核心机制包括:
- 领导者选举:在 Raft 协议中,集群中的一个节点被选举为领导者,所有写操作都通过领导者进行。
- 日志复制:领导者将写操作记录到日志中,并将日志复制到其他节点。其他节点会将日志应用到本地状态机,从而保证数据的一致性。
- 安全性:Raft 协议保证了在任何时刻,集群中只有一个领导者,并且领导者必须通过多数节点的确认才能提交写操作。
- 领导者退位机制
- 领导者检测到网络分区:如果领导者发现自己无法与大多数节点通信,它会退位,不再接受新的写操作。这是为了防止在分区期间出现数据不一致的情况。
- 新的领导者选举:在分区的另一侧,如果某个节点发现自己无法与当前领导者通信,并且认为自己可以与大多数节点通信,它会发起新的领导者选举。如果选举成功,新的领导者会开始接受写操作。
- 避免脑裂的具体机制
- 任期编号的单调递增:每个任期编号是单调递增的,这意味着在任何时刻,集群中只有一个任期编号是有效的。当一个节点收到一个任期编号比自己当前任期编号大的请求投票消息时,它会更新自己的任期编号,并投票给该候选人。这确保了在任何时刻,集群中只有一个领导者。
- 投票机制的限制:每个节点在每个任期内最多只能投票给一个候选人。如果一个节点已经投票给了一个候选人,它不会再次投票给其他候选人,直到该任期结束。这确保了在任何时刻,最多只有一个候选人能够获得大多数节点的投票。
- 日志匹配原则:在 Raft 协议中,日志匹配原则确保了只有当候选人的日志与投票节点的日志一致时,投票节点才会投票给该候选人。这进一步确保了数据的一致性。
另外
Raft 协议只能保证结果一致性,因为不同副本的 Log 或许不完全一样。
Raft 协议先要持久化 Log,过半节点 ACK 之后,才 commit 这条 Log,之后才执行指令。
所以保证结果一致性没啥问题,过程有问题也只是 Log 持久化的时候会有问题。下面会讲场景。
有很多场合都会不一样,至少不同副本节点的Log的末尾,会短暂的不同。例如,一个 Leader 开始发出一轮AppendEntries 消息,但是在完全发完之前就故障了。这意味着某些副本收到了这个 AppendEntries,并将这条新 Log 存在本地。而那些没有收到 AppendEntries 消息的副本,自然也不会将这条新 Log 存入本地。所以,这里很容易可以看出,不同副本中,Log 有时会不一样。
不过对于 Raft 来说,Raft 会最终强制不同副本的 Log 保持一致。或许会有短暂的不一致,但是长期来看,所有副本的 Log 会被 Leader 修改,直到 Leader 确认它们都是一致的。
这里要知道,没过半数节点 ACK 的 Log,就意味着不会被 commit,这条 Log 对应的命令也就不会被执行。
而这条命令是通过 “客户端 → 应用程序 → 应用程序的 Raft 指定节点(Leader)”。
如果命令执行失败,那么客户端等待指令就会超时,就会变成下面做法:
- 客户端设置 RPC 超时(如 3 s);
- 超时未收到响应 → 客户端 SDK 自动重发同一请求(带相同 req_id 幂等);
- 重试请求打到任意节点(通常还是原 Leader 或新 Leader);
- 新 Leader 重新走一遍
Start() → commit → applyCh → 回客户端;- 客户端最终收到结果。

最后补一张 Raft-应用程序 交互的图,学的时候理清思路画的,感觉会有一些问题,但大体是对的,觉得不对的地方看一下 MIT 的原文。
原文:https://mit-public-courses-cn-translatio.gitbook.io/mit6-824/lecture-06-raft1/6.6-ying-yong-ceng-jie-kou
Raft实际应用场景
最后的最后补充一点,因为 Raft 是强一致性协议,导致 Raft 虽然适用于分布式存储系统设计中,但是它并不适合大规模分布式存储系统。
这也是为什么 etcd 使用 Raft 作为一致性协议的原因,etcd 本身多用来存储分布式系统运行的元数据,恰巧这些元数据吞吐量很小,需要一致性强,Raft 协议正合适。例如 K8s 一般利用 etcd 保存一下元数据:
- 谁是 leader
- 哪个节点在线
- 哪些 Pod 存在
- 哪些 service endpoint 存活
- 分布式锁是否持有
- 哪个 controller 当前正在执行某个任务
一旦不一致,Kubernetes 会直接乱套甚至整体崩溃。
RoLingG | 博客
评论(0)