一致性协议raft详解(二):安全性

前言

有关一致性协议的资料网上有很多,当然错误也有很多。笔者在学习的过程中走了不少弯路。现在回过头来看,最好的学习资料就是Leslie LamportDiego Ongaro的数篇论文、Ongaro在youtube上发的三个视频讲解,以及何登成的ppt。

本系列文章是只是笔者在学习一致性协议过程中的摘抄和总结,有疏漏之处敬请谅解,欢迎讨论。

安全性

raft的log replication机制并不能充分的保证每一个状态机会按照相同的顺序执行相同的指令。例如,一个follower可能会进入不可用状态同时leader已经提交了若干的日志条目,然后这个follower可能会被选举为leader并且覆盖这些日志条目;因此,不同的状态机可能会执行不同的指令序列

我们通过在leader election增加限制来完善raft算法。在任何基于leader的一致性算法中,leader都必须存储所有已经提交的日志条目(或者通过额外的机制保证leader丢失的日志条目可以同步给leader)。Raft 使用了一种更加简单的方法,它可以保证所有之前的任期号中已经提交的日志条目在选举的时候都会出现在新的leader中,不需要传送这些日志条目给leader。这意味着日志条目的传送是单向的,只从leader传给跟随者,并且leader从不会覆盖自身本地日志中已经存在的条目。

**由于raft节点中的日志是顺序添加的,那么raft在leader election的时候通过比较两份日志中最后一条日志目录的索引号index和任期号term,就可以判断出谁的日志更新。**只有最新的人才有权利作为leader。候选人为了赢得选举必须联系集群中的大部分节点,这意味着每一个已经提交额度日志条目至少会存在于这些节点中的至少一个上。如果候选人的日志至少和大多数的服务器节点一样新,那么他一定持有了所有已经提交的日志条目

log recovery

以下摘自baidu braft文章

Log Recovery这里分为current Term修复和prev Term修复,Log Recovery就是要保证一定已经Committed的数据不会丢失,未Committed的数据转变为Committed,但不会因为修复过程中断又重启而影响节点之间一致性。

  • current Term修复主要是解决某些Follower节点重启加入集群,或者是新增Follower节点加入集群,Leader需要向Follower节点传输漏掉的Log Entry,如果Follower需要的Log Entry已经在Leader上Log Compaction清除掉了,Leader需要将上一个Snapshot和其后的Log Entry传输给Follower节点。
  • prev Term修复主要是在保证Leader切换前后数据的一致性。通过上面RAFT的选主可以看出,每次选举出来的Leader一定包含已经committed的数据(抽屉原理,选举出来的Leader是多数中数据最新的,一定包含已经在多数节点上commit的数据),新的Leader将会覆盖其他节点上不一致的数据。虽然新选举出来的Leader一定包括上一个Term的Leader已经Committed的Log Entry,但是可能也包含上一个Term的Leader未Committed的Log Entry。这部分Log Entry需要转变为Committed,相对比较麻烦,需要考虑Leader多次切换且未完成Log Recovery,需要保证最终提案是一致的,确定的。 **RAFT中增加了一个约束:对于之前Term的未Committed数据,修复到多数节点,且在新的Term下至少有一条新的Log Entry被复制或修复到多数节点之后,才能认为之前未Committed的Log Entry转为Committed。**下图就是一个prev Term Recovery的过程:

raft figure8

  1. S1是Term2的Leader,将LogEntry部分复制到S1和S2的2号位置,然后Crash。
  2. S5被S3、S4和S5选为Term3的Leader,并只写入一条LogEntry到本地,然后Crash。
  3. S1被S1、S2和S3选为Term4的Leader,并将2号位置的数据修复到S3,达到多数;并在本地写入一条Log Entry,然后Crash。
  4. 这个时候2号位置的Log Entry虽然已经被复制到多数节点上,但是并不是Committed
    1. S5被S3、S4和S5选为Term5的Leader,将本地2号位置Term3写入的数据复制到其他节点,覆盖S1、S2、S3上Term2写入的数据
    2. S1被S1、S2、S3选为Term5的Leader,将3号位置Term4写入的数据复制到S2、S3,使得2号位置Term2写入的数据变为Committed

为了避免4-1中的现象,协议又强化了一个限制:

  • 只有当前 Term 的 LogEntry 提交条件为:满足多数派响应之后(一半以上节点 Append LogEntry 到日志)设置为 commit;
  • 前一轮 Term 未 Commit 的 LogEntry 的 Commit 依赖于高轮 Term LogEntry 的 Commit。

也就是说,我们要通过提交 NO-OP LogEntry 提交系统可用性

在 Leader 通过竞选刚刚成为 Leader 的时候,有一些等待提交的 LogEntry (即 SN > CommitPt 的 LogEntry),有可能是 Commit 的,也有可能是未 Commit 的(PS: 因为在 Raft 协议中 CommitPt 不用实时刷盘)。

所以为了防止出现非线性一致性(Non Linearizable Consistency);即之前已经响应客户端的已经 Commit 的请求回退,并且为了避免出现上面4-1中的 Corner Case(已经commit的日志被覆盖),往往我们需要通过下一个 Term 的 LogEntry 的 Commit 来实现之前的 Term 的 LogEntry 的 Commit (隐式commit),才能保障提供线性一致性。

