Raft中的日志复制

分布式一致性问题

Posted by bbkgl on October 22, 2019

夜来幽梦忽还乡

小轩窗

正梳妆

日志简介

关于日志状态机

Raft 的目标是将日志完整地复制到集群内的所有服务器,这些复制的日志会被状态机所使用。假设我们希望程序或应用能可靠地执行,能够实现的一种方式是保证集群中所有服务器内的状态机都能按照相同的方式执行命令,这就是状态机复制同步的目的,这里的状态机通常指的是一个输入输出程序或应用。

如果系统的客户端将要执行的命令传递给集群中的一台服务器,假设命令是 X ,那么它会被该台服务器记录,然后命令会被发送到其他服务器,并被其他服务器上的日志所记录。一旦命令被安全的复制到日志中,那么它们就能被发送到状态机供执行。当其中的一台状态机完成了命令的执行,结果会被返回给客户端。可以注意到只要各个服务器上的日志是相同的,各个服务器上的状态机就能以相同的顺序执行相同的命令,这样它们执行的结果也都是一样的。所以共识性模块的任务就是管理这些日志,并保证它们正确的在集群内复制并且决定何时将命令传送给状态机才是安全的。

日志的结构

日志的结构如下图:

Hfc6e2d0e03fb4a1aab10cc0f3326a3e2s

可以看到每条日志记录由任期和命令组成。每台服务器节点,无论是群众还是领导人,都有一个日志副本。日志记录是由下标索引的位置来进行唯一标识的,在记录内部有两个主要信息(任期和状态机命令)。首先,每条记录都包括供状态机执行的一条命令,命令的格式可以是客户端与状态所达成一致的某种格式。其次,每条记录都包括一个任期号,这个任期号是该条记录创建时,领导者所处的任期,随着日志记录的增多,这个任期号也会单调上升。每台服务器都必须保证日志能在崩溃后还可以恢复,所以日志本身通常是存于磁盘或其他一些稳定的存储介质中。无论服务器作何更新,它都需要在收到来自于其他服务器的响应之前,将内容写入到磁盘。如果某条记录已存储于大多数服务器,例如上图中的记录 7 (Entry-7),那么我们就称该条记录已提交(committed)。这是 Raft 协议里非常重要的一个属性。如果一条记录是已提交的,那么它就能安全被传送给状态机进行执行,Raft 可以保证该条记录的耐久性。在上图中记录 7 是已提交的,所有先于记录 7 的记录也是已提交的状态,但是记录 8 还处于未提交状态,因为它只存储于两台服务器上。

日志的一致性

Raft设计了一种日志机制来维护一个不同服务器的日志之间的高层次的一致性。这么做不仅简化了系统的行为也使得更加可预计,同时他也是安全性保证的一个重要组件。Raft 维护着以下的特性:

  • 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令
  • 如果在不同的日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也全部相同

特性1

日志记录的索引以及任期号的组合可以唯一标识一条日志记录。也就是说如果有两条记录的索引是一样的,任期号也是一样的,那么就可以保证它们所存储的命令也是相同的。领导人最多在一个任期里在指定的一个日志索引位置创建一条日志条目,同时日志条目在日志中的位置也从来不会改变。

特性2

任期号和索引的组合可以唯一标识整个日志的起始至该点的位置。如果某条记录是已提交的,那么其所有前序的记录都应该处于已提交状态。这也与之前介绍的规则一致,如果发现服务器存储记录(如下图记录5),因为有了特性1和2,它们存储的前序记录也必须相同。所以这些前序记录也存在于集群的大多数服务器上。

img

如何检查前序记录是否相同呢?在发送附加日志 RPC 的时候,领导人会把新的日志条目紧接着之前的条目的索引位置和任期号包含在里面。如果群众在它的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝接收新的日志条目。一致性检查就像一个归纳步骤:一开始空的日志状态肯定是满足日志匹配特性的,然后一致性检查保护了日志匹配特性当日志扩展的时候。因此,每当附加日志 RPC 返回成功时,领导人就知道群众的日志一定是和自己相同的了。

处理不一致

在Raft算法中,领导人处理群众结点和自己不一致的方法简单粗暴:直接覆盖。要让群众的日志和领导人的日志保持一致,需要从后往前(从新到旧)找到不同的起点,然后开始覆盖,其实就是找到这个点(索引号)后,领导人会将这个点(索引号)及以后的日志条目都发送过来,进行删除和增加。领导人针对每一个群众维护了一个 nextIndex,这表示下一个需要发送给群众的日志条目的索引地址。

H39d1442802274e2195821a8e54ccef92f

上图就表示了nextIndex是如何起作用以及领导人是如何让群众的日志和自己保持一致性的。在上面的例子中,任期 7 的领导者的最后一条记录的索引位置是 10 ,那么它会将 nextIndex 设置成 11 。领导者会根据 AppendEntries 调用发现一致性问题,因为当群众接收到 AppendEntries 调用时,都会进行检查。这个检查就可以发现所有的问题。所以当下一次领导者想要与群众进行通信时,它都会包括下标位置索引(10)以及任期号(6)作为请求的参数。当选为领导者后,下一次请求也有可能是以心跳检测的方式发送的,心跳检测与 AppendEntries 调用的方式一样,只是没有新值创建,但还是包括一致性检查的。所以当消息到达群众(a)后,它会将接收到的下标位置索引与任期与自己的日志信息进行比较,并没有匹配的记录,所以它会拒绝 AppendEntries 请求,当领导者收到拒绝的响应之后,它的响应很简单,它要做的只是将 nextIndex 减 1 ,所以这个值就变成了 10 。如此逐一减少,直到最终 nextIndex 为 5 的时候,领导者再次发送请求的信息会包括下标位置索引(4)以及任期号(4),这时它与群众(a)当前的日志记录信息是相匹配的,所以这时群众会接受 AppendEntries 请求,并追加记录 5-4 。直到领导者将群众的日志记录填充完整。相似的过程也会在群众(b)上出现。当 nextIndex 减少到 4 时,领导者会包括下标位置索引(3)以及任期号(1)作为请求的参数,并修正群众(b)上的日志记录。

以上内容部分参考Raft论文中文译文Raft 实现日志复制同步