说明:本文为论文 《 In Search of an Understandable Consensus Algorithm (Extended Version) 》 的个人理解,难免有理解不到位之处,欢迎交流与指正 。
论文地址:Raft Paper
复制状态机 (Replicated state machine)
方法在分布式系统中被用于解决 容错问题 ,这种方法中,一个集群中各服务器有相同状态的副本,并且在一些服务器宕机的情况下也可以正常运行 。
如上图所示,每台服务器都存储一个包含一系列命令的 日志 ,并且按照日志的顺序执行。每台服务器都顺序执行相同日志上的命令,因此它们可以保证相同的状态 。
一致性算法 的工作就是保证复制日志的相同 。一台服务器上,一致性模块接收 client 的请求命令并将其写入到自己的日志中,它和其他服务器上一致性模块通信来保证集群中服务器的日志都相同 。命令被正确地复制后,每一个服务器的状态机按照日志顺序执行它们,最后将输出结果返回给 client 。
因此,服务器集群看起来就是一个高可用的状态机,只要集群中大多数机器可以正常运行,就可以保证可用性 。
关于复制状态机的更详细内容,可以阅读 VM-FT ,不过 Raft 应用到的复制状态机一般是应用级复制,不必达到像 VM-FT 那样的机器级复制 。
可将复制状态机应用于 MapReduce 的 master 、GFS 的 master 以及 VM-FT 的存储服务器 。
Raft 是一种为了管理复制日志的一致性算法 。为了提高 可理解性 :
Raft 算法在许多方面都和现有的一致性算法很相似,但也有独特的特性:
一个 Raft 集群必须包含奇数个服务器,若包含 2f+1 台服务器,则可以容忍 f 台服务器的故障 。( 为了保留多数服务器在线,以正常完成日志提交和 leader 选举 )
Raft 通过选举一个 leader ,然后给予它全部的管理日志的权限,以此来实现一致性 。
一个服务器处于三种状态之一:leader
、follower
和 candidate
:
一次选举开始对应这一个新的 term (任期)
,term 使用连续的整数标记 ,一个 term 最多有一个 leader 。
Raft 会使用一种 心跳机制
来触发领导人选举,当 leader 在位时,它周期性地发送心跳包(不含 log entry 的 AppendEntries RPC
请求) 给 follower ,若 follower 在一段时间内未接收到心跳包( 选举超时
),则认为系统中没有 leader ,此时该 follower 会发起选举 。
当系统启动时,所有服务器都是 follower ,第一个选举超时的发起选举 。
RequestVote RPC
请求split vote
问题 ),则所有 candidate 都超时,并增加 term 号,开始新一轮的选举RequestVote RPC
请求中包含了 candidate 的日志信息,投票方服务器会拒绝日志没有自己新的投票请求
随机选举超时时间
。这样可以把服务器超时时间点分散开,一个 candidate 超时后,它可以赢得选举并在其他服务器超时之前发送心跳包 。即使出现了一个 split vote 情况,多个 candidate 会重置随机选举超时时间,可以避免下一次选举也出现 split vote 问题 。( 保证之前 term 中已提交的 entry 在选举时都出现在新的 leader 的日志中 ):
Raft 要求 安全性不能依赖时间:整个系统不能因为某些事件运行的比预期快一点或慢一点就产生错误的结果
为选举并维持一个稳定的领导人,系统需满足:
广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障时间(MTBF)
每个 log entry 存储一个 状态机命令 和从 leader 收到这条命令的 term ,每一条 log entry 都有一个整数的 log index 来表明它在日志中的位置 。
committed log entry
指可以安全应用到状态机的命令。当由 leader 创建的 log entry 复制到大多数的服务器上时,log entry 就会被提交 。同时,leader 日志中之前的 log entry 也被提交 (包含其他 leader 创建的 log entry )。
日志的作用:
leader 接收到来自 client 的请求
leader 将请求中的命令作为一个 log entry 写入本服务器的日志
leader 并行地发起 AppendEntries RPC
请求给其他服务器,让它们复制这条 log entry
当这条 log entry 被复制到集群中的大多数服务器( 即成功提交 ),leader 将这条 log entry 应用到状态机( 即执行对应命令 )
leader 执行命令后响应 client
leader 会记录最后提交的 log entry 的 index ,并在后续 AppendEntries RPC
请求( 包含心跳包 )中包含该 index ,follower 将此 index 指向的 log entry 应用到状态机
若 follower 崩溃或运行缓慢或有网络丢包,leader 会不断重复尝试 AppendEntries RPC
,直到所有 follower 都最终存储了所有 log entry
若 leader 在某条 log entry 提交前崩溃,则 leader 不会对 client 响应,所以 client 会重新发送包含此命令的请求
Raft 使用 日志机制
来维护不同服务器之间的一致性,它维护以下的特性:
对于 特性一: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 的日志与自己的相同了 。
上图体现了一些 leader 和 follower 不一致的情况 。a~b 为 follower 有缺失的 log entry ,c~d 为 follower 有多出的未提交的 log entry ,e~f 为两种问题并存的 。
Raft 算法中,leader 处理不一致是通过:强制 follower 直接复制自己的日志 。
nextIndex
,表示下一个需要发送给 follower 的 log entry 的 indexAppendEntries RPC
被 follower 拒绝后( leader 和 follower 不一致 ),leader 减小 nextIndex 值重试( prevLogIndex 和 prevLogTerm 也会对应改变 )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 流程同理
一种 优化方法:
当 AppendEntries RPC
被拒绝时返回冲突 log entry 的 term 和 属于该 term 的 log entry 的最早 index 。leader 重新发送请求时减小 nextIndex 越过那个 term 冲突的所有 log entry 。这样就变成了每个 term 需要一次 RPC 请求,而非每个 log entry 一次 。
减小 nextIndex 跨过 term 的不同情况:
follower 返回失败时包含:
- XTerm:冲突的 log entry 所在的 term
- XIndex:冲突 term 的第一个 log entry 的 index
- XLen:follower 日志的长度
follower 返回失败时的情况:
其中 S2 是 leader ,它向 S1 发送新的 entry ,
AppendEntries RPC
中 prevLogTerm=6对于 Case 1:nextIndex = XIndex
对于 Case 2:nextIndex = leader 在 XTerm 的最后一个 index
对于 Case 3:nextIndex = XLen
( 论文中对此处未作详述,只要满足要求的设计均可 )
如果 follower 和 candidate 崩溃了,那么后续发送给它们的 RPC 就会失败 。
Raft 处理这种失败就是无限地重试;如果机器重启了,那么这些 RPC 就会成功 。
如果一个服务器完成一个 RPC 请求,在响应前崩溃,则它重启后会收到同样的请求 。这种重试不会产生什么问题,因为 Raft 的 RPC 都具有幂等性 。( 如:一个 follower 如果收到附加日志请求但是它已经包含了这一日志,那么它就会直接忽略这个新的请求 )
日志不断增长带来了两个问题:空间占用太大、服务器重启时重放日志会花费太多时间 。
使用 快照
来解决这个问题,在快照系统中,整个系统状态都以快照的形式写入到持久化存储中,然后到那个时间点之前的日志全部丢弃 。
每个服务器独立地创建快照,快照中只包括被提交的日志 。当日志大小达到一个固定大小时就创建一次快照 。使用 写时复制 技术来提高效率 。
上图为快照内容示例,可见快照中包含了:
保存 last included index 和 last included term 是为了支持快照后复制第一个 log entry 的 AppendEntries RPC
的一致性检查 。
为支持集群成员更新,快照也将最后一次配置作为最后一个条目存下来 。
通常由每个服务器独立地创建快照,但 leader 会偶尔发送快照给一些落后的 follower 。这通常发生在当 leader 已经丢弃了下一条需要发送给 follower 的 log entry 时 。
leader 使用 InstallSnapShot RPC
来发送快照给落后的 follower 。
客户端交互过程:
client 启动时,会随机挑选一个服务器进行通信
若该服务器是 follower ,则拒绝请求并提供它最近接收到的 leader 的信息给 client
client 发送请求给 leader
若 leader 已崩溃,client 请求会超时,之后再随机挑选服务器进行通信
Raft 可能接收到一条命令多次:client 对于每一条命令都赋予一个唯一序列号,状态机追踪每条命令的序列号以及相应的响应,若序列号已被执行,则立即返回响应结果,不再重复执行。
只读的操作可以直接处理而无需记录日志 ,但是为防止脏读( leader 响应客户端时可能已被作废 ),需要有两个保证措施:
AppendEntries RPC
的多数回复后的一段时间内,它可以响应 client 的只读请求集群的 配置 可以被改变,如替换宕机的机器、加入新机器或改变复制级别 。
配置转换期间,整个集群会被分为两个独立的群体,在同一个时间点,两个不同的 leader 可能在同一个 term 被选举成功,一个通过旧的配置,一个通过新的配置 。
Raft 引入了一种过渡的配置 —— 共同一致 ,它允许服务器在不同时间转换配置、可以让集群在转换配置过程中依然响应客户端请求 。
共同一致是新配置和老配置的结合:
保证配置变化安全性的关键 是:防止 \(C_{old}\) 和 \(C_{new}\) 同时做出决定( 即同时选举出各自的 leader )。
集群配置在复制日志中以特殊的 log entry 来存储 。下文中的 某配置做单方面决定 指的是在该配置内部选出 leader 。
可以看到,整个过程中,\(C_{old}\) 和 \(C_{new}\) 都不会在同时做出决定(即在同时选出各自的 leader ),因此配置转换过程是安全的 。
示例:
若 \(C_{old}\) 为 S1 、S2 、S3 ,\(C_{new}\) 为 S4 、S5 、S6
- 开始只允许 \(C_{old}\) 中有 leader
- S1 、S2 、S4 、S5 被写入 \(C_{o,n}\) log entry 后, \(C_{o,n}\) log entry 变为已提交,此时 \(C_{old}\) 和 \(C_{new}\) 中的大多数都已是 \(C_{o,n}\) ,它们无法各自选出 leader
- 此后 leader 将 \(C_{new}\) log entry 开始复制到 S4 、S5 、S6
- \(C_{new}\) log entry 提交后,S1 、S2 、S3 退出集群,若 leader 是三者之一,则退位,在 S4 、S5 、S6 中重新选举
- 配置转换完成
事实上,实际 Raft 的配置转换实现一般都采用
Single Cluser MemberShip Change
机制,这种机制在 Diego 的 博士论文 中有介绍 。关于论文中叙述的使用共同一致的成员变更方法,Diego 建议仅在学术中讨论它,实际实现使用Single Cluster MemberShip Change
更合适。至于该机制的内容,之后抽空看了再补充吧 /(ㄒoㄒ)/~~
Raft论文《 In Search of an Understandable Consensus Algorithm (Extended Version) 》研读
原文:https://www.cnblogs.com/brianleelxt/p/13251540.html