ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Raft一致性协议详解:从选举到日志复制及工程实践

Raft一致性协议详解:从选举到日志复制及工程实践 如果你接触过分布式系统一定绕不开这个经典问题一堆机器组在一起要对外表现得像一台机器多副本之间怎么保持一致Raft算法就是目前被大规模落地的一致性协议之一etcd、Consul、TiKV这些你耳熟能详的组件底层核心逻辑靠的都是它。这篇教程我会用“脑补图解”的方式把Raft从Leader选举、日志复制、安全机制到工程实践全部拆开适合刚接触大数据和分布式系统的同学也适合那些读过点理论但想把细节串起来的人。写这篇东西之前我先说句大实话Raft的论文只有16页但真正吃透它的人不多因为网上大多数教程只讲了“选主”和“复制日志”这两件事却忽略了背后的安全约束。而恰恰是这些约束决定了Raft在断电、断网、节点宕机的混乱场景下能不能保证数据不错不乱。下面我开始讲正事。1. 为什么大数据系统离不开Raft从一致性难题说起1.1 多副本到底解决了什么问题先想象一个最简单的场景你有一台数据库服务器存着用户订单。某天服务器硬盘坏了数据全丢。这时候你才意识到靠单机扛所有数据是非常脆弱的。最简单的解法是复制多放几台机器每台都存一份完整数据。一台挂了另外几台还能顶上。副本多了以后新的问题立刻出现多个副本之间怎么保持“同一个状态”客户端往A机器写入“订单金额100”B机器怎么知道自己也要改成100如果B机器没收到同步消息用户下次访问B时读到的还是旧的订单金额这时候系统就出问题了。这就是分布式系统里“一致性”的基本面多个节点之间数据要收敛到同一个结果而且对外要表现得像是只有一个副本。Raft正是用来解决这个问题的一类协议——一致性协议。1.2 两台机器都觉得自己是老大脑裂的根源很多人会想那我让所有副本都接收写请求每个节点写入后互相通知一下不就行了吗现实中这条路走不通原因很经典叫“脑裂”。想象两个副本A和B中间网络断开了但两台机器本身都活着。客户端1还在A上正常读写客户端2连上了BB也觉得自己活得好好的。这时候系统里同时出现两个“主”两边都在接受写请求数据立刻分叉。等网络恢复谁也说服不了谁改自己的数据因为两边都干了很多“真事”。破解脑裂的办法也很古老多数派。三个节点的集群网络分区把集群分成“1个节点”和“2个节点”两部分只有拿到超过一半节点支持的“老大”才算合法。这样一侧只有一个节点拿不到多数派自然不敢接受写操作另一侧有两个节点成了合法的少数服从多数。这就是Raft最核心的思路基础多数派仲裁。1.3 Paxos的教训大神们写得看不懂工程拿去也不好用聊Raft之前不得不提它的前辈Paxos。Paxos在理论上是完备的也是很多年间分布式系统教科书里的一尊神。但Paxos有个致命短板太难理解而且论文里的算法骨架离工程实现太远。Lamport本人当年用拜占庭将军故事打比方结果多数人连故事都没看懂。后续做工程的人各自为战同一个Paxos在各家实现里千差万别正确性很难验证。Raft的出现就是为了治疗这个痛点。它的作者Diego Ongaro和John Ousterhout在论文里开宗明义设计目标不是发明一套新的共识算法而是让共识算法能被更多人理解。Raft把问题拆成了几个相对独立的子问题Leader选举、日志复制、安全性、成员变更。每个子问题都有明确的机制和规则你可以一项一项学不用一上来就啃高深的数学证明。这也是为什么现在提到“入门一致性协议”大家第一推荐基本都是Raft。2. Leader选举Raft的决策核心是怎么运转的2.1 三种角色与状态转换模型Raft把集群里的所有节点划分为三种状态Leader领导者、Follower跟随者、Candidate候选人。正常情况下每轮任期内集群只有一个Leader其余节点全是Follower。Follower被动接收来自Leader的消息不主动发起任何写操作Leader负责接收客户端请求、分发日志、推进提交Candidate是Follower在选举超时后进入的临时状态用来争取下一任Leader的位置。这三个状态形成一个环Follower - Candidate - Leader ^ | |________ 任期结束/发现更高任期 ________|文字描述一下流程所有节点启动时都是Follower维护一个任期号Term。Follower如果在一个随机的超时时间内没有收到Leader的心跳AppendEntries RPC就认为Leader可能挂了于是自增Term切换到Candidate并向其他节点发起投票请求。如果获得多数派支持Candidate升为Leader如果发现收到的响应里带着更高的Term说明有别的节点在发起新一轮选举自己会让步转为Follower如果票被几个候选人瓜分没有人拿到多数票候选人会等待超时后再次发起选举并随机重置超时时间以减少再次冲突的概率。2.2 Term任期和随机化超时是Raft聪明的时钟Term是Raft的一张逻辑时钟从1开始递增每次发生选举任期号就加一。Term在Raft里是一个极其重要的标尺Leader发心跳会带上自己的TermFollower收到更高Term的请求立刻承认新主节点发投票请求时也带上当前Term别人一看就知道这是哪一轮选举。可以说Term贯穿了整个Raft的每一个判断。选举超时怎么设置Raft论文建议的时间是150ms到300ms之间随机选取。为什么一定要随机因为如果所有Follower同时超时它们会同时变成Candidate同时互相投票票数永远被摊分集群永远选不出Leader。而随机的超时时间让每个节点重新发起选举的时刻错开多数时候只有一个节点先发起选举它最容易拿到多数票。需要特别提醒的一点是心跳间隔必须显著小于最小选举超时。生产环境中常见设置是Leader每100ms发一次心跳而选举超时在300ms到500ms之间随机。这样正常运行时Follower会被心跳不断刷新超时计时器不至于频繁触发选举。我见不少新手实验时把心跳间隔和选举超时设得差不多结果集群不断“选新主”根本跑不稳。2.3 RequestVote投票流程和日志比较规则Candidate发起选举时会并行给集群内所有其他节点发送RequestVote RPC里面带两个关键信息最后一条日志的任期号lastLogTerm和最后一条日志的索引lastLogIndex。这两个字段决定了候选人有没有资格当选。Follower收到投票请求后按两层条件判断如果自己的投票记录里本轮Term已经投给过别人那这一票不能给。如果候选人的日志不如自己“新”那这一票也不能给。什么叫“日志更新”比较规则很简单先比较lastLogTerm谁的任期号更大谁更新如果任期号相同谁的lastLogIndex更大谁更新。这个规则背后是安全性的一个核心约束我后面单独展开。这里先记住结论Raft的选举不是看谁嗓门大而是看谁的日志更接近完整状态。2.4 平票场景与Pre-Vote机制极端情况下还是会出现平票一次心跳断了两个Follower同时进入候选状态集群分成了两组各投各的。Raft应对平票的招数就是前文说的随机超时这次没选出来下个随机超时到来时某个节点先发起新一轮选举通常就能打破僵局。这个机制简单粗暴但有效也是Raft亲民的一个体现。工程实现里很多成熟框架还会额外加一个PreVote机制。PreVote的逻辑是所有节点进入选举之前先“预投票”Candidate向别人问如果我现在发起选举并且自增Term你们会支持我吗只有获得了多数预选票才真的发起选举。这么做的意义在于一个被网络分区隔离的节点短暂恢复后又马上断开如果没有PreVote它会反复自增Term打断整个集群正常的工作。PreVote避免了这种无意义的Term膨胀。这个点论文里没有强制但实际做分布式存储时基本必加。3. 日志复制与提交让所有节点保持一致的关键链路3.1 一条写请求的完整旅程集群选出了Leader客户端写请求进来后数据是怎么从一个点扩散到所有节点的我按顺序拆一遍客户端把写请求发给Leader。Leader将请求追加为本地的日志条目Log Entry此时日志处于“未提交”状态不能应用。Leader并行向所有Follower发送AppendEntries RPC里面带上新日志条目。每个Follower收到后先做合法性检查通过则把日志追加到自己本地然后返回成功。Leader收到超过半数节点的成功响应后将这条日志标记为“已提交”状态机应用这条日志然后向客户端返回写成功。Leader在下一轮心跳里顺带把commitIndex广播给所有FollowerFollower也在此时应用日志到状态机。这个有序链路是Raft一切正确性的基石。注意第5步里“超过半数”是关键只要多数派把日志落盘了即使少数派丢失了这条日志多数派内部依然能保证这条数据在后续选举中存活。3.2 日志条目长什么样term、index、command每个日志条目实际上就是一个三元组信息Term这条日志产生时的Leader任期号。Index日志在整体序列里的位置编号。Command具体的操作内容比如“set key5”。Term和Index合在一起给每条日志在全局空间中一个唯一的坐标。Raft的日志匹配特性是这么描述的如果两个节点上某条日志的Term, Index相同那么在它之前的所有日志一定也相同。这个特性是Raft能高效同步日志而不用全量对账的基础。为了维护这个特性Leader向Follower追加日志时AppendEntries RPC里还带了previousLogTerm和previousLogIndex。意思是请你在覆盖我之前先检查你的日志序列在指定位置是否和我一致。如果Follower发现自己的prevLogTerm对不上会直接拒绝这条请求。Leader收到拒绝后把自己要发送的日志往前回退一格重新尝试直到找到一个共同匹配点再从这个点往后批量追加日志。整个过程类似一次回溯式的对齐。3.3 提交的秘密多数派确认才commit提交Commit是Raft里最容易被误解的概念之一。日志被“写入”某个节点并不代表它被“提交”了。被提交的定义是Leader确认这条日志已经被复制到多数派节点上。只有已提交的日志才能应用到状态机也才能向客户端返回成功。这里有个很微妙的规则Leader不能想当然地提交旧任期遗留下来的“未提交日志”。假设当前是任期5Leader发现任期4有一条日志已经复制到多数节点但它直接把它标成已提交这可能是错的——因为这条旧任期日志在更早的任期里可能没有被复制到某几个关键节点而随着新的写入这条日志有可能被未来某个日志覆盖。Raft论文里给出的精确规则是只有“当前任期的日志”被复制到多数派Leader才能用当前任期的日志提交顺带把前面旧任期的未提交日志一并提交。判断能不能提交必须依赖当前任期的一次多数派确认来做担保。这个规则初看有点绕但背后逻辑其实是最终提交一定得经过一次“现任领导确认”避免半路杀出个旧数据把它覆盖掉。理解了这一点你就基本摸到了Raft正确性的命门。3.4 读请求别以为读Leader就安全Raft的写路径我们讲清楚了但读请求一样有坑。如果客户端直接读Leader节点上的状态机拿到的结果不一定是线性一致的。原因很简单Leader可能在网络分区的角落里已经不算多数派了但它自己还不知道。此时旧的Leader上还留着旧数据客户端来读读到的是过期的数据这在需要强一致性的系统里是绝对不允许的。解决读一致性的通用方案是ReadIndexLeader接到读请求后先向多数派确认自己仍然是本任期有效的Leader然后等自己状态机应用到了当前commitIndex再返回本地状态机的结果。另一个常见优化是Lease Read即利用心跳租约在租约有效期内跳过多数派确认直接读本地状态机。Lease读性能好但对时钟稳定性有要求如果节点间时钟跳跃过于夸张租约判断可能出错。生产环境中并不是所有系统都开Lease读一些强约束场景宁可损失性能也要保证线性一致性。4. 安全性设计Raft如何避免脑裂和数据丢失4.1 Raft的三条安全规则很多讲Raft的文章把选举和日志复制讲完就收工但少了安全性这部分你对Raft的理解永远是残缺的。Raft能够保证正确性靠的是两条贯穿所有机制的安全规则加上一条一致性前提选举限制只有包含全部已提交日志的节点才有资格当选Leader。日志提交规则旧任期的日志不能独立进行提交必须依赖当前任期的一次多数派确认。Leader完整性一旦日志在某个任期被提交它一定存在于未来所有任期的Leader日志中。这三条合起来保证了“已提交的数据永不丢失”和“所有节点以相同顺序应用日志”这两个核心性质。用大白话讲只要你的数据被系统确认“提交成功”那么之后不管集群怎么折腾、怎么分离、怎么换主这条数据都丢不了。4.2 经典的反例为什么日志旧的节点选不上我用一个典型场景让你感受一下选举限制的意义。假设集群有5个节点A是任期4的Leader它收到了“keyvalue”这条写请求并成功把它复制到了B和C两个节点。此时A、B、C三条日志有这条数据但D、E还没有。这条日志已经算多数派复制A会把它标记为已提交并返回客户端成功。随后A宕机。E变成了Candidate发起任期5的选举。E的日志里根本没有“keyvalue”这条日志按我们反复提过的“日志新旧比较”规则D、E自己的日志都比E“新”吗不一定。但B和C手里有已提交的日志而这部分日志的lastLogIndex比E大所以B、C会拒绝给E投票。最终E拿不到多数票选举失败。B或C由于日志较新更可能在下轮选举中当选。如果当初E靠着较低的门槛当选了Leader它就缺少一条已提交日志后面它当选后开始覆盖其他节点日志而恰恰被覆盖的日志里包含那条已经确认给用户成功的“keyvalue”数据就这样在用户眼皮底下消失了。Raft通过选举限制硬性堵死了这条路。4.3 网络分区下的行为Raft如何保证只有一个主现在可以把脑裂场景完整梳理一遍。假设集群3个节点A原本是Leader网络分区把A单独隔开B和C成为另外大半区。分区瞬间A还在任期内继续向客户端提供服务但它发出的心跳到不了B、C因此AppendEntries得不到任何响应。A只有自己一个节点复制“成功”的日志远不够多数派于是A无论如何都不能提交新数据写请求在A这边的表现就是卡住或超时。而在另一边B、C收不到心跳后开始选举B或C拿到两票中的多数支持在一轮短暂混乱后选出了新的Leader继续正常服务。客户端写这边日志都能正常提交。当分区恢复旧Leader A发现新Leader的Term比自己的高就会认怂转为Follower并把自己那些未提交的日志统统回滚。对外表现就是分区期间旧Leader那边没写成功的数据最终不会留在系统里而通过新Leader提交的数据一条不会丢。这就是Raft应对脑裂的完整答案不是阻止脑裂发生而是用多数派和Term机制让脑裂中的孤立侧完全无法提交数据让大脑天然地归向多数派一侧。5. 集群成员变更、日志压缩与快照生产环境绕不开的进阶话题5.1 成员变更的危险为什么把配置改来改去会出大问题一个分布式集群不会永远固定5个节点扩容、缩容、节点替换都需要改集群配置也就是“当前集群有哪些成员”。但配置变更不能随意执行原因是一个典型的重叠多数问题。假设现在集群是3节点配置A是{c1, c2, c3}你要改成配置B是{c1, c2, c3, c4, c5}。如果直接把c1实例上的配置从A改成B而c2、c3还在用A那么可能出现某个节点按A配置收集到两票另一个节点按B配置也收集到两票。两个不同配置的多数派没有交集就可能同时产生两个Leader。Raft解决这个问题有两种主流方案。最简单的是“单节点变更”每次只调整一个成员比如从3节点变成4节点再从4节点变成5节点。这样新旧配置的多数派之间必然有交集因为旧多数派3个节点中的某些节点一定也在新配置的多数派里。另一种是论文里的Joint Consensus同时使用新旧两个配置的多数派确认变更过程中写请求必须同时满足两套配置的多数派才能提交。Joint Consensus推广性强但实现复杂生产系统里单节点变更基本够用。5.2 日志索引膨胀快照是怎么救场的Raft的每条日志都要落盘复制随着时间推移日志量无限增长。如果不做清理磁盘迟早爆节点间同步日志的耗时也越来越长。解决方案是快照把当前状态机的完整状态保存一份并把之前的所有日志丢弃。快照里除了状态机数据还必须记录两个关键信息最后一条被包含日志的Term和Index。这样新加入的节点或落后太久的节点拿到快照后能判断自己离Leader的最新状态还差多少。对于滞后极严重的节点Leader还会直接发送InstallSnapshot RPC把整个快照传过去而不是逐条重放日志。这里有个工程经验别指望快照能频繁打也别太晚打。快照太频繁磁盘和CPU开销大快照太稀疏重放日志的时间就长。TiKV这类系统的快照间隔和日志保留期限都有专门配置需要根据写入速度、故障恢复时间目标RTO来做权衡。还有一点快照文件的传输通常要压缩网络带宽和磁盘IO都会在打快照瞬间冲高得错峰进行。6. 从理论到落地Raft在工程实践中的常见坑与选型建议6.1 那些你用过但不知道在跑Raft的系统Raft已经渗透到大数据和中间件领域的各个角落。最熟知的几个etcdKubernetes的元数据存储核心存储引擎就是Raft。Consul服务发现和配置管理它的共识部分用的也是Raft。TiKV分布式事务数据库的存储层基于Raft实现多副本强一致支持自动故障转移。CockroachDBNewSQL数据库分布式事务和复制层同样是Raft。SOFAJRaft蚂蚁集团开源的一个生产级Raft实现适合Java系做分布式系统时直接集成。这些系统无一例外选择了Raft而不是自己发明共识协议主要就是图Raft的实现复杂度可控、调试和维护成本低。Paxos系在底层也有大量应用比如Google的内部系统和部分数据库的Paxos实现但一般团队根本不具备从零实现Paxos的条件。工程选型就是这么现实你能搞懂、能维护、能证明它没写错的协议才是好协议。6.2 亲手实验的方法从动画到Lab想真正吃透Raft光看文章是不够的必须亲手跑起来。我推荐这条路径第一步去Raft官网raft.github.io看那个交互式选举动画把Term、心跳、随机超时、票数多数派这些概念先建立直觉。第二步参考MIT 6.824分布式系统课程的Lab用Go或你用着顺手的语言实现一遍Raft核心逻辑。这个Lab在网上有大量资料但建议自己写别直接抄否则完全达不到理解效果。第三步读一个生产级Raft实现的核心代码比如etcd的raft模块或SOFAJRaft看你自己的实现和工程实现之间差了哪些细节比如PreVote、Batch心跳、网络乱序处理、磁盘同步策略。当年自己做Lab最大的体会是写选举不难难的是日志冲突处理和状态转换在并发环境下的正确性。等你写完一遍再回头看论文会发现论文每句话都是有大用意的。6.3 生产环境里那些不写进论文的坑理论归理论生产环境里翻车往往都在不起眼的细节上。我列几个常见的坑超时设置太激进。心跳间隔、选举超时、网络抖动要一起考虑。云环境下网络延迟并不稳定超时设得太小会导致频繁选举设置得太大则故障恢复时间太长。用了系统墙上时钟而不是单调时钟。时钟回拨会让任期和租约判断出错必须使用单调时钟Monotonic Clock。持久化时机不当。Raft要求日志在收到多数派确认前必须持久化。如果为了性能批量写、落盘不及时断电后可能丢失已“提交”的日志。生产实现普遍用批量fsync和group commit来平衡性能与安全。快照和日志截断的边界处理错误。快照Index和日志起始Index之间的衔接不一致会导致后续同步日志时反复失败。Lease读在跨洲跨机房场景下容易出问题。节点间真实时钟偏差超过租约时间就可能读到过期数据强一致场景慎用。最后一个建议不要把Raft当成万能药。它适合数据量可控、需要强一致、节点规模中等通常不超过十几二十个的存储系统。如果你需要做的是大规模海量数据分布式计算关心的是吞吐而不是单写强一致那直接上Raft反而是负担。选型之前先把问题域定义清楚。我个人学习Raft的经历也证明了那句话看图千遍不如动手一遍。这篇文里所有机制只有在自己机器上把节点杀一遍、把网络断一遍、把日志乱一下之后才真正变成你的东西。把文章收藏没有用找一天静下心从那个动画开始一步步走完这条路收获绝对对得起你花的时间。
返回列表