JRaft--基础--03--实现细节解析之高效的线性一致读

JRaft–基础–03–实现细节解析之高效的线性一致读


1、线性一致读

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

在这里插入图片描述

如上图 Client A、B、C、D 均符合线性一致读,其中 D 看起来是 stale read(脏读),其实并不是,D 请求横跨了 3 个阶段,而读可能发生在任意时刻,所以读到 1 或 2 都行

2、实现线性一致读

重要:接下来的讨论均基于一个大前提,就是业务状态机的实现必须是满足线性一致性的,简单说就是也要具有 java volatile 的语义

2.1、怎么实现

  1. 要实现线性一致读,首先我们简单直接一些,是否可以直接从当前 leader 节点读?
    1. 仔细一想,这显然行不通,因为你无法确定这一刻当前的 “leader” 真的是 leader,比如在网络分区的情况下,它可能已经被推翻王朝却不自知
  2. 最简单易懂的实现方式:
    1. 同 “写” 请求一样,“读” 请求也走一遍 raft 协议(raft log)
      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6AOS3qoY-1677998584462)(./image1/9.png)]

    2. 这一定是可以的,但性能上显然不会太出色,走 raft log 不仅仅有日志落盘的开销,还有日志复制的网络开销,另外还有一堆的 raft “读日志” 造成的磁盘占用开销,这在读比重很大的系统中通常是无法被接受的

2.2、ReadIndex Read 方式

2.2.1、这是 raft 论文中提到的一种优化方案,具体来说:

  1. Leader 将自己当前 log 的 commitIndex 记录到一个 local 变量 ReadIndex 里面
  2. 接着向 followers 发起一轮 heartbeat,如果半数以上节点返回了对应的 heartbeat response,那么 leader 就能够确定现在自己仍然是 leader(证明了自己是自己)
  3. Leader 等待自己的状态机执行,直到 applyIndex 超过了 ReadIndex,这样就能够安全的提供 Linearizable Read 了,也不必管读的时刻是否 leader 已飘走
    1. 思考:为什么等到 applyIndex 超过了 ReadIndex 就可以执行读请求?
  4. Leader 执行 read 请求,将结果返回给 Client

2.2.2、通过ReadIndex,也可以很容易在 followers 节点上提供线性一致读

  1. Follower 节点向 leader 请求最新的 ReadIndex
  2. Leader 执行上面前 3 步的过程(确定自己真的是 leader),并返回 ReadIndex 给 follower
  3. Follower 等待自己的 applyIndex 超过了 ReadIndex
  4. Follower 执行 read 请求,将结果返回给 client(JRaft 中可配置是否从 follower 读取,默认不打开)

2.2.3、ReadIndex小结

相比较于走 raft log 的方式,ReadIndex 省去了磁盘的开销,能大幅度提升吞吐,结合 JRaft 的 batch + pipeline ack + 全异步机制,三副本的情况下 leader 读的吞吐可以接近于 RPC 的吞吐上限

延迟取决于多数派中最慢的一个 heartbeat response,理论上对于降低延时的效果不会非常显著

2.3、Lease Read

  1. Lease read 与 ReadIndex 类似,但更进一步,不仅省去了 log,还省去了网络交互。它可以大幅提升读的吞吐也能显著降低延时

  2. 基本的思路:

    1. leader 取一个比 election timeout 小的租期(最好小一个数量级),在租约期内不会发生选举,这就确保了 leader 不会变,所以可以跳过 ReadIndex 的第2步,也就降低了延时。可以看到 Lease read 的正确性和时间是挂钩的,因此时间的实现至关重要,如果时钟漂移严重,这套机制就会有问题
  3. 实现方式:

    1. 定时 heartbeat 获得多数派响应,确认 leader 的有效性(在 JRaft 中默认的 heartbeat 间隔是 election timeout 的十分之一)
    2. 在租约有效时间内,可以认为当前 leader 是 raft group 内的唯一有效 leader,可忽略 ReadIndex 中的 heartbeat 确认步骤(2)
    3. Leader 等待自己的状态机执行,直到 applyIndex 超过了 ReadIndex,这样就能够安全的提供 Linearizable Read 了

2.4、更进一步:Wait Free

  1. 到目前为止 lease 省去了 ReadIndex 的第 2 步(heartbeat),实际上还能再进一步,继续省去第 3 步

  2. 我们想想前面的实现方案的本质是什么:

    1. 首先我们定义两个状态:日志状态(log_state)和状态机状态(st_state),Leader 的 log_state 反映了当前 raft group 最新的数据状态,因为所有的写请求一定都先记录在 raft log 中
    2. 当 leader 接收到 read_request 那一刻,以 log_state 作为逻辑时间参考点,等到 st_state 追上之前记录 log_state 时,显然 read_request 那个时间点的所有数据已经全部应用到状态机,自然是能保证线性一致读了(只要你的业务状态机能保证可见性)
    3. 总结起来即是等待当前节点的状态机达到了接收 read_request 那一刻的时间点相同甚至更新的状态(applyIndex >= commitIndex)
  3. 通过以上分析可以看到 applyIndex >= commitIndex 的约束其实很保守,本质上我们只要保证当前时刻,当前节点状态机一定是最新即可

  4. 那么问题来了,leader 节点的状态机能保证一定是最新的吗?

    1. 首先 leader 节点的 log 一定是最新的,即使新选举产生的 leader,它也一定包含全部的 commit log,但它的状态机却可能落后于旧的 leader
    2. 不过等到 leader 成功应用了自己当前 term 的第一条 log 之后,它的状态机就一定是最新的
    3. 所以可以得出结论:当 leader 已经成功应用了自己 term 的第一条 log 之后,不需要再取 commitIndex,也不用等状态机,直接读,一定是线性一致读
  5. 小结:可以想象,Wait Free 机制将最大程度的降低读延迟

3、在 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()));
            }
        }
    });
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值