MIT6.824-Raft笔记3:Raft日志、应用层和raft之间的日志“传递“

1. 日志(Raft Log)

你们应该关心的一个问题是:为什么Raft系统这么关注Log,Log究竟起了什么作用?

  1. Log是Leader用来对操作排序的一种手段。这对于复制状态机(复制状态机基于:对于复制的服务 service 或者其它computer things,其内部操作都是确定的,除非有外部输入影响,详见4.2)而言至关重要,对于这些复制状态机来说,所有副本不仅要执行相同的操作,还需要用相同的顺序执行这些操作。Log与其他很多事物,共同构成了Leader对接收到的客户端操作分配顺序的机制。比如有10个客户端同时向Leader发出请求,Leader必须对这些请求确定一个顺序,并确保所有其他的副本都遵从这个顺序。实际上,Log是一些按照数字编号的槽位(类似一个数组),槽位的数字表示了Leader选择的顺序。
  2. 对于Raft的Follower来说,Log是用来存放临时操作的地方。在一个副本收到了操作,但是还没有执行操作时,该副本需要将这个操作存放在某处,直到收到了Leader发送的新的commit号才执行。Follower收到了这些临时的操作,但是还不确定这些操作是否被commit了,这些操作可能会被丢弃。
  3. Leader需要在它的Log中记录操作,因为这些操作可能需要重传给Follower。如果一些Follower由于网络原因或者其他原因短时间离线了或者丢了一些消息,Leader需要能够向Follower重传丢失的Log消息。Leader也需要一个地方来存放客户端请求的拷贝。即使对那些已经commit的请求,为了能够向丢失了相应操作的副本重传,也需要存储在Leader的Log中。
  4. 帮助重启的服务器恢复状态。你可能的确需要一个故障了的服务器在修复后,能重新加入到Raft集群,要不然你就永远少了一个服务器。比如对于一个3节点的集群来说,如果一个节点故障重启之后不能自动加入,那么当前系统只剩2个节点,那将不能再承受任何故障,我们需要能够重新并入故障重启了的服务器。对于一个重启的服务器来说,会使用存储在磁盘中的Log。每个Raft节点都需要将Log写入到它的磁盘中,这样它故障重启之后,Log还能保留。而这个Log会被Raft节点用来从头执行其中的操作进而重建故障前的状态,并继续以这个状态运行。Log也会被用来持久化存储操作,服务器可以依赖这些操作来恢复状态。

2.中,比如5节点,只在leader和另一个follower上面成功记录,那么这个follower记录的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的速度。

个人理解:

  1. Leader会对齐Follower当前的Index,只需要从这里开始发送日志
  2. 每次并不是从上文的Index发到尾,二是有一个可配置的最大值控制
  3. Leader的Log会持久化到存储设备,未同步完成的日志无需存在内存中
  4. 快照机制,快速恢复

ETCD里面的leader会记录每一个副本的in flight消息的数目,同时也有设置的最大值控制这个值。

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

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

个人理解:

  1. 重选leader,因为一些限制条件(过半复制、过半投票),这个leader一定会有最全的日志
  2. leader当选之后,会先确定commitID

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

Robert教授:我们来假设Leader和Follower同时故障了,那么根据Raft论文图2,它们只有non-volatile状态。这里的状态包括了Log和最近一次任期号(Term ID)。如果大家都出现了故障然后大家都重启了,它们中没有一个在刚启动的时候就知道它们在故障前执行到了哪一步。这个时候,会先进行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协议的工作流程。

2. 应用层和raft库之间的接口