但是有可能接下来的客户端的写请求不能及时到达,那么为了保障 Leader 快速提供读服务,系统可首先发送一个 no-op LogEntry 来保障快速进入正常可读状态。

为什么no-op能解决不一致的问题?

刚当选的leader发送no-op,如果这个no-op可以commit成功那么可以认为这个节点之前所有的日志都已经commit成功了。换句话说,大部分节点已经将当前这个leader写入了log中。

也就是说,通过这个no-op commit,我们commit了leader节点上所有的log。就算后面再有figure8中的覆盖问题,覆盖的也是这个no-op

详情可以看我写的另外一篇文章:raft引入no-op解决了什么问题

成员变更

一文看尽 Raft 一致性协议的关键点

  1. 可从数学上严格证明,只要每次只允许增加或删除一个成员,Cold与Cnew不可能形成两个不相交的多数派。这种方式比较简单,也是etcd的做法。

  2. 如果每次增加超过两个。会导致变更过程中出现多个多数派,所以要引入用两阶段成员变更

  3. 系统状态变化同paxos一样通过日志同步

  4. add node

  5. catch up

  6. transition Cold和Cnew需要同时达成多数派,log replication才算是成功。

Single mempership change

论文中提以下几个关键点:

  1. 由于 Single 方式无论如何 Cold 和 CNew 都会相交,所以 Raft 采用了直接提交一个特殊的 replicated LogEntry 的方式来进行 single 集群关系变更
  2. 跟普通的 LogEntry 提交的不同点,configuration LogEntry 不需要 commit 就生效,只需要 append 到 Log 中即可。( PS: 原文 “The New configuration takes effect on each server as soon as it is added to the server’s log”)。
  3. 后一轮 MemberShip Change 的开始必须在前一轮 MemberShip Change Commit 之后进行,以避免出现多个 Leader 的问题。

注意:

  1. 后一轮 MemberShip Change 的开始必须在前一轮 MemberShip Change Commit 之后进行
  2. 提交特殊的 replicated LogEntry不需要commit(不需要形成多数派),只要有一条日志被commit了,整个系统后续也认为这条日志已经apply成功了。

raft用到的随机时间

  1. raft leader的任期是随机时间(一般情况下不需要指定leader的任期,因为term变动会导致系统不可服务)
  2. Raft 算法使用随机选举超时时间的方法来确保很少会发生选票瓜分的情况,每一任期已经投票的follower节点在过了选举超时时间之后(或者节点刚起来的时候,会等待随机的时间才变为candidate),会变为candidate,并向向其他节点发送requestVote。
  3. 每一个candidate在开始一次选举的时候会重置一个随机的选举超时时间,然后在超时时间内等待投票的结果。

raft 脑裂

Raft 协议强依赖 Leader 节点的可用性来确保集群数据的一致性。数据的流向只能从 Leader 节点向 Follower 节点转移。当 Client 向集群 Leader 节点提交数据后,Leader 节点接收到的数据处于未提交状态(Uncommitted),接着 Leader 节点会并发向所有 Follower 节点复制数据并等待接收响应,确保至少集群中超过半数节点已接收到数据后再向 Client 确认数据已接收。一旦向 Client 发出数据接收 Ack 响应后,表明此时数据状态进入已提交(Committed),Leader 节点再向 Follower 节点发通知告知该数据状态已提交。

在这个过程中,主节点可能在任意阶段挂掉,看下 Raft 协议如何针对不同阶段保障数据一致性的。

数据到达 Leader 节点前

这个阶段 Leader 挂掉不影响一致性,不多说。

数据到达 Leader 节点,但未复制到 Follower 节点

这个阶段 Leader 挂掉,数据属于未提交状态,Client 不会收到 Ack 会认为超时失败可安全发起重试。Follower 节点上没有该数据,重新选主后 Client 重试重新提交可成功。原来的 Leader 节点恢复后作为 Follower 加入集群重新从当前任期的新 Leader 处同步数据,强制保持和 Leader 数据一致。

数据到达 Leader 节点,成功复制到 Follower 所有节点,但还未向 Leader 响应接收

这个阶段 Leader 挂掉,虽然数据在 Follower 节点处于未提交状态(Uncommitted)但保持一致,重新选出 Leader 后可完成数据提交,此时 Client 由于不知到底提交成功没有,可重试提交。针对这种情况 Raft 要求 RPC 请求实现幂等性idempotent),也就是要实现内部去重机制(client如果发了个请求,就要一直无限重试知道成功,并且raft集群也会在自己恢复之前尝试commit但是可能因为节点crash没有commit成功的log(Figure 8)。也就是说raft节点会忽略自己已经commit的log的rpc请求,直接返回成功?)。

数据到达 Leader 节点,成功复制到 Follower 部分节点,但还未向 Leader 响应接收

这个阶段 Leader 挂掉,数据在 Follower 节点处于未提交状态(Uncommitted)且不一致,Raft 协议要求投票只能投给拥有最新数据的节点。所以拥有最新数据的节点会被选为 Leader 再强制同步数据到 Follower,数据不会丢失并最终一致。

疑问

  1. Q: uncommitted日志会作为当前的log entry被保存吗?A: 会的,应该没有commit rpc整个系统就可以运转正常了。
  2. Q: 什么时候回复client?A: leader append log entry 之后应该就可以回了,如果除了leader已经有超过半数写入了,那么不到半数也能回
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值