您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
深入理解分布式共识算法 Raft
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
深入理解分布式共识算法 Raft
wy****
2025-11-20
IP归属:北京
105浏览
“不可靠的网络”、“不稳定的时钟”和“节点的故障”都是在分布式系统中常见的问题,在文章开始前,我们先来看一下:如果在分布式系统中网络不可靠会发生什么样的问题。 有以下 3 个服务构成的分布式集群,并在 server\_1 中发生写请求变更 A = 1,“正常情况下” server\_1 将 A 值同步给 server\_2 和 server\_3,保证集群的数据一致性:  但是如果在数据变更时发生网络问题(延迟、断连和丢包等)便会出现以下情况:比如有两个写操作同时发生在 server\_1 或 server\_3 上,即便两个写操作有先后顺序,但可能由于网络延时导致各个服务中数据的不一致:  同样地情况,如果在 server\_1 上发生三次写操作,在数据同步的过程中因为网络延时或网络丢包也可能会导致数据的不一致:  那么为了避免以上这些集群间数据不一致的问题,便需要分布式共识算法来协调。分布式共识算法简单来说就是如何在多个服务器间对某一个值达成一致,并且当达成一致之后,无论之后这些机器间发生怎样的故障,这个值能保持不变。本篇文章我们便对 Raft 算法进行介绍。 ### 理解 Raft 算法 了解和学习过 Zookeeper 的同学可能听说过 Zab 算法,它用来保证 Zookeeper 中数据的 **顺序一致性**。Raft 也是一种分布式共识算法,它易于理解和实现,用于保证数据的 **线性一致性**,是最强一致性模型。 在遵循 Raft 算法的集群中,节点会有 3 种不同的角色。当集群在初始化时,每个节点的角色都是 Follower 跟随者,它们会等待来自 Leader 节点的心跳。因为此时并没有 Leader 节点,所以会等待心跳超时。等待超时的 Follower 节点会将角色转变为 Candidate 候选者,触发一次选举,触发选举时会标记 Term 任期变量,并将自己的一票投给自己,通知其他 Follower 节点发起投票。经过投票后,收到超过半数节点票数的 Candidate 节点会成为 Leader 领导者节点,其他节点为 Follower 跟随者节点,Leader 节点会不断地发送心跳给 Follower 节点来维持领导地位:  如果每个节点每次在触发选举时都是同时超时,这样是不是导致不能完成一次选举,产生 **“活锁”** 问题?的确可能,不过活锁问题也很好解决:即节点超时时间在合理的范围内取随机值,这样由于它的随机性就不太可能再同时发起竞选了,这个时候其他节点便有足够的时间向其他节点索要选票。 #### 写变更请求 当发生写变更请求时,由 Leader 节点负责处理,即使是请求到 Follower 节点,也需要转发给 Leader 节点处理。当 Leader 节点接收到写请求时,它并不立即对这个请求进行处理,而是先将请求信息 **按顺序追加到日志文件中(WAL: write-ahead-log)**,如图中标记的 log\_index 表示追加到的最新一条日志的序号:  在这个过程中,**日志必须持久化存储**。随后,Leader 节点通过 RPC 请求将日志同步到各个 Follower 节点,当超过半数节点成功将日志记录时,便认为同步成功。在这里可知 Raft 算法采用的是单主复制的模型,所以它也就会存在以下缺点: 1. 面对大量写请求负载时系统比较难扩展,因为系统只有一个主节点,写请求的性能瓶颈由单个节点决定 2. 当主节点宕机时,从节点提升为主节点不是即时的,可能会造成一些停机时间 随后,Leader 节点会更新最新同步日志的索引 commit\_index 为 1,并通过心跳下发给各个 Follower 节点:  在这个过程中可以发现 Follower 节点只是听从并响应 Leader 节点,没有任何主动性。现在,已经完成了日志在集群间的同步,但是请求对变量 A 的修改还没有被应用(Apply)。Apply 是在 Raft 算法中经常出现的一个名词,在多数与 Raft 算法相关的文章中经常会看到 “将已提交的日志条目应用到状态机” 等类似的表述。其实 “状态机” 理解起来并不复杂,通俗的理解是 **业务逻辑的载体** 或 **业务逻辑的执行者**,它的职责包括: 1. 接收来自日志文件中有序的命令 2. 执行具体的业务逻辑,在本次写请求中,业务逻辑指的便是变更 A 的值 3. 变更应用程序的状态 4. 返回执行结果 更加通俗的讲就是 **让请求生效**。将已经提交的日志应用到状态机是比较简单且自主的过程,各个服务实例会记录 apply\_index 来标记应用索引,当 apply\_index 小于 commit\_index 时,那么证明日志文件中记录的请求信息还有部分没生效,所以需要按顺序应用,直到 apply\_index = commit\_index:  在这个过程中,我一直在强调 **“按顺序”**,不论是日志的追加还是日志的被应用都是按顺序来的,因此才能保证数据的线性一致性。 #### 读请求 Raft 集群处理读请求会保证读请求的线性一致性,所谓线性一致性读就是在 t1 的时间写入了一个值,那么在 t1 之后,读一定能读到这个值,不可能读到 t1 之前的值,在 Raft 算法中实现线性一致性读有以下两种方式: ##### ReadIndex Read 在这种方式下,当 Leader 节点处理读请求时:  1. 首先将 commit\_index 记录到本地的 read\_index 变量里 2. 向其他节点发送一次 Heartbeat,确认自己仍然是 Leader 角色 3. Leader 节点等待自己的状态机执行,直到 apply\_index 超过了 read\_index,这样就能够安全的提供线性一致性读了 4. Leader 执行 read 请求,将结果返回 在第三步中,保证 `apply_index >= read_index` 是为了保证所有小于等于 `read_index` 的请求都已经生效。 如果是 Follower 节点处理读请求也和以上过程类似,当 Follower 节点收到读请求后,直接给 Leader 发送一个获取此时 read\_index 的请求,Leader 节点仍然处理以上流程然后将 read\_index 返回,此时 Follower 节点等到当前的状态机 apply\_index 超过 read\_index 后,就可以返回结果了。 ##### Lease Read 因为 ReadIndex Read 需要发送一次 Heartbeat 来确认 Leader 身份,存在 RPC 请求的开销,为了进一步优化,便可以采用租约(Lease)读。租约其实指的是 Leader 节点身份的过期约定时间,所以这种读请求只针对 Leader 节点,Follower 节点没有租约的概念,它通过以下公式计算: `lease_end = current_time() + election_timeout / clock_drift_bound` 其中 election\_timeout 为选举的超时时间,clock\_drift\_bound 表示时钟漂移,指的是在分布式系统中,两个或多个节点上的时钟以不同的速率运行,导致它们之间的时间差随时间不断累积和变化(也就是分布式系统中不稳定的时钟问题)。 举个简单的例子,假如选举过期时间是 10s,时钟漂移为 1.1,那么租约过期时间为:lease\_end = current\_time() + 10s / 1.1 ≈ current\_time() + 9s,如果在处理读请求时,在租约时间内,则无需发送 Heartbeat 来明确 Leader 身份,直接等待 apply\_index >= commit\_index 后返回请求结果。 *** 在以上读写流程中,Raft 分布式共识算法能让每个节点对日志的值和顺序达成共识,每个节点都存储相同的日志副本,使整个系统中的每个节点都能有一致的状态和输出,使得这些节点看起来就像一个单独的,高可用的状态机。在上文中我们提到过 Zookeeper 使用的 Zab 共识算法保证的是顺序一致性,Raft 算法保证的是线性一致性,所以借着这个引子也来谈谈我对一致性的理解。 #### 一致性 **一致性** 通常指的就是数据一致性,在分布式系统中的读写请求,表现得像在单机系统上一样,符合直觉和预期。一致性模型有很多种,在这里我们只谈以下常见的几种: **线性一致性** 是最强的一致性模型,也被称为强一致性,在 CAP 定理中的 C 表达的一致性含义便是线性一致性。这种一致性模型要求系统要像单一节点一样工作,并且所有操作是原子的,它有两个约束条件: 1. 顺序记录中的任何一次读必须读到最近一次写入的数据 2. 顺序记录要跟全局时钟下的顺序保持一致 **顺序一致性** 要比线性一致性弱,它只要求 **同一客户端或进程的操作在排序后保持先后顺序不变**,但 **不同客户端之间的先后顺序是可以任意改变的**,顺序一致性与线性一致性的主要区别在于 **没有全局时间的限制**。比如在社交网络场景下,一个人通常不关注他看到的所有朋友的帖子的顺序,但是对于某个具体朋友,仍然以正确的顺序显示帖子的顺序。 **因果一致性** 则是比 **顺序一致性** 更弱的一致性模型,因果一致性要求必须以相同的顺序看到因果相关的操作,而没有因果关系的并发操作可以被不同的进程以不同的顺序观察到。典型的例子就是社交网络中发帖和评论的关系:必须先有发帖才能对该帖子进行评论,所以发帖操作必须在评论操作之前。 **最终一致性** 是常见的最弱的一致性模型,所谓最终表达的含义是“对于系统到达稳定状态并没有硬性要求”,即便这听起来很不靠谱,但是在业务中被应用的很多也很好,而且这种一致性模型能使系统的性能很高。 > CAP 定理:C 代表一致性,当客户端访问所有节点时,返回的都是同一份最新的数据;A 代表可用性,指每次请求都能获取到非错误的响应,但不保证获取的数据是最新的;P 代表分区容错性,节点之间由于网络分区而导致消息丢失的情况下,系统仍能正常运行。 接下来我们再来谈谈脑裂问题: #### 脑裂问题 当集群中发生网络通讯问题时,读、写请求只能在超过半数节点的集群内生效,**过半数机制** 在数学上保证不可能同时存在两个Leader:  除此之外还有以下机制来避免脑裂问题: 1. **Term机制**:时间上保证旧Leader会自动让位给新Leader 2. **主动stepDown**:Leader无法联系到过半数节点时主动放弃领导权 3. **严格的投票规则**:每个term每个节点只能投票给一个候选人 当网络问题恢复时,Follower 节点能通过 Leader 节点的日志同步重新追回期间错过的数据。此外,一般采用 Raft 算法的集群在部署的时都是 **“奇数个节点”**,而不是偶数个节点,这其实是数学的体现,性价比更高:  如上图所示,虽然部署 4 个节点多出一个节点,但是和 3 节点集群相比,容错能力是相同的:只能容忍 1 个节点故障。在容错能力没有被提高的情况下又花费了更多的服务器成本和运维管理成本。 *** 以上我们基本了解了 Raft 算法的内容,如果想使用 Raft 算法,对系统模型有以下要求: 1. 服务可能宕机、停止运行,但过段时间能够恢复,但不能存在 **拜占庭故障** 2. 消息可能丢失、延迟乱序或重复;可能有网络分区,并在一段时间之后恢复 *** ### 巨人的肩膀 * [SOFAJRaft](https://www.sofastack.tech/projects/sofa-jraft/overview/) * [The Raft Consensus Algorithm](https://raft.github.io/) * [TiKV 功能介绍 – Lease Read](https://cn.pingcap.com/blog/lease-read/) * 《深入理解分布式系统》
上一篇: RAG 分块策略:从原理到实战优化,喂饭级教程不允许你踩坑
下一篇:深入理解软件设计:什么是好的架构?
wy****
文章数
47
阅读量
22174
作者其他文章
01
高性能MySQL实战(一):表结构
最近因需求改动新增了一些数据库表,但是在定义表结构时,具体列属性的选择有些不知其所以然,索引的添加也有遗漏和不规范的地方,所以我打算为创建一个高性能表的过程以实战的形式写一个专题,以此来学习和巩固这些知识。1. 实战我使用的 MySQL 版本是 5.7,建表 DDL 语句如下所示:根据需求创建 接口调用日志 数据库表,请大家浏览具体字段的属性信息,它们有不少能够优化的点。CREATE TABLE
01
缓存之美:从根上理解 ConcurrentHashMap
本文将详细介绍 ConcurrentHashMap 构造方法、添加值方法和扩容操作等源码实现。ConcurrentHashMap 是线程安全的哈希表,此哈希表的设计主要目的是在最小化更新操作对哈希表的占用,以保持并发可读性,次要目的是保持空间消耗与 HashMap 相同或更好,并支持利用多线程在空表上高效地插入初始值。在 Java 8 及之后的版本,使用 CAS 操作、 synchronized
01
缓存之美:万文详解 Caffeine 实现原理(上)
由于神灯社区最大字数限制,本文章将分为两篇,第二篇文章为缓存之美:万文详解 Caffeine 实现原理(下)文章将采用“总-分-总”的结构对配置固定大小元素驱逐策略的 Caffeine 缓存进行介绍,首先会讲解它的实现原理,在大家对它有一个概念之后再深入具体源码的细节之中,理解它的设计理念,从中能学习到用于统计元素访问频率的 Count-Min Sketch 数据结构、理解内存屏障和如何避免缓存伪
01
由 Mybatis 源码畅谈软件设计(六):Interceptor 拦截器的设计
本节我们来介绍 Mybatis 的拦截器 Interceptor,它依靠 @Intercepts 和 @Signature 注解驱动,配置拦截器的切入方法,这种声明方式非常直观,能够准确的知道每个拦截器的作用范围。而且它是非侵入性的,采用了 动态代理模式,在不修改原有逻辑的前提下便能实现功能的扩展,遵循 开闭原则。Spring 框架中的 AOP 也采用的是同样的思想,但是它引入了很多概念(切面、连
wy****
文章数
47
阅读量
22174
作者其他文章
01
高性能MySQL实战(一):表结构
01
缓存之美:从根上理解 ConcurrentHashMap
01
缓存之美:万文详解 Caffeine 实现原理(上)
01
由 Mybatis 源码畅谈软件设计(六):Interceptor 拦截器的设计
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号