自定义一致性协议

  首先定义几个摘要以便容易被搜索到:

  分布式一致性协议  

  raft性能不好

  取代raft 替代raft

       ==========================================

  上一篇博文讲了分布式存储的架构:https://mp.csdn.net/console/editor/html/105946387

  其中没有详述一致性协议,特意留到这篇博文来讲。

  

  讨论分布式存储,必定离不开一致性协议。

       先说说Raft。Raft的基本概念就不解释了,网上有大量的相关帖子,官方paper(以及中文版)也可以找到。本人最初也是研读了官方paper。

       我现在想说的是,raft并不适合高并发场景,原因有两个:

  1) raft心跳太频繁,对CPU和网络的消耗很大。

  2) raft导致写放大一倍,对磁盘IO的消耗不小。

  原因1)的分析:

  Raft的leader和followers之间依靠频繁的心跳(间隔不能太大,基本要求每秒一次)保持联系以及传递数据。这样一来在分布式存储场景中,假如一个存储节点上有一万个raft实例(每个存储节点上,每个需要保证一致性的对象都必须有一个raft实例。一个10TB的存储节点,每个文件1GB,就有10240个文件,也就有10240个raft实例),假设这一万个raft实例都是leader,则这个存储节点每秒要发送2万个心跳到其他存储节点,同时要接收处理2万个心跳的回复。平均来算,每秒要发送1.35万个心跳和接收处理1.35万个心跳回复。就算这个规模减低10倍,每秒也有1350个心跳和1350个心跳回复。这对CPU的压力和对网络的压力太大了,特别是在光纤直连的多机房中跨机房部署时,心跳对光纤带宽的消耗很大。

  原因2)的分析:

  从raft原理可知,raft先append日志(要落盘),然后抄送给followers,最后提交。这样,一个写请求就需要2次写磁盘操作(3副本总计需要6次写磁盘)。这会导致性能的直接下降。特别是对大数据量的写请求来说,写放大现象特别明显。

       本人比较奇怪的是,近2~3年说到分布式存储,大家必选raft做一些性协议,貌似非raft不可。真的是非raft不可吗?有没有深入去思考在分布式存储中使用raft的问题呢?有没有想过去寻找其他一致性协议呢?

       当然,并非说raft怎么不好。我只是想说,一项技术,首先要适合我们的应用场景才能被选用,不能一谈到raft就觉得很高大上很怎么着。

       除了raft(以及ZK,Paxos等,他们用在分布式存储系统中都有类似),其实还有更适合分布式存储的一致性协议!

       首先,从上下文的角度来分析,当多个参与者完全对等,同时没有任何外力可借用的情况下,一致性协议只有raft(以及ZK,Paxos等)。但是,当多个参与者有外力可借用的场景下,就可以有其他选择。在分布式存储场景中,有这样的外力存在:管理节点,以及客户端。这就为我们打开了一扇门,否则下面讲的都是废话。

       先说几个基础概念。

       1)下文的大文件均是指上一篇博文中的大文件,也就是站在集群角度的大文件,一致性的最小单位。

       2)管理节点为一主两从(至于主从选举可用raft的无日志状态选举来实现,这个相对完整的raft要简单很多),主故障后会很快选出新主。

       3)各个大文件都有versionId和termId。每个写请求对应一个versionId,versionId之递增。每次副本的leader发生切换,termID递增。VersionId和termID作为大文件的元数据持久化在大文件各个副本的头部空间。客户端的每个写请求先发送到管理节点,管理节点为其分配versionId和termID,然后携带versionID和termID发送到leader副本的存储节点。

  4)所有存储节点和所有管理节点保持心跳(周期为3秒)。存储节点刚启动时,在心跳中汇报自己所拥有的大文件(version,termID等必要元数据),后续存储节点上新建大文件后只需汇报新的大文件元数据,以及version,termID发生变化的大文件元数据。这样所有管理节点内存中都实时维护着集群所有的大文件元数据,以及所有的存储节点基本信息,主从管理节点的信息延迟只有一个心跳(3秒),从而,管理节点能很方便的进行主从切换。

       5)任何管理节点宕机重启后,会要求存储节点重新汇报其所有的大文件,这样管理节点在几十秒内即可汇总得知集群所有的大文件(可能几千万个甚至几亿个)。

       6)写请求必须先到管理节点(不要担心性能,一般都是读远多于写),分配一个version(递增)和当前termID,记录在内存队列中,并且开始计时,一般是3个心跳还未完成及视为超时。有任何写请求超时之后管理节点暂停该大文件的新写请求。

  7)写请求的3副本中,多数副本(2个副本)成功即认为请求最终成功。

      

  下面进入正题。以3副本为例。

  3个副本在某一时刻必须有其仅有一个leader副本,leader副本的作用有2个:

  1) 保证写入顺序的一致性。某一段时间内来自客户端的多个写请求只能到达3副本中固定的一个。如果一个写请求到达副本1,此时该请求还未在3副本中完全执行完,另一个写请求到达副本2,这样写请求就乱了(Google的GFS允许这样,这背后是有GFS的特定场景,一般场景下不能这样做),无法保证多副本数据的一致性。

  2) 将写请求数据抄送给其他副本,并做到有follower版本(version)落后时,一直向其做增量抄送,或全量抄送(在增量不足以追赶上version时),以保证最终一致性。

  所以这样来看,我们的自定义一致性协议就是raft的变形。但是我们不需要选举,不需要副本之间高频率的心跳,写请求不用先append到日志中,而是只记录到内存中(循环队列)即可。

  怎么确定leader?

  通过死规则指定leader,可以指定IP最小者为leader(可以优化,有些大文件指定IP最小者为leader,有些指定IP最大者为leader,但是规则最初定下来就不能变,持久化到副本的元数据中)。

