Raft: 一点阅读笔记 (4)

上述1、2情景都可以推断存在一个合法的存活的Leader,因此拒绝投票以避免新Leader的产生;3是一个特例,因为Leader Transfer机制本身就是期望能在一个election timeout内选举出一个新的Leader。

Leader Transfer

Leader Transfer是做负载均衡的策略之一,例如说将Leader的角色交给一台性能更好地服务器;同时也是必要的,例如说我们希望关停某台机器进行维护,而它偏偏是个Leader,关停它可能会丢掉部分请求且导致一段时间内集群不可用;

Leader Transfer的规则大致如下:

当前Leader拒接一切client的请求,因为Leader会改变,所以这些请求大概率会被截断,不如不接;

Leader检查transferee的match Index,如果transferee缺少日志的话,则全量将自己的日志发送给它;

使用一条MsgTimeoutNow告知Transferee计时器立刻超时并触发选举流程;

如果一个election timeou中Leader仍未发现自己被罢黜,那么应当立刻撤销Leader Trasnferee的过程,恢复接受client的请求;

回顾一下我之前担心的几个问题:

当前的Leader不是最新的Leader;

过期的LeaderTransferee;

与lease这种基于election timeout的策略产生冲突;

在etcd的实现中,Leader在tick时都会立刻进行一次check_qurom,如果check_qurom失败了就会立刻主动下台(在原论文中没有提及Leader主动下台的情景),这样虽然存在假阳性的情况,但无疑大大避免了问题1的情景;

问题3主要在考虑原Leader的lease期会和transferee的Leader期存在交集导致了一种类似”双主“的情景,但原Leader此时已经停止接收一切客户请求了,因此应该不会出现问题;

比较麻烦的是2;Leader Transfer消息被作为本地消息并没有被标注消息发出时的term,这意味着Follower会相应并执行一切的这些消息,这可能扰乱集群;选举规则应该能避免这种情景;

Group Commit

Group Commit并不是一种优化,而是一种对Commit情景的更加严格的限制;初始阶段假定所有的peers都属于同一个group,也可以为每个peer指定不同的group_id;提交时,要求entries至少备份到不同的group上,否则不允许提交;

Tests group commit.

Logs should be replicated to at least different groups before committed;

all peers are configured to the same group, simple quorum should be used.

优化类型

从这部分开始可以讨论一下Raft的优化了。优化是一个很大的课题,可以优化的层次非常多:

Multi - Raft,对 Key - Value 进行range - based或者hash - based的分片(Sharding);

prepare优化,即增添Follower -> Leader 的日志流,打破Leader必须拥有全部已提交日志的限制条件;

Batch、Pipeline;

读优化,例如ReadIndex、Follower Read、Lease Read;

落盘、通信的优化等;

优化的层次非常多,这里只讨论内联到算法内部的优化;即3和4;2我只听说PolarDB实现了,但没见过。

ReadIndex、Follower Read、Lease Read

首先,不要把读操作狭义的理解为Key - Value中的读一个或几个Key的操作。读操作应该抽象成某个时刻的状态机的部分或全部状态的只读操作;因此Raft中不会提供读操作的具体实现,而是仅仅告知应用层读操作可以进行。

ReadIndex可以满足强一致读,即必定返回的是某个时刻的最新数据。请求到来时会生成一条ReadIndex记录,其中记录了这个时刻的commitIndex和读请求本身的ctx;

如果是Follower收到了读请求,Follower会将这个请求转发给Leader;Leader收到了读请求时,会记录下这个请求以及请求到来时的commitIndex;在发送心跳包/AppendEntries时,会记录下有哪些Follower成功响应。如果超过Qurom的Follower们回复了响应,那么当appliedIndex执行到那个读请求的commitIndex时,那个读请求以及之前的所有读请求都可以正确的执行了,此时Raft会向应用层返回一个ReadState,里面记录了所有可以实现一致读的请求,应用层根据ReadState检索并执行读请求;如果这个读请求来自于Follower,那么一条MsgReadIndexResp也将转发给Follower,里面记录了相应的commitIndex以及读请求的ctx,Follower也可以根据这条消息生成自己的ReadState返还给应用层;

那么ReadIndex为什么满足强一直读呢?注意经过一轮Qurom响应后,Leader即可确认在读请求到来时刻,自己的commitIndex是那个时刻最新的commitIndex,那么那个时刻的最新状态,就应该是commitIndex之前所有的日志均被执行后的状态,即appliedIndex == commitIndex时的状态。这样也可以理解为什么经过了Qurom check之后,仍然要等待appliedIndex == commitIndex才能响应读请求了。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zwyszy.html