SOFAStackgit
Scalable Open Financial Architecture Stackgithub
是蚂蚁金服自主研发的金融级分布式架构,包含了构建金融级云原生架构所需的各个组件,是在金融场景里锤炼出来的最佳实践。算法
本文为《剖析 | SOFAJRaft 实现原理》第三篇,本篇做者米麒麟,来自陆金所。《剖析 | SOFAJRaft 实现原理》系列由 SOFA 团队和源码爱好者们出品,项目代号:<SOFA:JRaftLab/>,目前领取已经完成,感谢你们的参与。缓存
SOFAJRaft 是一个基于 Raft 一致性算法的生产级高性能 Java 实现,支持 MULTI-RAFT-GROUP,适用于高负载低延迟的场景。安全
SOFAJRaft :github.com/sofastack/s…服务器
线性一致读是在分布式系统中实现 Java volatile 语义,当客户端向集群发起写操做的请求而且得到成功响应以后,该写操做的结果要对全部后来的读请求可见。实现线性一致读常规手段是走 Raft 协议,将读请求一样按照 Log 处理,经过日志复制和状态机执行获取读结果返回给客户端,SOFAJRaft 采用 ReadIndex 替代走 Raft 状态机的方案。本文将围绕 Raft Log Read,ReadIndex Read 以及 Lease Read 等方面剖析线性一致读原理,阐述 SOFAJRaft 如何使用 ReadIndex 和 Lease Read 实现线性一致读:网络
什么是线性一致读? 所谓线性一致读,一个简单的例子是在 t1 的时刻咱们写入了一个值,那么在 t1 以后,咱们必定能读到这个值,不可能读到 t1 以前的旧值(想一想 Java 中的 volatile 关键字,即线性一致读就是在分布式系统中实现 Java volatile 语义)。简而言之是须要在分布式环境中实现 Java volatile 语义效果,即当 Client 向集群发起写操做的请求而且得到成功响应以后,该写操做的结果要对全部后来的读请求可见。和 volatile 的区别在于 volatile 是实现线程之间的可见,而 SOFAJRaft 须要实现 Server 之间的可见。架构
如上图 Client A、B、C、D 均符合线性一致读,其中 D 看起来是 Stale Read,其实并非,D 请求横跨 3 个阶段,而 Read 可能发生在任意时刻,因此读到 1 或 2 都行。app
实现线性一致读最常规的办法是走 Raft 协议,将读请求一样按照 Log 处理,经过 Log 复制和状态机执行来获取读结果,而后再把读取的结果返回给 Client。由于 Raft 原本就是一个为了实现分布式环境下线性一致性的算法,因此经过 Raft 很是方便的实现线性 Read,也就是将任何的读请求走一次 Raft Log,等此 Log 提交以后在 apply 的时候从状态机里面读取值,必定可以保证这个读取到的值是知足线性要求的。异步
固然,由于每次 Read 都须要走 Raft 流程,Raft Log 存储、复制带来刷盘开销、存储开销、网络开销,走 Raft Log不只仅有日志落盘的开销,还有日志复制的网络开销,另外还有一堆的 Raft “读日志” 形成的磁盘占用开销,致使 Read 操做性能是很是低效的,因此在读操做不少的场景下对性能影响很大,在读比重很大的系统中是没法被接受的,一般都不会使用。
在 Raft 里面,节点有三个状态:Leader,Candidate 和 Follower,任何 Raft 的写入操做都必须通过 Leader,只有 Leader 将对应的 Raft Log 复制到 Majority 的节点上面认为这次写入是成功的。因此若是当前 Leader 能肯定必定是 Leader,那么可以直接在此 Leader 上面读取数据,由于对于 Leader 来讲,若是确认一个 Log 已经提交到大多数节点,在 t1 的时候 apply 写入到状态机,那么在 t1 后的 Read 就必定能读取到这个新写入的数据。
那么如何确认 Leader 在处理此次 Read 的时候必定是 Leader 呢?在 Raft 论文里面,提到两种方法:
第一种是 ReadIndex Read,当 Leader 须要处理 Read 请求时,Leader 与过半机器交换心跳信息肯定本身仍然是 Leader 后可提供线性一致读:
使用 ReadIndex Read 提供 Follower Read 的功能,很容易在 Followers 节点上面提供线性一致读,Follower 收到 Read 请求以后:
不一样于经过 Raft Log 的 Read,ReadIndex Read 使用 Heartbeat 方式来让 Leader 确认本身是 Leader,省去 Raft Log 流程。相比较于走 Raft Log 方式,ReadIndex Read 省去磁盘的开销,可以大幅度提高吞吐量。虽然仍然会有网络开销,可是 Heartbeat 原本就很小,因此性能仍是很是好的。
虽然 ReadIndex Read 比原来的 Raft Log Read 快不少,但毕竟仍是存在 Heartbeat 网络开销,因此考虑作更进一步的优化。Raft 论文里面说起一种经过 Clock + Heartbeat 的 Lease Read 优化方法,也就是 Leader 发送 Heartbeat 的时候首先记录一个时间点 Start,当系统大部分节点都回复 Heartbeat Response,因为 Raft 的选举机制,Follower 会在 Election Timeout 的时间以后才从新发生选举,下一个 Leader 选举出来的时间保证大于 Start+Election Timeout/Clock Drift Bound,因此能够认为 Leader 的 Lease 有效期能够到 Start+Election Timeout/Clock Drift Bound 时间点。Lease Read 与 ReadIndex 相似但更进一步优化,不只节省 Log,并且省掉网络交互,大幅提高读的吞吐量而且可以显著下降延时。
Lease Read 基本思路是 Leader 取一个比 Election Timeout 小的租期(最好小一个数量级),在租约期内不会发生选举,确保 Leader 不会变化,因此跳过 ReadIndex 的第二步也就下降延时。因而可知 Lease Read 的正确性和时间是挂钩的,依赖本地时钟的准确性,所以虽然采用 Lease Read 作法很是高效,可是仍然面临风险问题,也就是存在预设的前提即各个服务器的 CPU Clock 的时间是准的,即便有偏差,也会在一个很是小的 Bound 范围里面,时间的实现相当重要,若是时钟漂移严重,各个服务器之间 Clock 走的频率不同,这套 Lease 机制可能出问题。
Lease Read 实现方式包括:
SOFAJRaft 采用 ReadIndex 替代走 Raft 状态机的方案,简而言之是依靠 ReadIndex 原则直接从 Leader 读取结果:全部已经复制到多数派上的 Log(可视为写操做)被视为安全的 Log,Leader 状态机只要按照顺序执行到此条 Log以后,该 Log 所体现的数据就能对客户端 Client 可见,具体分解为如下四个步骤:
经过 ReadIndex 优化,SOFAJRaft 可以达到 RPC 上限的 80%。上面的步骤中发现第 3 步仍然须要 Leader 经过向 Followers 发送心跳确认本身的 Leader 身份,由于 Raft 集群中的 Leader 身份随时可能发生改变。因此 SOFAJRaft 采用 Lease Read 的方式把第 3 步 RPC 省略掉。租约理解为 Raft 集群给 Leader 一段租期 Lease 的身份保证,在此期间不会剥夺 Leader 的身份,这样当 Leader 收到 Read 请求以后,若是发现租期还没有到期,无需再经过和 Followers 通讯来确认本身的 Leader 身份,这样跳过第 3 步的网络通讯开销。经过 Lease Read 优化,SOFAJRaft 几乎已经可以达到 RPC 的上限。然而经过时钟维护租期自己并非绝对的安全(时钟漂移问题),因此 SOFAJRaft 默认配置是线性一致读,由于一般状况下线性一致读性能已足够好。
默认状况下,SOFAJRaft 提供的线性一致读是基于 Raft 协议的 ReadIndex 实现,三副本的状况下 Leader 读的吞吐接近于 RPC 的吞吐上限,延迟取决于多数派中最慢的一个 Heartbeat Response。使用 Node#readIndex(byte [] requestContext, ReadIndexClosure done) 发起线性一致读请求,当安全读取时传入的 Closure 将被调用,正常状况下从状态机中读取数据返回给客户端, SOFAJRaft 将保证读取的线性一致性。线性一致读在任何集群内的节点发起,并不须要强制要求放到 Leader 节点上,容许在 Follower 节点执行,所以大大下降 Leader 的读取压力。
SOFAJRaft 基于 Raft 协议的 ReadIndex 线性一致读实现是调用 RaftServerService#handleReadIndexRequest 接口根据当前节点状态为 STATE_LEADER,STATE_FOLLOWER 以及 STATE_TRANSFERRING 状况处理 ReadIndex 请求:
一、当前节点状态是 STATE_LEADER 即为 Leader 节点,接收 ReadIndex 请求调用 readLeader(request, ReadIndexResponse.newBuilder(), done) 方法提供线性一致读:
二、当前节点状态是 STATE_FOLLOWER 即为 Follower 节点,接收 ReadIndex 请求经过 readFollower(request, done) 方法支持线性一致读:
SOFAJRaft 基于 Batch+Pipeline Ack+ 全异步机制的 ReadIndex 核心逻辑:
SOFAJRaft 针对更高性能要求场景保证集群内机器的 CPU 时钟同步需求,采用 Clock+Heartbeat 的 Lease Read 优化,经过服务端设置 RaftOptions 的 ReadOnlyOption 参数为 ReadOnlyLeaseBased 实现,ReadOnlyLeaseBased 经过依赖 Leader 租约确保只读请求的可线性化,可能受时钟漂移的影响。若是时钟漂移无限制,Leader 节点可能保持租约长于应有的时间(时钟能够向后移动/暂停而没有任何限制),此种状况下 ReadIndex 是不安全的。
SOFAJRaft 基于 Lease Read 线性一致读实现是经过 Leader 节点调用 handleReadIndexRequest 接口接收 ReadIndex 请求获取 ReadIndex 请求级别 ReadOnlyOption 配置,当 ReadOnlyOption 配置为 ReadOnlyLeaseBased 时确认 Leader 租约是否有效即检查 Heartbeat 间隔是否小于 election timeout 时间,Leader 租约超时须要转变为 ReadIndex 模式。Leader 租约有效期间认为当前 Leader 是 Raft Group 内的惟一有效 Leader,忽略 ReadIndex 发送 Heartbeat 确认身份步骤,直接返回 Follower 节点和本地节点 Read 请求成功响应。Leader 节点继续等待状态机执行,直到 applyIndex 超过 ReadIndex 安全提供 Linearizable Read。
SOFAJRaft 基于时钟和心跳实现的线性一致读 Lease Read 优化逻辑:
本文围绕 Raft Log Read,ReadIndex Read 以及 Lease Read 线性一致读实现细节方面剖析 SOFAJRaft 线性一致读基本原理,阐述 SOFAJRaft 如何使用 Batch+Pipeline Ack+全异步机制和 Clock+Heartbeat 手段优化 ReadIndex 和 Lease Read 线性一致读具体实现。
公众号:金融级分布式架构(Antfin_SOFA)