SOFAJRaft 日志复制 - pipeline 实现剖析 | SOFAJRaft 实现原理

SOFAStackScalable Open Financial  Architecture Stack) 是蚂蚁金服自主研发的金融级分布式架构,包含了构建金融级云原生架构所需的各个组件,是在金融场景里锤炼出来的最佳实践。git

SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。github

本文为《剖析 | SOFAJRaft 实现原理》第六篇,本篇做者徐家锋,来自专伟信息,力鲲,来自蚂蚁金服。《剖析 | SOFAJRaft 实现原理》系列由 SOFA 团队和源码爱好者们出品,项目代号:<SOFA:JRaftLab/>,文章尾部有参与方式,欢迎一样对源码热情的你加入。算法

SOFAJRaft :github.com/sofastack/s…服务器

本文的目的是要介绍 SOFAJRaft 在日志复制中所采用的 pipeline 机制,可是做者落笔时忽然以为这个题目有些唐突,咱们不该该假设读者理所应当的对日志复制这个概念已经了然于胸,因此做为一篇解析,我以为仍是应该先介绍一下 SOFAJRaft 中的日志复制是要解决什么问题。网络

概念介绍

SOFAJRaft 是对 Raft 共识算法的 Java 实现。既然是共识算法,就不可避免的要对须要达成共识的内容在多个服务器节点之间进行传输,在 SOFAJRaft 中咱们将这些内容封装成一个个日志块 (LogEntry),这种服务器节点间的日志传输行为在 SOFAJRaft 中也就有了专门的术语:日志复制架构

为了便于阅读理解,咱们用一个象棋的故事来类比日志复制的流程和可能遇到的问题。并发

假设咱们穿越到古代,要为一场即将举办的象棋比赛设计直播方案。固然全部电子通信技术此时都已经不可用了,幸亏象棋比赛是一种能用精简的文字描述赛况的项目,好比:“炮二平五”, “马8进7”, “车2退3”等,咱们将这些描述性文字称为棋谱。这样只要咱们在场外一样摆上棋盘 (可能很大,方便围观),经过棋谱就能够把棋手的对弈过程直播出来。框架

图1 - 经过棋谱直播

图1 - 经过棋谱直播分布式

因此咱们的直播方案就是:赛场内两位棋手正常对弈,设一个专门的记录员来记录棋手走出的每一步,安排一个旗童飞奔于赛场内外,棋手每走一步,旗童就将其以棋谱的方式传递给场外,这样观众就能在场外准实时的观看对弈的过程,得到同观看直播相同的体验。性能

图2 - 一个简单的直播方案

图2 - 一个简单的直播方案

这即是 SOFAJRaft 日志复制的人肉版,接下来咱们完善一下这个“直播系统”,让它逐步对齐真实的日志复制。

改进1. 增长记录员的数量

假设咱们的比赛得到了很高的关注度,咱们须要在赛场外摆出更多的直播场地以供更多的观众观看。

图3 - 更多的直播平台

这样咱们就要安排更多的旗童来传递棋谱,场外的每一台直播都须要一个旗童来负责,这些旗童不停的在赛场内外奔跑传递棋谱信息。有的直播平台离赛场远一些,旗童要跑好久才行,相应的直播延迟就会大一些,而有些直播平台离得很近,对应的旗童就能很快的将对弈状况同步到直播。

随着直播场地的增长,负责记录棋局的记录员的压力就会增长,由于他要针对不一样的旗童每次提供不一样的棋谱内容,有的慢有的快。若是记录员一旦记混了或者眼花了,就会出现严重的直播事故(观众看到的再也不是棋手真正的棋局)。

图4 - 压力很大的记录员

图4 - 压力很大的记录员

为此咱们要做出一些优化,为每一个场外的直播平台安排一个专门的记录员,这样 “赛局-记录员-旗童-直播局” 就构成了单线模式,专人专职高效可靠。

图5 - “赛局-记录员-旗童-直播棋局”

图5 - “赛局-记录员-旗童-直播棋局”

改进2. 增长旗童每次传递的信息量

起初咱们要求棋手每走一步,旗童就向外传递一次棋谱。但是随着比赛进行,其弊端也逐渐显现,一方面记录员记录了不少棋局信息没有传递出去,以致于不得不请求棋手停下来等待 (难以想象);另外一方面,场外的观众对于这种“卡帧”的直播模式也很不满意。

