什么是SOFAJRaft?
SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。 使用 SOFAJRaft 你能够专一于本身的业务领域,由 SOFAJRaft 负责处理全部与 Raft 相关的技术难题,而且 SOFAJRaft 很是易于使用,你能够经过几个示例在很短的时间内掌握它。
SOFAJRaft 是从百度的 braft 移植而来,作了一些优化和改进,感谢百度 braft 团队开源了如此优秀的 C++ Raft 实现。
Raft 是一种更易于理解的分布式共识算法,核心协议本质上仍是师承 Paxos 的精髓,不一样的是依靠 Raft 模块化的拆分以及更加简化的设计,Raft 协议相对更容易实现。html
模块化的拆分主要体如今:Raft 把一致性协议划分为 Leader 选举、MemberShip 变动、日志复制、Snapshot 等几个几乎彻底解耦的模块。node
更加简化的设计则体如今:Raft 不容许相似 Paxos 中的乱序提交、简化系统中的角色状态(只有 Leader、Follower、Candidate 三种角色)、限制仅 Leader 可写入、使用随机化的超时时间来设计 Leader Election 等等。git
一句话总结 Strong Leader: "大家不要 BB! 按我说的作,作完了向我汇报!"。github
另外,身为 Leader 必须保持一直 BB(heartbeat) 的状态,不然就会有别人跳出来想要 BB 。算法
篇幅有限,这里只对 Raft 中的几个概念作一个简单介绍,详细请参考 Raft paper。数据库
本图出自《Raft: A Consensus Algorithm for Replicated Logs》编程
SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。 使用 SOFAJRaft 你能够专一于本身的业务领域,由 SOFAJRaft 负责处理全部与 Raft 相关的技术难题,而且 SOFAJRaft 很是易于使用,你能够经过几个示例在很短的时间内掌握它。缓存
SOFAJRaft 是从百度的 braft 移植而来,作了一些优化和改进,感谢百度 braft 团队开源了如此优秀的 C++ Raft 实现。安全
1.Leader election:Leader 选举,这个很少说,上面已介绍过 Raft 中的 Leader 机制。性能优化
2.Log replication and recovery:日志复制和日志恢复。
2)Prev term 日志恢复:主要针对 Leader 切换先后的日志一致性。
3.Snapshot and log compaction:定时生成 snapshot,实现 log compaction 加速启动和恢复,以及 InstallSnapshot 给 Followers 拷贝数据,以下图:
本图出自《In Search of an Understandable Consensus Algorithm》
4.Membership change:用于集群线上配置变动,好比增长节点、删除节点、替换节点等。
5.Transfer leader:主动变动 leader,用于重启维护,leader 负载平衡等。
6.Symmetric network partition tolerance:对称网络分区容忍性。
如上图 S1 为当前 leader,网络分区形成 S2 不断增长本地 term,为了不网络恢复后 S2 发起选举致使正在良心 工做的 leader step-down,从而致使整个集群从新发起选举,SOFAJRaft 中增长了 pre-vote 来避免这个问题的发生。
7.Asymmetric network partition tolerance:非对称网络分区容忍性。
如上图 S1 为当前 leader,S2 不断超时触发选主,S3 提高 term 打断当前 lease,从而拒绝 leader 的更新。
8.Fault tolerance:容错性,少数派故障不影响系统总体可用性,包括但不限于:
9.Workaround when quorate peers are dead:多数派故障时,整个 grop 已不具有可用性,安全的作法是等待多数节点恢复,只有这样才能保证数据安全;可是若是业务更加追求系统可用性,能够放弃数据一致性的话,SOFAJRaft 提供了手动触发 reset_peers 的指令以迅速重建整个集群,恢复集群可用。
10.Metrics:SOFAJRaft 内置了基于 Metrics 类库的性能指标统计,具备丰富的性能统计指标,利用这些指标数据能够帮助用户更容易找出系统性能瓶颈。
11.Jepsen:除了几百个单元测试以及部分 chaos 测试以外, SOFAJRaft 还使用 jepsen 这个分布式验证和故障注入测试框架模拟了不少种状况,都已验证经过:
除了功能上的完整性,SOFAJRaft 还作了不少性能方面的优化,这里有一份 KV 场景(get/put)的 Benchmark 数据, 在小数据包,读写比例为 9:1,保证线性一致读的场景下,三副本最高能够达到 40w+ 的 ops。
这里挑重点介绍几个优化点:
1. Batch: 咱们知道互联网两大优化法宝即是 Cache 和 Batch,SOFAJRaft 在 Batch 上花了较大心思,整个链路几乎都是 Batch 的,依靠 disruptor 的 MPSC 模型批量消费,对总体性能有着极大的提高,包括但不限于:
批量提交 task
批量网络发送
本地 IO batch 写入
要保证日志不丢,通常每条 log entry 都要进行 fsync 同步刷盘,比较耗时,SOFAJRaft 中作了合并写入的优化。
批量应用到状态机
须要说明的是,虽然 SOFAJRaft 中大量使用了 Batch 技巧,但对单个请求的延时并没有任何影响,SOFAJRaft 中不会对请求作延时的攒批处理。
2. Replication pipeline:流水线复制,一般 Leader 跟 Followers 节点的 Log 同步是串行 Batch 的方式,每一个 Batch 发送以后须要等待 Batch 同步完成以后才能继续发送下一批(ping-pong),这样会致使较长的延迟。SOFAJRaft 中经过 Leader 跟 Followers 节点之间的 pipeline 复制来改进,很是有效下降了数据同步的延迟,提升吞吐。经咱们测试,开启 pipeline 能够将吞吐提高 30% 以上,详细数据请参照 Benchmark。
3. Append log in parallel:在 SOFAJRaft 中 Leader 持久化 log entries 和向 Followers 发送 log entries 是并行的。
4. Fully concurrent replication:Leader 向全部 Follwers 发送 Log 也是彻底相互独立和并发的。
Asynchronous:SOFAJRaft 中整个链路几乎没有任何阻塞,彻底异步的,是一个彻底的 callback 编程模型。
ReadIndex:优化 Raft read 走 Raft log 的性能问题,每次 read,仅记录 commitIndex,而后发送全部 peers heartbeat 来确认 Leader 身份,若是 Leader 身份确认成功,等到 appliedIndex >= commitIndex,就能够返回 Client read 了,基于 ReadIndex Follower 也能够很方便的提供线性一致读,不过 commitIndex 是须要从 Leader 那里获取,多了一轮 RPC;关于线性一致读文章后面会详细分析。
Lease Read:SOFAJRaft 还支持经过租约 (lease) 保证 Leader 的身份,从而省去了 ReadIndex 每次 heartbeat 确认 Leader 身份,性能更好,可是经过时钟维护 lease 自己并非绝对的安全(时钟漂移问题,因此 SOFAJRaft 中默认配置是 ReadIndex,由于一般状况下 ReadIndex 性能已足够好)。
1. Node:Raft 分组中的一个节点,链接封装底层的全部服务,用户看到的主要服务接口,特别是 apply(task)
用于向 raft group 组成的复制状态机集群提交新任务应用到业务状态机。
2.存储:上图靠下的部分均为存储相关。
Log 存储,记录 Raft 用户提交任务的日志,将日志从 Leader 复制到其余节点上。
LogStorage 是存储实现,默认实现基于 RocksDB 存储,你也能够很容易扩展本身的日志存储实现;
LogManager 负责对底层存储的调用,对调用作缓存、批量提交、必要的检查和优化。
Metadata 存储,元信息存储,记录 Raft 实现的内部状态,好比当前 term、投票给哪一个节点等信息。
Snapshot 存储,用于存放用户的状态机 snapshot 及元信息,可选:
SnapshotStorage 用于 snapshot 存储实现;
SnapshotExecutor 用于 snapshot 实际存储、远程安装、复制的管理。
3. 状态机
StateMachine:用户核心逻辑的实现,核心是 onApply(Iterator)
方法, 应用经过 Node#apply(task)
提交的日志到业务状态机;
FSMCaller:封装对业务 StateMachine 的状态转换的调用以及日志的写入等,一个有限状态机的实现,作必要的检查、请求合并提交和并发处理等。
4. 复制
Replicator:用于 Leader 向 Followers 复制日志,也就是 Raft 中的 AppendEntries 调用,包括心跳存活检查等;
ReplicatorGroup:用于单个 Raft group 管理全部的 replicator,必要的权限检查和派发。
5. RPC:RPC 模块用于节点之间的网络通信
RPC Server:内置于 Node 内的 RPC 服务器,接收其余节点或者客户端发过来的请求,转交给对应服务处理;
RPC Client:用于向其余节点发起请求,例如投票、复制日志、心跳等。
6. KV Store:KV Store 是各类 Raft 实现的一个典型应用场景,SOFAJRaft 中包含了一个嵌入式的分布式 KV 存储实现(SOFAJRaft-RheaKV)。
单个节点的 SOFAJRaft-node 是没什么实际意义的,下面是三副本的 SOFAJRaft 架构图:
单个 Raft group 是没法解决大流量的读写瓶颈的,SOFAJRaft 天然也要支持 multi-raft-group。
什么是线性一致读? 所谓线性一致读,一个简单的例子就是在 t1 的时刻咱们写入了一个值,那么在 t1 以后,咱们必定能读到这个值,不可能读到 t1 以前的旧值 (想一想 Java 中的 volatile 关键字,说白了线性一致读就是在分布式系统中实现 Java volatile 语义)。
如上图 Client A、B、C、D 均符合线性一致读,其中 D 看起来是 stale read,其实并非,D 请求横跨了 3 个阶段,而读可能发生在任意时刻,因此读到 1 或 2 都行。
重要:接下来的讨论均基于一个大前提,就是业务状态机的实现必须是知足线性一致性的,简单说就是也要具备 Java volatile 的语义。
1. 要实现线性一致读,首先咱们简单直接一些,是否能够直接从当前 Leader 节点读?
仔细一想,这显然行不通,由于你没法肯定这一刻当前的 "Leader" 真的是 Leader,好比在网络分区的状况下,它可能已经被推翻王朝却不自知。
2. 最简单易懂的实现方式:同 “写” 请求同样,“读” 请求也走一遍 Raft 协议 (Raft Log)
本图出自《Raft: A Consensus Algorithm for Replicated Logs》
这必定是能够的,但性能上显然不会太出色,走 Raft Log 不只仅有日志落盘的开销,还有日志复制的网络开销,另外还有一堆的 Raft “读日志” 形成的磁盘占用开销,这在读比重很大的系统中一般是没法被接受的。
3. ReadIndex Read
这是 Raft 论文中提到的一种优化方案,具体来讲:
Leader 将本身当前 Log 的 commitIndex 记录到一个 Local 变量 ReadIndex 里面;
接着向 Followers 发起一轮 heartbeat,若是半数以上节点返回了对应的 heartbeat response,那么 Leader 就可以肯定如今本身仍然是 Leader (证实了本身是本身);
Leader 等待本身的状态机执行,直到 applyIndex 超过了 ReadIndex,这样就可以安全的提供 Linearizable Read 了,也没必要管读的时刻是否 Leader 已飘走 (思考:为何等到 applyIndex 超过了 ReadIndex 就能够执行读请求?);
Leader 执行 read 请求,将结果返回给 Client。
经过 ReadIndex,也能够很容易在 Followers 节点上提供线性一致读:
Follower 节点向 Leader 请求最新的 ReadIndex;
Leader 执行上面前 3 步的过程(肯定本身真的是 Leader),并返回 ReadIndex 给 Follower;
Follower 等待本身的 applyIndex 超过了 ReadIndex;
Follower 执行 read 请求,将结果返回给 Client。(SOFAJRaft 中可配置是否从 Follower 读取,默认不打开)
ReadIndex小结:
相比较于走 Raft Log 的方式,ReadIndex 省去了磁盘的开销,能大幅度提高吞吐,结合 SOFAJRaft 的 batch + pipeline ack + 全异步机制,三副本的状况下 Leader 读的吞吐能够接近于 RPC 的吞吐上限;
延迟取决于多数派中最慢的一个 heartbeat response,理论上对于下降延时的效果不会很是显著。
4. Lease Read
Lease Read 与 ReadIndex 相似,但更进一步,不只省去了 Log,还省去了网络交互。它能够大幅提高读的吞吐也能显著下降延时。
基本的思路是 Leader 取一个比 election timeout 小的租期(最好小一个数量级),在租约期内不会发生选举,这就确保了 Leader 不会变,因此能够跳过 ReadIndex 的第二步,也就下降了延时。能够看到 Lease Read 的正确性和时间是挂钩的,所以时间的实现相当重要,若是时钟漂移严重,这套机制就会有问题。
实现方式:
定时 heartbeat 得到多数派响应,确认 Leader 的有效性 (在 SOFAJRaft 中默认的 heartbeat 间隔是 election timeout 的十分之一);
在租约有效时间内,能够认为当前 Leader 是 Raft Group 内的惟一有效 Leader,可忽略 ReadIndex 中的 heartbeat 确认步骤(2);
Leader 等待本身的状态机执行,直到 applyIndex 超过了 ReadIndex,这样就可以安全的提供 Linearizable Read 了 。
在 SOFAJRaft 中发起一次线性一致读请求的代码展现:
// KV 存储实现线性一致读
public void readFromQuorum(String key, AsyncContext asyncContext) {
// 请求 ID 做为请求上下文传入
byte[] reqContext = new byte[4];
Bits.putInt(reqContext, 0, requestId.incrementAndGet());
// 调用 readIndex 方法, 等待回调执行
this.node.readIndex(reqContext, new ReadIndexClosure() {
@Override
public void run(Status status, long index, byte[] reqCtx) {
if (status.isOk()) {
try {
// ReadIndexClosure 回调成功,能够从状态机读取最新数据返回
// 若是你的状态实现有版本概念,能够根据传入的日志 index 编号作读取
asyncContext.sendResponse(new ValueCommand(fsm.getValue(key)));
} catch (KeyNotFoundException e) {
asyncContext.sendResponse(GetCommandProcessor.createKeyNotFoundResponse());
}
} else {
// 特定状况下,好比发生选举,该读请求将失败
asyncContext.sendResponse(new BooleanCommand(false, status.getErrorMsg()));
}
}
});
}复制代码
PD
Store
Region
以上几点(尤为二、3) 基本都是依托于 SOFAJRaft 自身的功能来实现,详细介绍请参考 SOFAJRaft 文档 。
感谢 braft、etcd、tikv 贡献了优秀的 Raft 实现,SOFAJRaft 受益良多。
蚂蚁金服中间件团队持续在寻找对于基础中间件(如消息、数据中间件以及分布式计算等)以及下一代高性能面向实时分析的时序数据库等方向充满热情的小伙伴加入,有意者请联系 boyan@antfin.com。
公众号:金融级分布式架构(Antfin_SOFA)