原文链接:http://nil.csail.mit.edu/6.824/2020/papers/raft-extended.pdf
CAP理论与BASE理论
在介绍RAFT之前需要先介绍分布式里经典的CAP理论和BASE理论
CAP 也就是 Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容错性) 这三个单词首字母组合。
- 一致性(Consistence) : 所有节点访问同一份最新的数据副本
- 可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
- 分区容错性(Partition tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫网络分区。
在实际应用中,这三者通常无法同时满足。
更准确的说,在分布式系统中,分区容错性是必须满足的特性,因此实际上分布式系统中通常的模式为CP或者AP。举例而言,本文介绍的RAFT便为满足CP的模型,而ZooKeeper则为满足AP的模型(为了提升读操作吞吐量牺牲了强一致性,选择满足最终一致性)。
为啥无同时保证 CA 呢?
举个例子:若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。
选择的关键在于当前的业务场景,没有定论,比如对于需要确保强一致性的场景如银行一般会选择保证 CP 。
参考链接:掘金
介绍
Raft算法是一种用于管理复制日志的一致性算法。它支持多台机器作为一个集群共同工作来处理容灾问题。
Raft之前也出现过很多一致性算法,例如Paxos、Viewstamp等。与之相比Raft的最大有事就是其好理解。
此外,与之前的算法比起来,Raft存在下面几个创新点
1、Strong Leader:Raft通过leader对数据进行管理,通过设置log只能从leader流向follower来简化管理流程
2、Leader Election:Raft通过一系列精妙的细节保障了leader间信息的同步以及选举的效率
3、Membership Changes:Raft支持集群配置更新
值得注意的是,RAFT性能不如单机,运行速度较慢。其每次请求都需要由leader进行处理,如果client请求发送给了client,client也会把请求重定向到leader这里,导致无法充分利用集群性能,集群其他机器单纯用于容灾。随着机器增多,leader选举需要连接更多机器,获得票数,选拔时间增加,整体时间性能会变差。
原始RAFT也不是在收到日志后立刻开始分发,而是直到心跳时间才进行分发。好处是可以通过batch的方式传递信息,一次传递多个LogEntry,增加效率(每次传递除了固定信息还需要建立连接,报文增加前缀等,成本不低)。缺点则是性能(CAP中的A)会受到影响。
复制状态机
复制状态机(Replicated state machine)用于在分布式系统中处理容灾问题。通过设置不同的副本保障即使主服务器出现问题也不会影响到整体的运行过程。
复制状态机
如上图所示,每台服务器都存储一个包含一系列命令的 日志 ,并且按照日志的顺序执行。每台服务器都顺序执行相同日志上的命令,因此它们可以保证相同的状态 。
一致性算法 的工作就是保证复制日志的相同 。一台服务器上,一致性模块接收 client 的请求命令并将其写入到自己的日志中,它和其他服务器上一致性模块通信来保证集群中服务器的日志都相同 。命令被正确地复制后,每一个服务器的状态机按照日志顺序执行它们,最后将输出结果返回给 client 。
因此,服务器集群看起来就是一个高可用的状态机,只要集群中大多数机器可以正常运行,就可以保证可用性
Raft实现
Raft基本概念
Raft集群通常包含奇数个机器(论文里是5),这样只要不超过一半的机器(2)挂掉就不会影响整体集群的运行。
在任何一个时刻,一个server有三种状态:leader,follower,candidate。Follower单纯接收candidate和leader发送过来的rpc请求而不会主动发起任何请求;Leader用于分发消息以及传递消息给client;Candidate则是follower成为leader的中间态。三者关系如下图所示:
Raft把时间分为terms,每个term开始时都会伴随着选举过程的启动。一个term里最多只有一个leader(当出现split-vote时因为各个candidate都没收到足够的票,导致可能没有leader)。如果没有leader,则会在一定时间后发起新的投票过程开启新的term。
每个server都会保存其当前的term,term之间会进行通信,如果某个term发现别人的term比自己的term大,其就会自动进行更新到新的term。如果leader或candidate发现自己的term不是全局最大的,其会退回到follower状态。
领导人选举
Raft通过heartbeat机制来实现领导人选举。如果一个follower在其自己的election timeout时间段(每个server的timeout不一样)内没有收到leader的heartbeat,他就会增加自己的term,把自己变成candidate。
投票过程为:
1、每个candidate只给自己投票
2、每个server在一个term里只投一次票,投票规则为先到先得。
当candidate获得集群内大部分server的票会转换为leader结束选举;或者当其意识到已经诞生新的leader或者长时间没结果的话选举过程也会结束。
当candidate成为leader时,其会立刻发送heartbeat给集群内全部server宣布自己成为leader这个消息。这时其他candidate会进行判断,如果其term大于等于候选candidate的term,则会得到认可;否则其leader资格会被否决,选举过程继续。
值得特别注意的一点是,为了避免出现split-vote问题(例如集群里全部机器同时转为candidate给自己投票导致没人能获得多数选票选不出leader陷入死循环),Raft采用了随机设置election timeout的方法,保证不同机器的选举时间错开。
日志复制
client会发送执行请求给leader。leader会分发执行log给集群里其他server,当集群里过半的server储存这个log后leader会执行(committed)这个消息,并将结果返还给client。另外,leader会持续与未保存日志的server联络直到集群内全部server都包含了该条日志。
值得注意的是,在commit最新的log时也会执行leader日志队列里之前还未执行的日志。Leader会始终维护highest index of committed log。这样当follower意识到日志已经提交后,会在本地执行对应日志并保存结果。
Raft 使用 日志机制
来维护不同服务器之间的一致性,它维护以下的特性:
- 如果在不同的日志中的两个 entry 拥有相同的 index 和 term,那么他们存储了相同的命令
- 如果在不同的日志中的两个 entry 拥有相同的 index 和 term,那么他们之前的所有 log entry 也全部相同
对于 特性一:leader 最多在一个 term 里,在指定的一个 index 位置创建 log entry ,同时 log entry 在日志中的位置不会改变 。
对于 特性二: AppendEntries RPC
包含了 一致性检验
。发送 AppendEntries RPC
时,leader 会将新的 log entry 以及之前的一条 log entry 的 index 和 term 包含进去,若 follower 在它的日志中找不到相同的 index 和 term ,则拒绝此 RPC 请求 。所以,每当 AppendEntries RPC
返回成功时,leader 就知道 follower 的日志与自己的相同了 。
下图展示了follower和leader对应日志不同的情况,follower可能比leader信息多也可能比leader信息少,甚至可能二者同时出现
上图体现了一些 leader 和 follower 不一致的情况 。a~b 为 follower 有缺失的 log entry ,c~d 为 follower 有多出的未提交的 log entry ,e~f 为两种问题并存的 。
Raft 算法中,leader 处理不一致是通过:强制 follower 直接复制自己的日志 。
- leader 对每一个 follower 都维护了一个
nextIndex
,表示下一个需要发送给 follower 的 log entry 的 index - leader 刚上任后,会将所有 follower 的 nextIndex 初始化为自己的最后一条 log entry 的 index 加一( 上图中的 11 )
- 如果一个
AppendEntries RPC
被 follower 拒绝后( leader 和 follower 不一致 ),leader 减小 nextIndex 值重试( prevLogIndex 和 prevLogTerm 也会对应改变 ) - 最终 nextIndex 会在某个位置使得 leader 和 follower 达成一致,此时,
AppendEntries RPC
成功,将 follower 中的冲突 log entry 删除并加上 leader 的 log entiry
冲突解决示例:
10 11 12 13 S1 3 S2 3 3 4 S3 3 3 5
- S3 被选为 leader ,term 为 6,S3 要添加新的 entry 13 给 S1 和 S2
- S3 发送
AppendEntries RPC
,包含 entry 13 、nextIndex[S2]=13、prevLogIndex=12 、preLogTerm=5- S2 和 S3 在 index=12 上不匹配,S2 返回 false
- S3 减小 nextIndex[S2] 到 12
- S3 重新发送
AppendEntries RPC
,包含 entry 12+13 、nextIndex[S2]=12、preLogIndex=11、preLogTerm=3- S2 和 S3 在 index=11 上匹配,S2 删除 entry 12 ,增加 leader 的 entry 12 和 entry 13
- S1 流程同理
一种优化方法:rpc请求被拒绝时返回冲突 log entry 的 term 和 属于该 term 的 log entry 的最早 index 。leader 重新发送请求时减小 nextIndex 越过那个 term 冲突的所有 log entry 。这样就变成了每个 term 需要一次 RPC 请求,而非每个 log entry 一次 。
Follower或Candidate崩溃
如果 follower 和 candidate 崩溃了,那么后续发送给它们的 RPC 就会失败 。
Raft 处理这种失败就是无限地重试;如果机器重启了,那么这些 RPC 就会成功 。
如果一个服务器完成一个 RPC 请求,在响应前崩溃,则它重启后会收到同样的请求 。这种重试不会产生什么问题,因为 Raft 的 RPC 都具有幂等性 。( 如:一个 follower 如果收到附加日志请求但是它已经包含了这一日志,那么它就会直接忽略这个新的请求 )
日志压缩
日志不断增长带来了两个问题:空间占用太大、服务器重启时重放日志会花费太多时间 。
使用 快照
来解决这个问题,在快照系统中,整个系统状态都以快照的形式写入到持久化存储中,然后到那个时间点之前的日志全部丢弃 。
每个服务器独立地创建快照,快照中只包括被提交的日志 。当日志大小达到一个固定大小时就创建一次快照 。使用 写时复制 技术来提高效率 。
上图为快照内容示例,可见快照中包含了:
- 状态机状态(以数据库举例,则快照记录的是数据库中的表)
- 最后被包含的 index :被快照取代的最后一个 log entry 的 index
- 最后被包含的 term:被快照取代的最后一个 log entry 的 term
保存 last included index 和 last included term 是为了支持快照后复制第一个 log entry 的 AppendEntries RPC
的一致性检查 。
为支持集群成员更新,快照也将最后一次配置作为最后一个条目存下来 。
通常由每个服务器独立地创建快照,但 leader 会偶尔发送快照给一些落后的 follower 。这通常发生在当 leader 已经丢弃了下一条需要发送给 follower 的 log entry 时 。
leader 使用 InstallSnapShot RPC
来发送快照给落后的 follower 。
7. 客户端交互
客户端交互过程:
-
client 启动时,会随机挑选一个服务器进行通信
-
若该服务器是 follower ,则拒绝请求并提供它最近接收到的 leader 的信息给 client
-
client 发送请求给 leader
-
若 leader 已崩溃,client 请求会超时,之后再随机挑选服务器进行通信
Raft 可能接收到一条命令多次:client 对于每一条命令都赋予一个唯一序列号,状态机追踪每条命令的序列号以及相应的响应,若序列号已被执行,则立即返回响应结果,不再重复执行。
只读的操作可以直接处理而无需记录日志 ,但是为防止脏读( leader 响应客户端时可能已被作废 ),需要有两个保证措施:
- leader 在任期开始时提交一个空的 log entry 以确定日志中的已有数据是已提交的( 根据领导人完全特性 ,会把之前未提交的日志一起进行提交)
- leader 在处理只读的请求前检查自己是否已被作废
- 可以在处理只读请求前发送心跳包检测自己是否已被作废
- 也可以使用 lease 机制,在 leader 收到
AppendEntries RPC
的多数回复后的一段时间内,它可以响应 client 的只读请求
集群成员变化
集群的 配置 可以被改变,如替换宕机的机器、加入新机器或改变复制级别 。
配置转换期间,整个集群会被分为两个独立的群体,在同一个时间点,两个不同的 leader 可能在同一个 term 被选举成功,一个通过旧的配置,一个通过新的配置 。
Raft 引入了一种过渡的配置 —— 共同一致 ,它允许服务器在不同时间转换配置、可以让集群在转换配置过程中依然响应客户端请求 。
共同一致是新配置和老配置的结合:
- log entry 被复制给集群中新、老配置的所有服务器
- 新、旧配置的服务器都可以成为 leader
- 达成一致( 针对选举和提交 )需要分别在两种配置上获得大多数的支持
配置转换过程
保证配置变化安全性的关键 是:防止 𝐶𝑜𝑙𝑑Cold 和 𝐶𝑛𝑒𝑤Cnew 同时做出决定( 即同时选举出各自的 leader )。
集群配置在复制日志中以特殊的 log entry 来存储 。下文中的 某配置做单方面决定 指的是在该配置内部选出 leader 。
- leader 接收到改变配置从 𝐶𝑜𝑙𝑑Cold 到 𝐶𝑛𝑒𝑤Cnew 的请求,初始只允许 𝐶𝑜𝑙𝑑Cold 做单方面决定
- leader 将 𝐶𝑜,𝑛Co,n log entry 写入日志当中,并向其他服务器中复制这个 log entry ( 服务器总是使用最新的配置,无论对应 entry 是否已提交 )
- 𝐶𝑜,𝑛Co,n log entry 提交以前,若 leader 崩溃重新选举,新 leader 可能是 𝐶𝑜𝑙𝑑Cold 也可能是 𝐶𝑜,𝑛Co,n(取决于新 leader 是否含有此 𝐶𝑜,𝑛Co,n log entry ) 。这一阶段, 𝐶𝑛𝑒𝑤Cnew 不被允许做单方面的决定。
- 𝐶𝑜,𝑛Co,n log entry 提交之后( 𝐶𝑜𝑙𝑑Cold 和 𝐶𝑛𝑒𝑤Cnew 中大多数都有该 entry ),若重新选举,只有 𝐶𝑜,𝑛Co,n 的服务器可被选为 leader 。——这一阶段,因为 𝐶𝑜𝑙𝑑Cold 和 𝐶𝑛𝑒𝑤Cnew 各自的大多数都处于 𝐶𝑜,𝑛Co,n,所以 𝐶𝑜𝑙𝑑Cold 和 𝐶𝑛𝑒𝑤Cnew 都无法做单方面的决定 。
- 此时,leader 可以开始在日志中写入 𝐶𝑛𝑒𝑤Cnew log entry 并将其复制到 𝐶𝑛𝑒𝑤Cnew 的服务器。
- 𝐶𝑛𝑒𝑤Cnew log entry 提交之前,若重新选举,新 leader 可能是 𝐶𝑛𝑒𝑤Cnew 也可能是 𝐶𝑜,𝑛Co,n 。——这一阶段,𝐶𝑛𝑒𝑤Cnew 可以做出单方面决定 。
- 𝐶𝑛𝑒𝑤Cnew log entry 提交之后,配置改变完成。——这一阶段,𝐶𝑛𝑒𝑤Cnew 可以做出单方面决定 。
可以看到,整个过程中,𝐶𝑜𝑙𝑑Cold 和 𝐶𝑛𝑒𝑤Cnew 都不会在同时做出决定(即在同时选出各自的 leader ),因此配置转换过程是安全的 。
示例:
若 𝐶𝑜𝑙𝑑Cold 为 S1 、S2 、S3 ,𝐶𝑛𝑒𝑤Cnew 为 S4 、S5 、S6
- 开始只允许 𝐶𝑜𝑙𝑑Cold 中有 leader
- S1 、S2 、S4 、S5 被写入 𝐶𝑜,𝑛Co,n log entry 后, 𝐶𝑜,𝑛Co,n log entry 变为已提交,此时 𝐶𝑜𝑙𝑑Cold 和 𝐶𝑛𝑒𝑤Cnew 中的大多数都已是 𝐶𝑜,𝑛Co,n ,它们无法各自选出 leader
- 此后 leader 将 𝐶𝑛𝑒𝑤Cnew log entry 开始复制到 S4 、S5 、S6
- 𝐶𝑛𝑒𝑤Cnew log entry 提交后,S1 、S2 、S3 退出集群,若 leader 是三者之一,则退位,在 S4 、S5 、S6 中重新选举
- 配置转换完成