因此咱们作出改进,要求旗童每次多记几步棋,这样记录员不会积攒太多的待直播信息,观众也能一次看到好几步,而这对于聪明的旗童来讲并非什么难事,如此改进达到了双赢的局面。

图6 - 旗童批量携带信息

图6 - 旗童批量携带信息

改进3. 增长快照模式

棋局愈发精彩,应棋迷的强烈要求,咱们临时增长了几个直播场地,这时棋手已经走了不少步了,按照咱们的常规手段,负责新直播的记录员和旗童须要把过去的每一步都在直播棋盘上还原一遍(回放的过程),与此同时棋手还在不断下出新的内容。

从直觉上来讲这也是一种很不聪明的方式,因此这时咱们采用快照模式,再也不要求旗童传递过去的每一步棋谱,而是把当前的棋局图直接描下来,旗童将图带出去后,按照图谱直接摆子。这样新直播平台就能快速追上棋局进度,让观众欣赏到赛场同步的棋局对弈了。

图7 - 采用快照模式

图7 - 采用快照模式

改进4. 每个直播平台用多个旗童传递信息

虽然咱们以前已经在改进 2 中增长了旗童每次携带的信息量,可是在一些状况下(棋手下快棋、直播平台很远等),记录员依然没法将信息及时同步给场外。这时咱们须要增长多个旗童,各旗童有次序的将信息携带到场外,这样记录员就能够更快速的把信息同步给场外直播平台。

图8 - 利用多个旗童传递信息,实现 pipeline 效果

图8 - 利用多个旗童传递信息,实现 pipeline 效果

如今这我的肉的直播平台在咱们的逐步改进下已经具有了 SOFAJRaft 日志复制的下面几个主要特色:

特色1: 被复制的日志是有序且连续的

若是棋谱传递的顺序不同,最后下出的棋局可能也是彻底不一样的。而 SOFAJRaft 在日志复制时,其日志传输的顺序也要保证严格的顺序,全部日志既不能乱序也不能有空洞 (也就是说不能被漏掉)。

图9 - 日志保持严格有序且连续

图9 - 日志保持严格有序且连续

特色2: 复制日志是并发的

SOFAJRaft 中 Leader 节点会同时向多个 Follower 节点复制日志,在 Leader 中为每个 Follower 分配一个 Replicator,专用来处理复制日志任务。在棋局中咱们也针对每一个直播平台安排一个记录员,用来将对弈棋谱同步给对应的直播平台。

图10 - 并发复制日志

图10 - 并发复制日志

特色3: 复制日志是批量的

SOFAJRaft 中 Leader 节点会将日志成批的复制给 Follower,就像旗童会每次携带多步棋信息到场外。

图11 - 日志被批量复制

图11 - 日志被批量复制

特色4: 日志复制中的快照

在改进 3 中,咱们让新加入的直播平台直接复制当前的棋局,而再也不回放过去的每一步棋谱,这就是 SOFAJRaft 中的快照 (Snapshot) 机制。用 Snapshot 可以让 Follower 快速跟上 Leader 的日志进度,再也不回放很早之前的日志信息,即缓解了网络的吞吐量,又提高了日志同步的效率。

特色5: 复制日志的 pipeline 机制

在改进 4 中,咱们让多个旗童参与信息传递,这样记录员和直播平台间就能够以“流式”的方式传递信息,这样既能保证信息传递有序也能保证信息传递持续。

在 SOFAJRaft 中咱们也有相似的机制来保证日志复制流式的进行,这种机制就是 pipeline。Pipeline 使得 Leader 和 Follower 双方再也不须要严格听从 “Request - Response - Request” 的交互模式,Leader 能够在没有收到 Response 的状况下,持续的将复制日志的 AppendEntriesRequest 发送给 Follower。

在具体实现时,Leader 只须要针对每一个 Follower 维护一个队列,记录下已经复制的日志,若是有日志复制失败的状况,就将其后的日志重发给 Follower。这样就能保证日志复制的可靠性,具体细节咱们在源码解析中再谈。

图12 - 日志复制的 pipeline 机制

图12 - 日志复制的 pipeline 机制

源码解析