每个大文件的3个副本存放到哪3个存储节点上,是由管理节点确定,同时客户端的每个写请求必须先到管理节点,获取其3副本所在的3个存储节点IP(还要递增version,获取当前termID)。存储节点故障之后,必须要管理节点分配新的存储节点,递增termID。管理节点即我们这里说的外力。

  先假设某个文件的3副本稳定,没有故障发生。任何写操作都会先到管理节点,得知当前写请求对应的3个副本所在的3个存储节点IP以及version和termID,其IP最小者为leader。然后写请求都从客户端(携带version和termID)发送给这个leader副本所在的存储节点。

  然后,假设有1个副本发生故障。

  1) 不是leader发生故障,且新分配的存储节点IP比现leader副本的IP要大,则此时leader不发生转移,读写不受影响。

  2) 不是leader发生故障,且新分配的存储节点IP比现leader副本的IP要小,则此时leader要发生转移。新的leader副本最初没有任何数据,需要从其他follower上拉取数据,且能根据各个副本version得知最新的数据。这个过程大概要几十秒到几分钟,这几分钟内当前大文件不可写。实际中可以优化,新分配存储节点时尽可能分配IP大的存储节点,避免leader发生转移。同时,管理节点(内存中记录了各个大文件的前leader副本,从而能知道leader发生了转移)做控制:等原leader上的请求都完成(成功或失败,怎么知道?根据version和超时来判断),然后才能继续对该文件写,写请求会发到新的leader。这个等待时间大概10秒以内。对具体某个文件来说,副本故障是极低概率的事情,不会造成什么问题。

  3) 如果是leader发生故障,则新分配存储节点后,一定会发生leader转移(出现新leader),管理节点先递增当前大文件的termID并在心跳中回复给相关存储节点。然后管理节点和2)一样进行处理。

  这里要注意是否有脑裂问题。通过管理节点的协助可避免脑裂:

  1)写请求必须先到管理节点,leader副本发生转移是,必须等到之前的发到原leader副本的写请求全部结束后,才能开始新的写请求(新的写请求会发送到新leader副本上),新旧切换之间写操作服务暂停,一般来说暂停大概3~5个心跳周期(10来秒内)(写请求不会挤压太多,有超时和限流)。

  2)还有,原来的leader副本被管理节点判定为失效之后,实际上其可能还存活着,其上有一些列写请求还在慢吞吞的执行(包括抄送给他认为的followers)。这样几个心跳周期之后,新的leader副本生效了,此时可能同时存在两个leader副本,所以还要补充手段。这种场景下,termID就排上用场了。存储节点上,各个副本的每个写请求,version必须比前面写请求的version大,termId必须和当前副本的termID相同,否则就失败。副本的leader发生转移之后,管理节点会在心跳中及时通知存储节点新termID,存储节点就及时更新相应大文件副本的termID。这样大文件的followers副本接收到原leader副本抄送的写请求之后发现termID不匹配,就会拒绝这些写请求,因此,就算原leader副本还存活着,也不起作用,其上剩余的写请求会失败。

  如果管理节点切换了怎么办?新的主管理节点内存中没有写请求队列,但是新的管理节点能知道某个大文件的leader副本是否发生了改变,因为新的主管理节点知道每个大文件的所有副本的IP,如果有IP发生了变化(失联或者来了新的IP),所有管理节点都同时知道,所以新的主管理节点对比大文件的前后IP即可知道leader是否有变化。如果leader发生了变化,更新termId后,死等几个心跳,原来leader上的写请求要不都结束(要不剩余的因为termID不匹配会失败),然后开始受理新的写请求(新的写请求会发送到新的leader副本上)。这个时间大概3~5个心跳,而且发生管理节点主备切换的概率非常低,所以写请求暂停3~5个心跳不会导致什么问题。

  如果所有管理节点都重启了怎么办?管理节点全部重启之后,会立刻选出新的主,新主上没有写请求记录,采用和主备切换时相同的逻辑来确保原来的写请求都结束后,再开始受理新的写请求。

  如果管理节点都宕机了,没有管理节点可用,怎么办。很简单,写操作不能进行。管理节点一主两备时,这种场景可以忽略。

  综上,脑裂问题可以避免,且不影响服务可用性。

  Leader副本确定好之后,一致性保证就清晰了,也很简单。采用和raft相同的做法。leader一直向followers抄送,直到成功。Leader副本的内存中,记录有followers的version,同时记录有最近的未确认的写请求,如果follower的version落后,就向其发送对应的写请求,直到其回复成功。对客户端来说,所有写请求幂等,失败(以及超时)后必须重试直到成功。

  一致性还涉及到管理节点。存储节点上的任何一个大文件,其version发生变化之后,就在下一个心跳中汇报给(所有)管理节点,主管理节点以此判断该大文件的写请求执行情况,如果有某次写请求超时时间到了,至少2个副本version还未汇报齐全,即认为该次写请求超时。写请求超时之后,暂不再接收新的写请求,以做限流(还可结合其他限流措施)。

   再然后,假设有2个副本发生故障。

  1) 如果leader副本发生故障,则有可能丢失最近的几次写请求的数据(如果该存储节点无法及时恢复)。这在raft中也一样。故障恢复流程和1个副本故障相同。

  2) 如果leader副本没有发生故障,则不会丢数据。故障恢复流程,和1个副本故障相同。

  总结:我这里自定义的一致性协议,和raft非常类似,最主要的区别是借助管理节点,不用raft选举流程,实现起来比raft要简单很多倍。同时不用raft的心跳机制,性能上要好很多倍。个人觉得,在分布式存储场景下,完全可以取代raft。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值