在上一篇文章Xline command 去重机制(一)—— RIFL 介绍中,我们从 command 去重机制的契机开始,介绍了去重的必要性以及目前 Xline 的去重机制存在的一些问题,同时讲解了 RIFL(Reusable Infrastructure for Linearizability) 的工作原理,并对其进行了一些性能分析。本文将在此基础上进一步更深讲解。
CURP 中实现 RIFL 存在的问题
RIFL 的目的是作为一个系统的基础设施,提供 RPC 的 exactly-once semantics,自然也可以应用到 CURP 系统中。在 CURP 的 paper 中,多次提到了 RIFL,以及 RIFL 迁移到 CURP 上需要对其进行的一些更改。
这里推荐没有阅读过之前 Xline 源码解析的读者先去阅读并了解一下 CURP 的原理,再继续阅读会更容易理解。
在 CURP paper §C.1 Modifications to RIFL 中,描述了以下两处更改:
首先,由于 witness 的 replay 机制的存在,任何 command 可能会被重复地被新的 master 接受到,如果我们依赖于 RIFL 提供的去重机制,在 replay command 的时候的乱序可能会导致比较靠后的 acknowledgments 拒绝掉了比较靠前的 command 的 replay。例如,恢复出的 Cmd(first\_incomplete=5), Cmd(first\_incomplete=1),如果按照 5 -> 1 的顺序 replay,那么 1 会被忽略掉。所以在 witness replay 阶段,不进行 processAck。
其次,RIFL 中,server 在 client crash 导致 client\_id 过期时会清理这个 client\_id 下的所有完成记录,这时在 witness replay 中,过期的 client\_id 的 command 会被忽略。所以 client\_id 的过期可能需要延迟到 command 同步到 backup server 上之后。
本文的内容是介绍在 Xline 的 CURP 系统中如何实现 RIFL 并解决一些问题。
Xline 中命令去重的具体实现
Lease Server 的实现
在 RIFL 中,LeaseManager 模块将充当一个 client 存活性的见证者,在 Xline 中,去依赖另一套系统提供 Lease 的机制会令人感到困惑,眼下看来,只有将 Lease Server 实现在 Xline 内部才可以解决这个问题。而在三种节点角色中,Leader 是最适合的。
- 实现 Lease Server 之前,我们需要明确需要的功能:
- Client 在发送第一个 proposal 时需要获取到自己的 client id,所以它需要向 Lease Server 注册并获得一个 client id
- Client 需要每隔一段时间向 Lease Server 发送心跳,以确保 lease 租期长期有效
- Client 在自己的 client id 失效时,需要从 Lease Server 处重新获取一个新的 client id
- Leader 在检查一个 proposal 是否过期时,需要检查 Lease Server 中该 client id 是否过期
- Lease Server 应当在 client id 失效时,将其删除回收
确定了上诉的功能之后,可以得到 Lease Server 的 RPC 定义:
message ClientLeaseKeepAliveRequest {
// The optional client_id, 0 means a grant request
uint64 client_id = 1;
}
message ClientLeaseKeepAliveResponse {
// The refreshed(generated) client_id
uint64 client_id = 1;
}
service Protocol {
...
rpc ClientLeaseKeepAlive (stream ClientLeaseKeepAliveRequest)
returns (ClientLeaseKeepAliveResponse);
}
RPC 的发送端是一串 stream,stream 中除了第一个申请 client id 的请求以外,接下来的请求都是心跳,用于给 lease 续租。
只有在 Lease Server 发现 client id 过期,或者不存在时,则会生成一个新的 client id 并返回给 client,否则的话,server 将会一直不返回消息。
另外,这也变相解决了 RIFL 论文里描述的一个问题:RPC Server 反复向 Lease Server 检查 Lease 的情况会成为一个瓶颈。
Tracker 的实现
回顾一下 RIFL 中的一些组件,可以看到有两个组件十分相似:RequestTracker 和 ResultTracker。
对于 client 端的 RequestTracker,它需要生成一系列连续的 sequence number,并且记录这些 sequence number 的接受情况,进而推动 first incomplete 增加,确认已经收到的位置并发送给 server 进行回收。
同样的,server 端的 ResultTracker 需要记录来自一个 Client 的 sequence number,并检查之前是否重复,最后根据 client 传来的 first incomplete 回收在此之前所有的记录。
我们很容易想到下面关于 sequence number 序列的一个线性的数据结构设计:
绿色表示 client/server 收到来自 server/client 的 返回/请求
红色表示 client/server 暂时未收到来自 server/client 的 返回/请求
First incomplete 则是第一个 没有收到RPC 的序列号,由于 first incomplete 是由 client 首先推动,再同步到 server 端,所以 server 上的 first incomplete 通常会落后一点(上图中用虚线表示)。
考虑到每个格子只有“绿色”和“红色”两种颜色,这种数据结构设计的最优实现是使用 bitvec(位图),可以节约很多内存的开销,并且在遍历的时候可以 batch 加速。
具体的数据结构如下:
/// Layout:
///
/// `010000000101_110111011111_000001000000000`
/// ^ ^ ^ ^
/// | |________________|____|
/// |________| len |____|
/// head tail
#[derive(Debug, Clone)]
struct BitVecQueue {
/// Bits store
store: VecDeque<usize>,
/// Head length indicator
head: usize,
/// Tail length indicator
tail: usize,
}
/// Track sequence number for commands
#[derive(Debug, Default, Clone)]
pub(super) struct Tracker {
/// First incomplete seq num, it will be advanced by client
first_incomplete: u64,
/// inflight seq nums proposed by the client, each bit
/// represent the received status starting from `first_incomplete`.
/// `BitVecQueue` has a better memory compression ratio than `HashSet`
/// if the requested seq_num is very compact.
inflight: BitVecQueue,
}
Tracker 的恢复
上面这些核心的组件正常工作时,一切都显得十分美好,但如果发生了问题,还会这样吗?
比如,leadership transfer 时,新 leader 拿不到旧 leader tracker 中的信息怎么办?很显然,这会出现问题,我们为了性能,每次去重并没有访问状态机,而是维护了一套基于内存的 tracker,就不得不考虑这套数据结构该如何在系统迁移中恢复了。
RIFL 论文中没有详细地介绍如何实现 tracker 的恢复,这是我们实现上遇到的具体的问题。RIFL 作为一个通用机制没有对这种情况展开讨论。不过 RIFL 论文中提到了一点,完成记录需要持久化:
In order to implement exactly-once semantics, RIFL must solve four overall problems: RPC identification, completion record durability, retry rendezvous, and garbage collection
另外,还提出一点,完成记录与操作对象需要存储在一起
RIFL uses a single principle to handle both migration and distributed operations. Each operation is associated with a particular object in the underlying system, and the completion record is stored wherever that object is stored.
如果我们能从操作对象或者完成记录那恢复出 tracker ,就可以避免 tracker 丢失导致误判了。
首先一个问题是完成记录存在哪里,可能这个问题早已有了答案,那就是在 log 里,这条 log 会在系统中被各个节点同步,并应用到自己的状态机上。
但 CURP 系统会有一些区别,Witness 节点上也会存有 proposal,而完成记录也可以来自于这些 proposal,在 CURP 系统的恢复阶段,我们可以从 Witness 中恢复的 proposal 里恢复出 tracker,例如,当我们收到 Cmd(client\_id=1, first\_incomplete=5) 时,就可以将 id 为 1 的 client 的 ResultTracker 更新到 5(恢复阶段注意不要 processAck,这会导致后续 first_incomplete 小于 5 的 command 被过滤)。基于这种机制,我们就不用担心提前 commit 的 proposal 会因为还没持久化而无法恢复出完成记录。
而在具体的实现中,我们结合了上面的两种情况:从 log 中恢复和从 recovered commands 中恢复。这样才可以保证我们完全恢复了 tracker。
真的完全恢复了吗?
这张图将一个 client 发送的 sequence number 序列分成三段,tracker 恢复的目标就是 client 向 server 确认完成的位置以及后面所有这个 client inflight 的 sequence number。
在这三段中,第一段可以不用恢复,client 都已经确认的话,说明已经返回给用户了,所以 client 不可能会重试这段 command。可能之前有重试过这个 command,并且重试的请求在网络中 delay 了很久,但也不会到达新的 leader 上。
第三段我们可以从 Witness 的 Speculative Pool 中恢复。
而对于第二段,默认情况下读取 log 可能恢复不全,在 log 有 compact 的机制下,我们不能预测 compact 的位置是否在第一段中,为了解决彻底解决这个问题,我们需要对 log compact 做出限制:不允许 compact 任何存活的 client 还未确认的 sequence number 之后。
Lease Server 的可用性问题
在之前有提到,为了不让 Xline 依赖于另一套 Lease 系统,我们把 Lease Server 实现在了 Leader 上,既然 Leader 上的 tracker 面临着可用性的问题,那 Lease Server 自然也有可用性的问题了吗?
其实没有 :D,换一个角度来想,Lease Server 充当着所有 client 存活的见证者,如果 Lease Server 崩溃了,我们不认为 Lease Server 崩溃了,我们认为所有的 client 都崩溃了,从而迫使所有 client 放弃重试,对用户反馈出这种情况,并让用户自己决定是否重试。
事实上,这是一种摆烂的做法,在这种做法下,我们甚至不用费尽心思去解决 tracker 的恢复(新的 leader 上肯定是不接受任何重试 RPC 的)。
既然我们实现了 tracker 的恢复,其中恢复流程得到的 client id 则可以拿来恢复租约,来做到 Lease Server 的恢复。
RIFL 中尚未完成的部分
完成记录持久化
RIFL 要求完成记录需要持久化,并且可以随着系统迁移。这样在一个新的系统中,也能检查出重复并返回给用户之前的执行结果。
我们并没有实现完成记录的持久化和迁移,如果实现这个功能,则需要将 command 的执行结果存储到对应的 log entry 中,对 log 这种数据结构来说,反复 seek 并插入执行结果的操作是不友好的。
所以在 leadership change 之后,新的 leader 检查到了重复的命令,无法获取到之前的完成记录。
可以考虑后续对 log 数据结构进行优化,来实现完成记录的持久化和迁移。
回收机制优化
在看到之前的这张图时,可能敏捷的你会发现这种结构会有一个弊端:队头阻塞
实现 HTTP 协议老前辈们已经摸着石头过河解决了这类问题了:Multiplexing。
不过 RIFL 的作者似乎不想趟这滩浑水
“The garbage collection mechanism could be implemented using a more granular approach that allows information for newer sequence numbers to be deleted while retaining information for older sequence numbers that have not completed. However, we were concerned that this might create additional complexity that impacts the latency of RPCs; as a result, we have deferred such an approach until there is evidence that it is needed.”
事实上这也不是很大的问题,不像 HTTP pipelining,没有收到返回并不影响后续请求的发送(如上图),这可能会导致的结果是 server 上处理完的 command 的结果无法及时回收,占用过多的内存。RIFL 作者提到可以添加一个上限(论文中是 512)来解决这样的问题,不过考虑到这种问题不是无法解决的,后续应该需要进一步优化。
命令去重下的一些结构的优化
RIFL 不仅仅可以作为一个 RPC 的去重机制使用,也可以拿来优化一些 Xline 中的结构。
事实上,去重想法的契机便是发现了 Xline 目前一些组件的定时 GC 机制可能会导致正确性的问题,这些问题的触发条件会在 madsim 的测试环境下被放大,使我们不得不去重新引入一套新的方法来解决这些问题。
目前,Xline 后台的 GC Task 包括 Speculative Pool 的 GC 和 Command Board 的 GC,结合去重的机制,可以将这两种间隔 GC 的 task 更换成 client 确认后主动 GC,这样可以防止不保守的主动 GC 会导致一些正确性上的问题。
Speculative Pool GC 优化
在回收 Speculative Pool 中残留 command 的时候,可以将原来不保守的定时 GC 改成主动询问 Leader 回收,Leader 上可以通过 RIFL 机制一定程度确定一个 command 是否已经 commit。
如果 RIFL 机制判断 command 已经确认,则这个 command 一定 commmit 了,对于一个已经 commit 给 client,但 client 没有确认的 command,RIFL 并不能立刻判断出它已经 commit,则需要在等一段时间,client 确认了这些 command 的时候,Leader 再去检查 command 是否 commit 即可。如果在这段期间内 client 崩溃,Leader 也可以通过 Lease Manager 来判断出 client 已经掉线,则认为它所有 inflight 的 command 全部失效,我们可以安全地清理掉这个 client id 下所有的 command.
CommandBoard GC 优化
同样的,CommandBoard 也有一段 GC 定时任务,这个 GC Task 可能会在极端条件下导致回收了 client 没来得及收到的完成记录,有了 RIFL 机制后,我们可以把定时 GC 换成 client 确认后才进行 GC,这样可以保证 client 一定收到了完成记录。
Summary
在 Xline 的 CURP 系统中,本文深入探讨了 RIFL(Reusable Infrastructure for Linearizability)作为基础设施,为 RPC 提供 Exactly-Once 语义的实现及相关问题的解决方案。主要包括 Lease Server 的设计、Tracker 的构思、优化与可靠恢复,以及在命令去重场景下的后台 GC Task 结构优化。之后,去重机制会进一步优化完成记录持久化以及回收机制的优化,提供更完整的 Exactly-Once 语义的保证和更低的性能开销。
往期推荐
Xline v0.6.1: 一个用于元数据管理的分布式KV存储
Xline command 去重机制(一)—— RIFL 介绍
Karmada 管理有状态应用 Xline 的早期探索与实践
Xline于2023年6月加入CNCF 沙箱计划,是一个用于元数据管理的分布式KV存储。Xline项目以Rust语言写就。感谢每一位参与的社区伙伴对Xline的帮助和支持,也欢迎更多使用者和开发者参与体验和使用Xline。
GitHub链接:
https://github.com/xline-kv/Xline
Xline官网:www.xline.cloud
Xline Discord:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。