赞
踩
Raft 是一种更易于理解的分布式共识算法,核心协议本质上还是师承 paxos 的精髓,不同的是依靠 raft 模块化的拆分以及更加简化的设计,raft 协议相对更容易实现。
模块化的拆分主要体现在:Raft 把一致性协议划分为 Leader 选举、MemberShip 变更、日志复制、Snapshot 等几个几乎完全解耦的模块
更加简化的设计则体现在:Raft 不允许类似 paxos 中的乱序提交、简化系统中的角色状态(只有 Leader、Follower、Candidate三种角色)、限制仅 Leader 可写入、使用随机化的超时时间来设计 Leader Election 等等
一句话总结 Strong Leader: “你们不要 BB! 按我说的做,做完了向我汇报!” 另外,身为 leader 必须保持一直 BB(heartbeat) 的状态,否则就会有别人跳出来想要 BB
对于一个无限增长的序列 a[1, 2, 3…],如果对于任意整数 i,a[i] 的值满足分布式一致性,这个系统就满足一致性状态机的要求 基本上所有的真实系统都会有源源不断的操作,这时候单独对某个特定的值达成一致显然是不够的。为了让真实系统保证所有的副本的一致性,通常会把操作转化为 write-ahead-log(WAL)。然后让系统中所有副本对 WAL 保持一致,这样每个副本按照顺序执行 WAL 里的操作,就能保证最终的状态是一致的
RTT << Heartbeat timeout < Election timeout(ET) << MTBF
Random(ET, 2ET)
(TermId, LogIndex, LogValue)
(TermId, LogIndex)
能确定唯一一条日志(prevTermId, prevLogIndex)
(TermId, LogIndex)
:
JRaft 是一个基于 RAFT 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。 使用 JRaft 你可以专注于自己的业务领域,由 JRaft 负责处理所有与 RAFT 相关的技术难题,并且 JRaft 非常易于使用,你可以通过几个示例在很短的时间内掌握它。
JRaft 是从百度的 braft 移植而来,做了一些优化和改进,感谢百度 braft 团队开源了如此优秀的 C++ RAFT 实现
Symmetric network partition tolerance:对称网络分区容忍性
如上图 S1 为当前 leader,网络分区造成 S2 不断增加本地 term,为了避免网络恢复后 S2 发起选举导致正在工作的 leader step-down,从而导致整个集群重新发起选举,JRaft 中增加了 pre-vote 来避免这个问题的发生。
Asymmetric network partition tolerance:非对称网络分区容忍性
如上图 S1 为当前 leader,S2 不断超时触发选主,S3 提升 term 打断当前 lease,从而拒绝 leader 的更新。
Fault tolerance:容错性,少数派故障不影响系统整体可用性,包括但不限于:
Workaround when quorate peers are dead:多数派故障时,整个 grop 已不具备可用性,安全的做法是等待多数节点恢复,只有这样才能保证数据安全;但是如果业务更加追求系统可用性,可以放弃数据一致性的话,JRaft 提供了手动触发 reset_peers 的指令以迅速重建整个集群,恢复集群可用
Metrics:JRaft 内置了基于 metrics 类库的性能指标统计,具有丰富的性能统计指标,利用这些指标数据可以帮助用户更容易找出系统性能瓶颈
Jepsen:除了几百个单元测试以及部分 chaos 测试之外, JRaft 还使用 jepsen 这个分布式验证和故障注入测试框架模拟了很多种情况,都已验证通过:
除了功能上的完整性,JRaft 还做了很多性能方面的优化,这里有一份 KV 场景(get/put)的 benchmark 数据, 在小数据包,读写比例为 9:1,保证线性一致读的场景下,三副本最高可以达到 40w+ 的 ops。
这里挑重点介绍几个优化点:
apply(task)
用于向 raft group 组成的复制状态机集群提交新任务应用到业务状态机onApply(Iterator)
方法, 应用通过 Node#apply(task)
提交的日志到业务状态机单个节点的 JRaft-node 是没什么实际意义的,下面是三副本的 JRaft 架构图
单个 Raft group 是无法解决大流量的读写瓶颈的,JRaft 自然也要支持 multi-raft-group
什么是线性一致读? 所谓线性一致读,一个简单的例子就是在 t1 的时刻我们写入了一个值,那么在 t1 之后,我们一定能读到这个值,不可能读到 t1 之前的旧值 (想想 java 中的 volatile 关键字,说白了线性一致读就是在分布式系统中实现 java volatile 语义)
如上图 Client A、B、C、D 均符合线性一致读,其中 D 看起来是 stale read,其实并不是,D 请求横跨了 3 个阶段,而读可能发生在任意时刻,所以读到 1 或 2 都行
重要:接下来的讨论均基于一个大前提,就是业务状态机的实现必须是满足线性一致性的,简单说就是也要具有 java volatile 的语义
最简单易懂的实现方式:同 ‘写’ 请求一样,’读’ 请求也走一遍 raft 协议 (raft log)
ReadIndex Read
Lease Read
更进一步:Wait Free
在 JRaft 中发起一次线性一致读请求的代码展示:
- // 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 全局的中心总控节点,负责整个集群的调度,不需要自管理的集群可不启用 PD (一个 PD 可管理多个集群,基于 clusterId 隔离)
Store 集群中的一个物理存储节点,一个 store 包含一个或多个 region
Region 最小的 KV 数据单元,每个 region 都有一个左闭右开的区间 [startKey, endKey), 可根据请求流量/负载/数据量大小等指标自动分裂以及自动副本搬迁
特点
以上几点(尤其2,3) 基本都是依托于 JRaft 自身的功能来实现,详细介绍请参考 JRaft 文档
感谢 braft、etcd、tikv 贡献了优秀的 raft 实现,JRaft 受益良多
蚂蚁金服中间件团队持续在寻找对于基础中间件(如消息、数据中间件以及分布式计算等)以及下一代高性能面向实时分析的时序数据库等方向充满热情的小伙伴加入,有意者请联系 boyan@antfin.com
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。