PolarFS的ParallelRaft

PolarFS的ParallelRaft

简介

本文摘自阿里在PVLDB的一篇论文《PolarFS: An Ultra-low Latency and Failure Resilient Distributed File System for Shared Storage Cloud Database》,其中介绍了一种比Raft更高效的一致性协议ParallelRaft。
Raft是一个比Paxos更易于理解与实现的一致性协议(consensus protocol),详细介绍参考Raft协议详解分布式系统的Raft算法

理解ParallelRaft首先要理解Raft协议。

ParallelRaft是为PolarFS服务的,PolarFS是一个文件系统。文件系统中有数据块的含义,文中有些地方会涉及到。

文中并没有提出一个更为应用广泛的协议,特别针对了PolarFS做描述,不过可以根据自己的业务场景,按照文中的理论做处理。

Raft的缺陷

为了简单和容易理解,Raft设计成了高度串行化的协议。Leader和Follower的日志都不允许出现空洞,这就是说follower应答的日志、leader提交和所有副本应用(apply)的日志,都是按照顺序的。这样当写请求并行执行的时候,也要按照顺序提交。队列前面的请求还没有处理,在队列尾部的请求就不能提交和应答,这样就增大了平均延迟和吞吐。
Leader和Follower之间有多个连接时,这种串行的方式就不太合适。如果某个连接卡住了或者变慢了,备机就会收到乱序的日志,而且不能做应答。
ParallelRaft解决了这个问题,下面通过日志复制、leader选举和catch up(follower追赶leader)方面描述。

leader和follower之间是单链路连接就可以保证一定是串行吗?明显不是的。不过单链接根本不用考虑并行优化。

乱序日志复制(Out-of-Order Log Replication)

Raft从两个方面串行化:

  1. leader向follower发送一个日志,follower在收到并且记录下来之后,向leader发送应答,这还表示这条日志之前的日志也都收到并且保存下来了;
  2. leader提交一个日志,向所有的follower广播消息,就意味着所有之前的日志都已经提交。

ParallelRaft打破了这个约束,并且乱序执行。为了保证协议的正确性,要做到:

  1. 没有串行化约束,所有提交的事务与典型的关系型数据库语义保持一致(应该是指ACID方面的语义);
  2. 所有提交的事务不会丢失。

乱序执行,怎么保证新版本的数据不会被旧版本数据覆盖?
换句话说,apply某个数据时,怎么保证不会覆盖与之有冲突的还未收到的数据。
如果某个范围的写入与其它的写入没有重叠,那么这些日志就没有冲突,可以以任意顺序执行。否则,冲突的写入操作必须按照顺序来执行。这样就保证了新修改的数据不会被旧的修改操作覆盖。

ParallelRaft现在为文件系统PolarFS服务,所以数据是否有冲突,只要对比写入的数据块范围是否有重叠即可。如果是数据库,就需要检查是否修改了同一行,或者更精细一点,是不是修改了同一个字段。

乱序应答Out-of-Order Acknowledge
一旦日志持久化成功,follower就给leader应答。这样减少了平均延迟时间,因为不用等前面的日志。

不完全准确,这里的“乱序”并不是绝对的,最终还是要确定一个顺序。在“乱序”达到一定程度时,还是要等待前面的日志。

乱序提交 Out-of-Order Commit
某个日志收到大多数follower应答之后,leader就提交。

抛出问题:收到较大编号的日志提交后,怎么保证之前的日志一定会提交成功?因为raft不允许出现空洞。使用“空”来弥补吗?

Apply带空洞的日志 Apply with Holes in the Log
因为日志不一定是按照顺序写入的,那么肯定会有些场景出现空洞。怎么保证日志有空洞时,apply的正确性?

其实这个就介绍了怎么保证旧版本数据不会覆盖新版本数据。

ParallelRaft引入了一个新的数据结构:look behind buffer。每个日志中都有一个这样的数据结构,look behind buffer包含前面N条日志修改的逻辑块地址(Logical Block Address),这样的话,look behind buffer就像一个日志空洞上的桥。N是这个桥的长度,就是允许出现的最大空洞。
注意虽然日志中可能存在多个空洞,所有日志条目(log entries)的逻辑块地址汇总信息总是完整的,除非有空洞比N大。
follower可以通过这个结构确定某条日志(log entry)是否有冲突,是指与在这条日志前面的,但是还未收到的日志修改的逻辑块地址是否有重叠。不与其它日志冲突的可以安全的apply,否则就加到一个pending列表中,稍后再apply。

文中说N选择为2,就是空洞最大是2条日志。

选主 Leader Election

跟Raft一样,ParallelRaft也是选择拥有最新term和最多事务日志的节点为主。
不过ParallelRaft选出来的主,可能会存在带空洞的日志。这样就需要把空洞补上。
选举为主之后,主的状态不是leader,而是leader candidate。成为真正的主之前增加一个过程,merge stage(合并阶段)。在合并阶段先从其它节点上找到缺失的日志,然后合并到本机。完成之后,状态变为leader

