MinerAG 2019-07-01
SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。 使用 SOFAJRaft 你可以专注于自己的业务领域,由 SOFAJRaft 负责处理所有与 Raft 相关的技术难题,并且 SOFAJRaft 非常易于使用,你可以通过几个示例在很短的时间内掌握它。
SOFAJRaft 是从百度的 braft 移植而来,做了一些优化和改进,感谢百度 braft 团队开源了如此优秀的 C++ Raft 实现。
如何理解分布式共识?
有哪些分布式共识算法?
Raft 是一种更易于理解的分布式共识算法,核心协议本质上还是师承 Paxos 的精髓,不同的是依靠 Raft 模块化的拆分以及更加简化的设计,Raft 协议相对更容易实现。
模块化的拆分主要体现在:Raft 把一致性协议划分为 Leader 选举、MemberShip 变更、日志复制、Snapshot 等几个几乎完全解耦的模块。
更加简化的设计则体现在:Raft 不允许类似 Paxos 中的乱序提交、简化系统中的角色状态(只有 Leader、Follower、Candidate 三种角色)、限制仅 Leader 可写入、使用随机化的超时时间来设计 Leader Election 等等。
特点:Strong Leader
一句话总结 Strong Leader: "你们不要 BB! 按我说的做,做完了向我汇报!"。
另外,身为 Leader 必须保持一直 BB(heartbeat) 的状态,否则就会有别人跳出来想要 BB 。

Raft 中的基本概念
篇幅有限,这里只对 Raft 中的几个概念做一个简单介绍,详细请参考 Raft paper。
Raft-node 的 3 种角色/状态

Message 的 3 种类型
任期逻辑时钟

本图出自《Raft: A Consensus Algorithm for Replicated Logs》
SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。 使用 SOFAJRaft 你可以专注于自己的业务领域,由 SOFAJRaft 负责处理所有与 Raft 相关的技术难题,并且 SOFAJRaft 非常易于使用,你可以通过几个示例在很短的时间内掌握它。
SOFAJRaft 是从百度的 braft 移植而来,做了一些优化和改进,感谢百度 braft 团队开源了如此优秀的 C++ Raft 实现。
SOFAJRaft 整体功能&性能优化

功能支持
1.Leader election:Leader 选举,这个不多说,上面已介绍过 Raft 中的 Leader 机制。
2.Log replication and recovery:日志复制和日志恢复。
Log replication 就是要保证已经被 commit 的数据一定不会丢失,即一定要成功复制到多数派。
Log recovery 包含两个方面:
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 来避免这个问题的发生。
SOFAJRaft 中在 request-vote 之前会先进行 pre-vote(currentTerm + 1, lastLogIndex, lastLogTerm),多数派成功后才会转换状态为 candidate 发起真正的 request-vote,所以分区后的节点,pre-vote 不会成功,也就不会导致集群一段时间内无法正常提供服务。
7.Asymmetric network partition tolerance:非对称网络分区容忍性。

如上图 S1 为当前 leader,S2 不断超时触发选主,S3 提升 term 打断当前 lease,从而拒绝 leader 的更新。
在 SOFAJRaft 中增加了一个 tick 的检查,每个 follower 维护一个时间戳记录下收到 leader 上数据更新的时间(也包括心跳),只有超过 election timeout 之后才允许接受 request-vote 请求。
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。
这里挑重点介绍几个优化点:
Batch: 我们知道互联网两大优化法宝便是 Cache 和 Batch,SOFAJRaft 在 Batch 上花了较大心思,整个链路几乎都是 Batch 的,依靠 disruptor 的 MPSC 模型批量消费,对整体性能有着极大的提升,包括但不限于:
SOFAJRaft 设计

apply(task)用于向 raft group 组成的复制状态机集群提交新任务应用到业务状态机。存储:上图靠下的部分均为存储相关。
Snapshot 存储,用于存放用户的状态机 snapshot 及元信息,可选:
状态机
onApply(Iterator) 方法, 应用通过 Node#apply(task) 提交的日志到业务状态机;复制
RPC:RPC 模块用于节点之间的网络通讯
SOFAJRaft Group
单个节点的 SOFAJRaft-node 是没什么实际意义的,下面是三副本的 SOFAJRaft 架构图:

SOFAJRaft Multi Group
单个 Raft group 是无法解决大流量的读写瓶颈的,SOFAJRaft 自然也要支持 multi-raft-group。

SOFAJRaft 实现细节解析之高效的线性一致读
什么是线性一致读? 所谓线性一致读,一个简单的例子就是在 t1 的时刻我们写入了一个值,那么在 t1 之后,我们一定能读到这个值,不可能读到 t1 之前的旧值 (想想 Java 中的 volatile 关键字,说白了线性一致读就是在分布式系统中实现 Java volatile 语义)。

如上图 Client A、B、C、D 均符合线性一致读,其中 D 看起来是 stale read,其实并不是,D 请求横跨了 3 个阶段,而读可能发生在任意时刻,所以读到 1 或 2 都行。
重要:接下来的讨论均基于一个大前提,就是业务状态机的实现必须是满足线性一致性的,简单说就是也要具有 Java volatile 的语义。
要实现线性一致读,首先我们简单直接一些,是否可以直接从当前 Leader 节点读?

本图出自《Raft: A Consensus Algorithm for Replicated Logs》
这一定是可以的,但性能上显然不会太出色,走 Raft Log 不仅仅有日志落盘的开销,还有日志复制的网络开销,另外还有一堆的 Raft “读日志” 造成的磁盘占用开销,这在读比重很大的系统中通常是无法被接受的。
ReadIndex Read
这是 Raft 论文中提到的一种优化方案,具体来说:
通过ReadIndex,也可以很容易在 Followers 节点上提供线性一致读:
ReadIndex小结:
Lease 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()));
}
}
});
}使用案例
一、基于 SOFAJRaft 设计一个简单的 KV Store

二、基于 SOFAJRaft 的 RheaKV 的设计

功能名词
PD
Store
Region
特点
自驱动
以上几点(尤其2、3) 基本都是依托于 SOFAJRaft 自身的功能来实现,详细介绍请参考 SOFAJRaft 文档 。
感谢 braft、etcd、tikv 贡献了优秀的 Raft 实现,SOFAJRaft 受益良多。
蚂蚁金服中间件团队持续在寻找对于基础中间件(如消息、数据中间件以及分布式计算等)以及下一代高性能面向实时分析的时序数据库等方向充满热情的小伙伴加入,有意者请联系 [email protected]。
本文为云栖社区原创内容,未经允许不得转载。