SOFAJRaft 设计
- Node:Raft 分组中的一个节点,连接封装底层的所有服务,用户看到的主要服务接口,特别是 apply(task)用于向 raft group 组成的复制状态机集群提交新任务应用到业务状态机。
- 存储:上图靠下的部分均为存储相关。
- Log 存储,记录 Raft 用户提交任务的日志,将日志从 Leader 复制到其他节点上。
- LogStorage 是存储实现,默认实现基于 RocksDB 存储,你也可以很容易扩展自己的日志存储实现;
- LogManager 负责对底层存储的调用,对调用做缓存、批量提交、必要的检查和优化。
- Metadata 存储,元信息存储,记录 Raft 实现的内部状态,比如当前 term、投票给哪个节点等信息。
- Snapshot 存储,用于存放用户的状态机 snapshot 及元信息,可选:
- SnapshotStorage 用于 snapshot 存储实现;
- SnapshotExecutor 用于 snapshot 实际存储、远程安装、复制的管理。
- 状态机
- StateMachine:用户核心逻辑的实现,核心是 onApply(Iterator) 方法, 应用通过 Node#apply(task) 提交的日志到业务状态机;
- FSMCaller:封装对业务 StateMachine 的状态转换的调用以及日志的写入等,一个有限状态机的实现,做必要的检查、请求合并提交和并发处理等。
- 复制
- Replicator:用于 Leader 向 Followers 复制日志,也就是 Raft 中的 AppendEntries 调用,包括心跳存活检查等;
- ReplicatorGroup:用于单个 Raft group 管理所有的 replicator,必要的权限检查和派发。
- RPC:RPC 模块用于节点之间的网络通讯
- RPC Server:内置于 Node 内的 RPC 服务器,接收其他节点或者客户端发过来的请求,转交给对应服务处理;
- RPC Client:用于向其他节点发起请求,例如投票、复制日志、心跳等。
- KV Store:KV Store 是各种 Raft 实现的一个典型应用场景,SOFAJRaft 中包含了一个嵌入式的分布式 KV 存储实现(SOFAJRaft-RheaKV)。
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 节点读?
- 仔细一想,这显然行不通,因为你无法确定这一刻当前的 "Leader" 真的是 Leader,比如在网络分区的情况下,它可能已经被推翻王朝却不自知。
- 最简单易懂的实现方式:同 “写” 请求一样,“读” 请求也走一遍 Raft 协议 (Raft Log)。
本图出自《Raft: A Consensus Algorithm for Replicated Logs》
这一定是可以的,但性能上显然不会太出色,走 Raft Log 不仅仅有日志落盘的开销,还有日志复制的网络开销,另外还有一堆的 Raft “读日志” 造成的磁盘占用开销,这在读比重很大的系统中通常是无法被接受的。
- 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,理论上对于降低延时的效果不会非常显著。
- 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 了 。
(编辑:ASP站长网)
|