日志合并
与Raft语义一样,具有相同term和index的日志保证是相同的。但是有一些特殊条件:

  1. 对于一个提交过的,但是不存在的日志(指在本机上不存在),leader候选人(leader candidate)可以最少从一个follower中找到这条日志。因为日志提交的条件是已经复制到了大多数节点。可以通过询问follower节点得知是否已经提交。
  2. 对于所有节点都没有提交的日志,可以直接跳过这条日志。按照Raft的协议机制,这种日志不能提交,因为没有被大多数节点接受。(我认为不是的,如果之前的主将数据发送到了备机,备机也都给了应答,但是还没来得及将commit命令发给备机,那么这条日志在follower上也是未提交的。也可能是没有理解透文中的意思。修正:这里对raft committed概念理解错误,raft原文中是这么描述committed的:The leader decides when it is safe to apply a log entry to the state machines; such an entry is called committed. Raft guarantees that committed entries are durable and will eventually be executed by all of the available state machines. A log entry is committed once the leader that created the entry has replicated it on a majority of the servers。就是说Leader将日志复制到了大多数节点,并且这些日志在大多数节点已经持久化durable,就是committed的,并且最终会被执行。)
  3. 如果某些节点保存的日志,index相同,但是term不同,那么leader候选人就选择term最大的日志:
    3.a ParallelRaft的合并阶段必须在新leader提供服务之前完成。这个决定了 某个日志设置了更大的term,index相同但是term比较小的日志不能提交,比较小的term的日志也不能出现在之前完全成功的合并阶段,否则更大term的日志不能有相同的index。
    3.b 系统宕机时,如果某个节点中包含了一个未提交的日志,这个日志的应答也可能已经发给了之前的leader,leader也给用户回复了。这样的话就不能简单的丢掉这条日志,否则就会造成数据丢失。更精确的描述,如果宕机的节点总数加上拥有这条未提交事务(拥有相同index最大term的日志)节点个数超过了其它节点的个数,那么这条日志就可能提交了。这样的话,就应该提交这条日志。

merge要确定本机不存在的空洞日志状态是如何的。要保证给客户端回复成功的日志不能丢,回复失败的不能提交,回复不确定的或者没有回复的日志,提交或不提交都是合理的。这几条推理,就是为了确认某条日志,是不是给客户端回复过。提交过的日志肯定是给客户端回复过的,follower上未提交的日志,可能是已经提交的,也可能没有提交。这里给出的是尽量按照已经提交的方法来判断(拥有这个日志的节点个数 + 宕机节点个数 > 不包含这条日志的节点个数)。

这个图片描述了3个副本的示例。
在这里插入图片描述

  1. follower发送本地日志给leader candidateleader candidate收到日志与本地日志合并;
  2. leader candidate同步状态信息到follower;
  3. leader candidate提交日志并且通知follower提交;
  4. leader candidate升级成leader

通过上面的机制,新leader会拥有所有已经提交的日志,就是说ParallelRaft不会丢数据。

checkpoint
ParallelRaft会不时地做checkpoint,checkpoint会记录一个当前系统的快照(snapshot),所有checkpoint之前的日志都已经apply到了数据块上,checkpoint允许包含checkpoint之后的日志(文中没有解释为什么,也没有介绍怎么处理的)。在实际实现中,ParallelRaft会选择有用最新checkpoint的节点作为主,而不是拥有最新日志的节点,原因就是catch up的实现(为了让follower更容易的追上leader的状态)。在merge阶段拥有最新checkpoint的节点更容易处理,因为不需要处理checkpoint之前的日志(就是checkpoint越新,相对来说需要做merge的日志就越少)。

Catch Up

一个滞后的follower追上leader,就称为Catch Up
有两种机制:fast-catch-upstreaming-catch-up

fast-catch-up是follower与leader差距比较小时,使用增量的方式进行同步(同步两个节点的状态可能需要同步checkpoint数据和日志,同步日志就是增量的方式,只需要同步部分数据)。
streaming-catch-up相对应的,follower与leader差距比较大的时候使用这个方法,比如follower停机了很多天。
两者在程序上的区别主要是leader判断能否使用增量同步的方式将日志复制到follower中。如果leader中的checkpoint比follower中的最新日志还要新,那么就只能使用streaming-catch-up了,就是全量同步checkpoint + log entries
如果使用fast-catch-up方法的话,follower的checkpoint后面的日志也可能包含空洞,这些空洞也使用look behind buffer来查找,直接从leader的数据中复制过来。
下面这个图片描述了follower追上(catch up)leader的3个场景,根据Leader的信息和Follower的信息来判断需要全量复制(checkpoint + log entries)还是增量复制(log entries)。
ParallelRaft Fast Catch Up流程

ParallelRaft的正确性

ParallelRaft与Raft最大的区别就是乱序提交。所以关键点就在新选举出来的leader怎么处理这些日志中的空洞。ParallelRaft引入merge阶段来保证不会有事务丢失,进而保证了leader的正确性(Safefy,不知道怎么翻译更准确)。
ParallelRaft通过look behind buffer来保证有冲突的事务不会乱序提交,从而保证状态机的正确性(State Machine Safefy)。
另外,Election Safefy、Leader Append-Only和Log Matching与Raft一样,没有变化。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值