6.5 日志(Raft Log)

你们应该关心的一个问题是:为什么Raft系统这么关注Log,Log究竟起了什么作用?这个问题值得好好来回答一下。

Raft系统之所以对Log关注这么多的一个原因是,Log是Leader用来对操作排序的一种手段。这对于复制状态机(详见4.2)而言至关重要,对于这些复制状态机来说,所有副本不仅要执行相同的操作,还需要用相同的顺序执行这些操作。而Log与其他很多事物,共同构成了Leader对接收到的客户端操作分配顺序的机制。比如说,我有10个客户端同时向Leader发出请求,Leader必须对这些请求确定一个顺序,并确保所有其他的副本都遵从这个顺序。实际上,Log是一些按照数字编号的槽位(类似一个数组),槽位的数字表示了Leader选择的顺序。

Log的另一个用途是,在一个(非Leader,也就是Follower)副本收到了操作,但是还没有执行操作时。该副本需要将这个操作存放在某处,直到收到了Leader发送的新的commit号才执行。所以,对于Raft的Follower来说,Log是用来存放临时操作的地方。Follower收到了这些临时的操作,但是还不确定这些操作是否被commit了。我们将会看到,这些操作可能会被丢弃。

Log的另一个用途是用在Leader节点,我(Robert教授)很喜欢这个特性。Leader需要在它的Log中记录操作,因为这些操作可能需要重传给Follower。如果一些Follower由于网络原因或者其他原因短时间离线了或者丢了一些消息,Leader需要能够向Follower重传丢失的Log消息。所以,Leader也需要一个地方来存放客户端请求的拷贝。即使对那些已经commit的请求,为了能够向丢失了相应操作的副本重传,也需要存储在Leader的Log中。

所有节点都需要保存Log还有一个原因,就是它可以帮助重启的服务器恢复状态。你可能的确需要一个故障了的服务器在修复后,能重新加入到Raft集群,要不然你就永远少了一个服务器。比如对于一个3节点的集群来说,如果一个节点故障重启之后不能自动加入,那么当前系统只剩2个节点,那将不能再承受任何故障,所以我们需要能够重新并入故障重启了的服务器。对于一个重启的服务器来说,会使用存储在磁盘中的Log。每个Raft节点都需要将Log写入到它的磁盘中,这样它故障重启之后,Log还能保留。而这个Log会被Raft节点用来从头执行其中的操作进而重建故障前的状态,并继续以这个状态运行。所以,Log也会被用来持久化存储操作,服务器可以依赖这些操作来恢复状态。

学生提问:假设Leader每秒可以执行1000条操作,Follower只能每秒执行100条操作,并且这个状态一直持续下去,会怎样?

Robert(教授):这里有一点需要注意,Follower在实际执行操作前会确认操作。所以,它们会确认,并将操作堆积在Log中。而Log又是无限的,所以Follower或许可以每秒确认1000个操作。如果Follower一直这么做,它会生成无限大的Log,因为Follower的执行最终将无限落后于Log的堆积。 所以,当Follower堆积了10亿(不是具体的数字,指很多很多)Log未执行,最终这里会耗尽内存。之后Follower调用内存分配器为Log申请新的内存时,内存申请会失败。Raft并没有流控机制来处理这种情况。

所以我认为,在一个实际的系统中,你需要一个额外的消息,这个额外的消息可以夹带在其他消息中,也不必是实时的,但是你或许需要一些通信来(让Follower)告诉Leader,Follower目前执行到了哪一步。这样Leader就能知道自己在操作执行上领先太多。所以是的,我认为在一个生产环境中,如果你想使用系统的极限性能,你还是需要一条额外的消息来调节Leader的速度。

学生提问:如果其中一个服务器故障了,它的磁盘中会存有Log,因为这是Raft论文中图2要求的,所以服务器可以从磁盘中的Log恢复状态,但是这个服务器不知道它当前在Log中的执行位置。同时,当它第一次启动时,它也不知道那些Log被commit了。

Robert教授:所以,对于第一个问题的答案是,一个服务器故障重启之后,它会立即读取Log,但是接下来它不会根据Log做任何操作,因为它不知道当前的Raft系统对Log提交到了哪一步,或许有1000条未提交的Log。

学生补充问题:如果Leader出现了故障会怎样?

Robert教授:如果Leader也关机也没有区别。让我们来假设Leader和Follower同时故障了,那么根据Raft论文图2,它们只有non-volatile状态(也就是磁盘中存储的状态)。这里的状态包括了Log和最近一次任期号(Term Number)。如果大家都出现了故障然后大家都重启了,它们中没有一个在刚启动的时候就知道它们在故障前执行到了哪一步。所以这个时候,会先进行Leader选举,其中一个被选为Leader。如果你回顾一下Raft论文中的图2有关AppendEntries的描述,这个Leader会在发送第一次心跳时弄清楚,整个系统中目前执行到了哪一步。Leader会确认一个过半服务器认可的最近的Log执行点,这就是整个系统的执行位置。另一种方式来看这个问题,一旦你通过AppendEntries选择了一个Leader,这个Leader会迫使其他所有副本的Log与自己保持一致。这时,再配合Raft论文中介绍的一些其他内容,由于Leader知道它迫使其他所有的副本都拥有与自己一样的Log,那么它知道,这些Log必然已经commit,因为它们被过半的副本持有。这时,按照Raft论文的图2中对AppendEntries的描述,Leader会增加commit号。之后,所有节点可以从头开始执行整个Log,并从头构造自己的状态。但是这里的计算量或许会非常大。所以这是Raft论文的图2所描述的过程,很明显,这种从头开始执行的机制不是很好,但是这是Raft协议的工作流程。下一课我们会看一种更有效的,利用checkpoint的方式。

所以,这就是普通的,无故障操作的时序。

最后更新于