上面就是日志复制在原理层面的介绍,而在代码实现中主要是由 Replicator 和 NodeImpl 来分别实现 Leader 和 Follower 的各自逻辑,主要的方法列于下方。在处理源码中有三点值得咱们关注。

图13 - 相关的方法

图13 - 相关的方法

关注1: Replicator 的 Probe 状态

图14 - Replicator 的状态

图14 - Replicator 的状态

Leader 节点在经过 Replicator 和 Follower 创建链接以后,要发送一个 Probe 类型的探针请求,目的是知道 Follower 已经拥有的的日志位置,以便于向 Follower 发送后续的日志。

图15 - 发送探针来知道 follower 的 logindex

图15 - 发送探针来知道 follower 的 logindex

关注2: 用 Inflight 来辅助实现 pipeline

Inflight 是对批量发送出去的 logEntry 的一种抽象,他表示哪些 logEntry 已经被封装成日志复制 request 发送出去了。

图16 - Inflight 结构

图16 - Inflight 结构

Leader 维护一个 queue,每发出一批 logEntry 就向 queue 中 添加一个表明这一批 logEntry 的 Inflight,这样当它知道某一批 logEntry 复制失败以后,就能够依赖 queue 中的 Inflight 把该批次 logEntry 以及后续的全部日志从新复制给 follower。既保证日志复制可以完成,又保证了复制日志的顺序不变。

这部分从逻辑上来讲比较清晰,可是代码层面须要考虑的东西比较多,因此咱们在此处贴出源码,读者能够在源码中继续探索。

图17 - 复制日志的主要方法

图17 - 复制日志的主要方法

图18 - 添加 Inflight 到队列中

图18 - 添加 Inflight 到队列中

固然在日志复制中其实还要考虑更加复杂的状况,好比一旦发生切换 leader 的状况,follower 该如何应对,这些问题但愿你们可以进入源码来寻找答案。

关注3: 通讯层采用单线程 & 单连接

在 pipeline 机制中,虽然咱们在 SOFAJRaft 层面经过 Inflight 队列保证了日志是被有序的复制,对于乱序传输的 LogEntry 经过各类异常流程去排除掉,可是这些被排除掉的乱序日志最终仍是要经过重传来保证最终成功,这就会影响日志复制的效率。

图19 - 通讯层不能保证有序

图19 - 通讯层不能保证有序

如上图所示,发送端的 Connection Pool 和 接收端的 Thread Pool 都会让本来“单行道”上有序传输的日志进入“多车道”,于是没法保证有序。因此在通讯层面 SOFAJRaft 作了两部分优化去尽可能保证 LogEntry 在传输中不会乱序。

  1. 在 Replicator 端,经过 uniqueKey 对日志传输所用的 Url 进行特殊标识 ,这样 SOFABolt (SOFAJRaft 底层所采用的通讯框架) 就会为这种 Url 创建单一的链接,也就是发送端的 Connection Pool 中只有一条可用链接。

图20 - 经过 uniqueKey 定制 Url

图20 - 经过 uniqueKey 定制 Url

  1. 在接收端不采用线程池派发任务,增长判断 _dispatch_msg_list_in_default_executor_ 使得咱们能够经过 io 线程直接将任务投递到 Processor 中。咱们对 SOFABolt 作过一些功能加强,这里提供相关 PR #84 ,有兴趣的读者能够前往了解。

图21 - SOFABolt 利用 IO 线程派发 AppendEntriesRequest 到 Processor

图21 - SOFABolt 利用 IO 线程派发 AppendEntriesRequest 到 Processor

这样日志复制的通讯模型就变成了咱们指望的“单行道”的模式。这种“单行道”可以很大程度上保证传输的日志是有序且连续的,从而提高了 pipeline 的效率。

图22 - 优化通讯模型

图22 - 优化通讯模型

总结

日志复制并非一个复杂的概念,pipeline 机制也是一种符合直觉思惟的优化方式,甚至在咱们的平常生活中也能找到这些概念的实践。在 SOFAJRaft 中,日志复制的真正难点是如何在分布式环境下既考虑到各类细节和异常,又保证高性能。本文只是从概念上尝试介绍了日志复制,更多的细节还需读者进入代码去寻找答案。

SOFAJRaft 源码解析系列阅读

公众号:金融级分布式架构(Antfin_SOFA)

相关文章
相关标签/搜索