这一部分简单介绍一下应用层和Raft层之间的接口。假设我们的应用程序是一个key-value数据库,下面一层是Raft层。在Raft集群中,每一个副本上,这两层之间主要有两个接口。

  • key-value层用来转发客户端请求的接口。如果客户端发送一个请求给key-value层,key-value层会将这个请求转发给Raft层,并说:请将这个请求存放在Log中的某处。这个接口实际上是个函数调用,只接收一个参数,就是客户端请求。key-value层说:我接到了这个请求,请把它存在Log中,并在committed之后告诉我。
  • Raft层通知key-value层请求已经commit了。Raft层通知的,不一定是最近一次Start函数传入的请求。例如在任何请求commit之前,可能会再有超过100个请求通过Start函数传给Raft层。这个向上的接口以go channel中的一条消息的形式存在。Raft层会发出这个消息,key-value层要读取这个消息。这里有个叫做applyCh的channel,通过它你可以发送ApplyMsg消息。key-value层需要知道从applyCh中读取的消息,对应之前调用的哪个Start函数,Start函数的返回需要有足够的信息给key-value层,这样才能完成对应。Start函数的返回值包括,这个请求将会存放在Log中的位置(index)。这个请求不一定能commit成功,但是如果commit成功的话,会存放在这个Log位置。同时,它还会返回当前的任期号(Term ID)和一些其它我们现在还不太关心的内容。在ApplyMsg中,将会包含请求(command)和对应的Log位置(index)。所有的副本都会收到这个ApplyMsg消息,它们都知道自己应该执行这个请求,弄清楚这个请求的具体含义,并将它应用在本地的状态中。所有的副本节点还会拿到Log的位置信息(index),但是这个位置信息只在Leader有用,因为Leader需要知道ApplyMsg中的请求究竟对应哪个客户端请求(进而响应客户端请求)。

个人理解:

  • 这里这种描述有点指定了具体的代码实现,感觉应用层同步等待结果,raft层异步处理这种方式应用层比较简单。
  • 第二个接口简单来说就是leader接收到应用层的请求之后,会执行raft log提交,发送给其它follower,当达到quorum的时候,就应用并且返回给上层结果。
  • 这里面描述的方案,应用层还得去知道返回的对应哪一个操作,感觉代码实现起来比较复杂,不如把底层实现封装起来,可以用feature的模式去等待。[go用channel实现feature对性能的影响?]
  • 对raft层来说是异步处理的,但是从应用层的视角里,应用层是在同步等待raft层的结果。

学生提问:为什么不在Start函数返回的时候就响应客户端请求呢?

Robert教授:我们假设客户端发送了任意的请求,我们假设这里是一个Put或者Get请求,是什么其实不重要,我们还是假设这里是个Get请求。客户端发送了一个Get请求,并且等待响应。当Leader知道这个请求被(Raft)commit之后,会返回响应给客户端。这里会是一个Get响应。(在Leader返回响应之前)客户端看不到任何内容。在实际的软件中,客户端调用key-value的RPC,key-value层收到RPC之后,会调用Start函数,Start函数会立即返回,但是这时,key-value层不会返回消息给客户端,因为它还没有执行客户端请求,它也不知道这个请求是否会被(Raft)commit。一个不能commit的场景是,当key-value层调用了Start函数,Start函数返回之后,它就故障了,它必然没有发送Apply Entry消息或者其他任何消息,也不能执行commit。实际上,Start函数返回了,随着时间的推移,对应于这个客户端请求的ApplyMsg从applyCh channel中出现在了key-value层。只有在那个时候,key-value层才会执行这个请求,并返回响应给客户端。

个人理解:

  1. Get请求可以同步一次日志,也可以不同步到日志中(这个困惑了好久,后面会具体说明)
    1. 如果允许短时间读到旧的数据,可以只从leader上读取数据(旧leader被网络隔离时,从旧leader上可能读到旧数据)
    2. 如果保障必须是最新的数据,可以对于每次读取都走一遍过半复制,也可以通过Read Index的方案实现,保障线性一致性。
  2. 对于leader来说,需要确保日志同步完成,才能响应。

感觉这里还是教授对raft实际代码实现的理解,在应用层异步感觉不如在raft层异步,应用层用起来舒服。

对于Log来说有一件有意思的事情:不同副本的Log或许不完全一样。有很多场合都会不一样,至少不同副本节点的Log的末尾,会短暂的不同。例如一个Leader开始发出一轮AppendEntries消息,但是在完全发完之前就故障了。这意味着某些副本收到了这个AppendEntries,并将这条新Log存在本地。而那些没有收到AppendEntries消息的副本,自然也不会将这条新Log存入本地。这里很容易可以看出,不同副本中,Log有时会不一样。Raft会最终强制不同副本的Log保持一致。或许会有短暂的不一致,但是长期来看,所有副本的Log会被Leader修改,直到Leader确认它们都是一致的。

参考文献:
[https://pdos.csail.mit.edu/6.824/schedule.html](https://pdos.csail.mit.edu/6.824/schedule.html)
[https://mit-public-courses-cn-translatio.gitbook.io/mit6-824/](https://mit-public-courses-cn-translatio.gitbook.io/mit6-824/)

  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值