Table of Contents
- 摘要
背景介绍
- Xline
Jepsen
- Checkers
- Nemesis
Jepsen 测试设计
数据一致性
- Serializability
- Linearizability
- Strict Serializability
Jepsen etcd test
- Registers
- Sets
- Append
- WR
- Jepsen Xline test
测试结果分析
- 测试结果
- 异步落盘
Revision生成
- 背景
- 旧的实现分析
- 1-RTT生成revision为什么不可行?
- Xline中的修复
- References
01、摘要
在本篇文章中, 我们主要会介绍Jepsen在测试分布式KV存储Xline中的应用。 包括对混沌工程框架Jepsen的介绍, 对分布式系统一致性模型的讨论, 以及对有关测试结果的分析。
02、背景介绍
首先我们先来了解一下Xline以及Jepsen测试框架的大致背景。
a. Xline
Xline是一个分布式的KV存储, 现在是CNCF的sandbox项目。Xline和etcd类似, 提供了一个一致性的的kv存储, 以及其他例如watch, 分布式锁的功能。
Xline提供了etcd兼容的API, 但它和etcd最主要的区别是共识协议上的。Xline使用了CURP作为共识协议, 这使它能够在多数情况下都能够仅在1-RTT(Round Trip Time)内就达成共识, 而RAFT达成共识至少需要2-RTT。 因此Xline在高延迟环境下能够达到更好的性能。
b. Jepsen
Jepsen是一个用于分布式系统的验证的框架, 属于混沌工程的范畴。 它提供了一致性检查以及错误注入(fault injection)的功能。具体来说, Jepsen进行的是一项黑盒测试, 在测试中它会模拟真实部署环境, 在这个环境下生成一系列对于数据库的操作, 在测试结束后使用一致性检查器对于操作的结果进行检查, 检验结果满足这个数据库的一致性保证。
i. Checkers
Jepsen使用checkers来对执行结果进行一致性检查。Jepsen目前有两种checkers, 一个是Knossos, 另外一个是Elle。Knossos用来检查结果是否是线性一致的(Linearizable), 而Elle是用来检查数据库事务的一致性的(Transactional Consistency)。注意这些checker并不能保证检测出所有不一致的情况, 因为判定结果是否是线性一致, 以及事务一致性中的可串行化检查(Serializability Checking)都是NP完全问题[1][2]。Jepsen的checkers会限定计算的规模以在较短时间内完成测试。
ii. Nemesis
Jepsen中fault injection的组件叫做nemesis。Jepsen有一些内置的nemesis:
- kill, 可以kill掉某些节点上数据库的进程
- pause, 可以pause某些节点上数据库的进程
- partition, 制造网络分区, 可以是任意两个节点之间, 例如我们可以分区多数/少数节点
- clock, 可以打乱某些节点上的时钟
这些组件可以模拟在分布式系统部署环境中常见的软硬件错误。 同时, Jepsen作为一个非常灵活的框架, 它支持用户自定义自己的nemesis, 例如, 对于etcd来说, 我们可以让集群成员变更也成为一个nemesis, 达到在测试中定时地增加/删除节点的目的。因此nemesis不仅仅可以用作fault injection, 它也可以用来触发系统中可能出现的一些事件。
03、Jepsen 测试设计
接下来我会对etcd Jepsen测试设计进行详细的分析, 以及介绍我们是如何在Xline上应用Jepsen的测试。
a. 数据一致性
首先我先简单介绍一下三种一致性模型作为下面测试分析的背景, 它们分别是Serializability, Linearizability 以及 Strict Serializability。 这些一致性模型在Jepsen官方网站中有更为详细的介绍[3]。
i. Serializability
Serializability 是一个事务模型, 它是多个对象而言的(例如在etcd中就是多个keys), 并且每一个事务的操作都是原子的。同时它具有几个特性:
- 内部一致性(Internal Consistency)即一个事务的一个读操作可以观测到前面所有写操作的结果
- 外部一致性(External Consistency)即一个事务T1中的读观测到另外一个事务T0中的写, 我们称之为T0对于T1可见(Visible), 这样就构成一个可见关系
- 全序可见性(Total visibility)即所有的可见关系构成一个全序关系, 这其实说明了有一些事务之间是没有这个关系的, 对于所有事务加上这个可见关系构成了一个偏序集
ii. Linearizability
Linearizability是对单个对象而言的, 并且每个操作也是atomic的。 它的所有操作形成一个实时的顺序(Real-Time Ordering)。这里的实时顺序就是指的是这些操作结果的顺序是反映出操作完成时的具体时间的。
iii. Strict Serializability
Strict Serializability则是最强的一致性保证。它可以是看作是 Serializability 和 Linearizability的结合, 即在多个对象上的Linearizablity。那么其实也就是说我们这时候不仅仅是保证全序可见性了, 而是所有的事务都排成了一个实时的顺序。
b. Jepsen etcd test
首先我来简单介绍一下Jepsen官方对于etcd的测试设计。etcd使用的是严格可序列化模型(Strict Serializability Model). Jepsen为etcd编写了以下测试:
i. Registers
Register是Knossos为了检查线性一致性内建的模型, 因为线性一致性是对单个对象而言的, 所以它将单个对象具体化成为一个register, 这个register支持read/write/compare-and-set这三种操作, Knossos会检验对regsiter所有操作的结果是否是线性一致的。
ii. Sets
Sets测试用来检测stale reads。etcd支持设置允许stale reads来达到更好的读性能, 但默认情况下是不允许的。Sets测试只有一个操作compare-and-set, 它对单一的key进行多次操作, 最后检查结果是否是可串行化的, 即检查每个CAS操作都是原子地发生的。
iii. Append
Append测试用来检查严格可序列化, 它有两种操作read/append。Append的意思就是把存储中key的value作为一个list。其中append操作是向这个list中append一个元素。实现append的方式就是先从etcd中读出一个key的值, 然后在一个事务中检查值是否有改变, 如果没有改变就写入append过后的新值。
在这个测试中, 不仅所有的事务都要按照原子地方式进行, 而且Jepsen会同时检查这些事务是否是按照真实时间顺序发生的。
iv. WR
WR测试使用事务向多个key进行read/write, 它同样是检查严格可序列化。
c. Jepsen Xline test
Jepsen测试框架主要分为四个部分: DB, Client, Checkers和Nemesis 每一个部分都是一个单独的接口可以供用户进行实现。由于Xline实现了etcd兼容的API, 所以我们在编写测试的时候直接复用了Jepsen对于etcd的测试。在此测试的基础上, 我们实现了Jepsen中Xline的DB的接口。同时Xline也有自己使用CURP共识的client SDK, 我们同样也对此也实现了Jepsen中client的接口。所以测试实际上是分为对etcd client的兼容测试, 以及Xline native的client的测试(目前还未完成此项测试)。
04、测试结果分析
接下来我会详细讲解Xline Jepsen测试结果以及我们找到的问题。
a. 测试结果
在最初的测试中, 我们遇到的问题可以说是比较多的, 其中问题最多的是和Xline中对于事务操作的部分, 其中有一些问题是很细微的bug, 另外一些则是设计上存在的漏洞。这些问题都花费了大量时间进行调试和识别。
其中我会详细解释两类主要的问题, 分别是异步落盘的问题和revision生成中的问题。
b. 异步落盘
etcd落盘都是同步的, 及当一个节点拿到log的时候, 它会同步地把log持久化到储存设备上, 然后再去执行log中的命令, 在执行完成后再把执行结果同步的持久化到储存设备, 这样做的好处就是集群可以容忍超过多数节点同时关闭/断电而不影响正确性。
有别于etcd的假设, Xline的假设是集群始终存在大多数节点, 而不考虑所有节点都失效的情况, 这也和一些基于内存的Raft实现类似。这给了我们一些优化空间, Xline初始的设计是所有落盘操作都是异步的。但是这么做会有几个问题:
- 执行的顺序更难推断 由于最后落盘执行结果是异步进行的, 所以我们并不能准确知道logs的执行顺序。在Xline的Read State实现中(在follower本地读, 用于减少leader负载), 一个follower受到一个只读操作后, 它会向leader要一些信息: 步骤一: 如果当前还有未执行完成的commands, 那么拿到他们的ids 步骤二: 如果所有command已经执行完了, 那么拿到当前已经执行完成后的log index 接下来follower会在本地等待这些信息代表的系统状态达成后再进行这个读操作。
这时, 我们之前所述的不确定的执行顺序就引入了不少的复杂性, 导致最开始的实现存在bug:
- 没有等待这个index之前的所有index代表的log都执行完就去执行了读操作。
- 实际上步骤一中仅仅拿到command id还不够, 因为log index可能比command id代表的系统状态还要新, 因为我们log的执行是异步的, 没法推断出他们的顺序!
- 会出现log存储和KV存储不一致的情况 考虑这样的情况: 因为log持久化和KV持久化是异步进行的, 那么在某个节点上KV持久化可能比log持久化先完成, 那么这时候如果节点因为某种原因重启了, 它读出的是没有持久化之前的log, 这会导致在恢复过程中, 同一条log被执行了两次, 而并不是所有的操作都是幂等的, 例如在同一条带有条件的事务(Predicate-Based Trasaction)执行两次是不幂等的. 这也说明了KV持久化必须是在log持久化之后才能进行的。
从以上问题可以得出结论, 异步持久化会带来很多额外的状态需要考虑, 使得系统状态难以推断和分析, 并且潜在的对性能带来一定影响。我们现在正在考虑使用同步的方案来代替异步落盘来简化系统实现, 尽管这可能会牺牲一部分性能, 但是性能始终应该是在正确性之后才需考虑的。
c. Revision生成
在最开始的设计中, 我们希望Xline能够在兼容etcd的同时, 也能够保持1-RTT达到共识的性能。etcd中有revision这一概念, 表示当前的系统修改的次数, 对于每个kv请求, 都需要返回一个revision。因此我们最开始是(错误的)实现了1-RTT生成revision的方法。而最终事实证明, 在1-RTT中生成revision是不可行的。这一实现也导致Jepsen中涉及revision的测试(具体来说是append)无法通过。
下面我来介绍我们旧的实现是怎么样的, 以及这个实现为什么是错误的, 最后讨论1-RTT生成revision为什么不可行。
i. 背景
首先我们需要有一些CURP共识[4]的背景。CURP实现1-RTT的原理是它在每个节点上有一个witness, 在command执行前, client需要把command记录到大多数witness上, 接下来直接在leader执行完不需要复制到follower上即可返回给client, 因为command的信息以及记录在了witness上面, 及时集群少数节点崩溃也能够从witness上恢复出原来的command。而witness上记录的command有两个特点:
- command之间都是commute的
- command之间没有顺序
这其实就是和Generalized Paxos[5]的实现非常接近, 都是利用commands之间的commutativity来优化请求的时延。而commutativity这一特性也就决定了上述的witness中command的两个特点。因为commutativity的意思就是两个commands之间修改的key不能有交集, 不commute的commands必然是串行执行的。而commute的commands之间是可以并发执行的, 这就表示它们之间没有顺序。
ii. 旧的实现分析
旧的实现中, 每个节点都在本地给每个命令分配revision, 这时候就要保证对于所有commands, revision分配的顺序必须相同。在每个节点的执行阶段前, 我们就会按照log index的顺序给每个节点分配revision。这乍一看可能是正确的, 既然按照log index的顺序分配, 那么我们也不就在各个节点上分配revision的顺序也相同了吗? 但是, 我们忽略了一件事情, 在witness上可能还记录了commands的一些信息! 如果某个节点crash了从witness上恢复的时候, 它并不知道恢复的commands之间的顺序, 这样就会导致revision分配在不同节点上出现不同的顺序。
iii. 1-RTT生成revision为什么不可行?
Revision的想法对于Raft是很自然的, 就是将整个分布式系统抽象为一个状态机, revision的数值就表示当前状态机所在的状态。这表明对于系统的所有的修改操作, 它们是有一个全局的顺序的。
对于CURP共识协议来说, 由于记录在witness上的commands是无序的(由于client的请求是并发的), 想要进行排序那么就必须额外地引入另外一个RTT来在各个witness上进行同步. 这样记录和排序的两步过程至少需要两个RTT。
和Generalized Paxos相同, CURP使用的commutativity就使得commands之间可能是没有顺序的, 那么所有commands形成的是一个偏序关系, 称为command history[5]。在这种情况下, revision的想法就不太适用了。
Generalized Consensus and Paxos论文中, 作者其实也证明了在Generalized Consensus下想要确定任意两个命令之间的顺序是需要至少两个message delays的(Learning is impossible in fewer than 2 message delays), 下面是作者的证明[5]:_Suppose l learns a value proposed by p in one message delay. Then it is possible that every message sent by p was lost except for ones received by l. If all messages from p and l to other processes are lost, then there is nothing to prevent another learner from learning a different value proposed by another proposer, violating consistency._
iv.Xline中的修复
- etcd兼容性
- 经过以上讨论, Xline其实是没办法在兼容etcd revision的情况下依然保持1-RTT的高性能的. 所以在etcd的兼容层中, 我们需要等待每一个command都commit之后再给它分配revison, 这样和etcd相同需要两个RTT。
- Append测试
- Append测试中的事务测试使用了Revision来确保事务的原子性。Append测试要求的是严格可系列化。在之前的讨论中, CURP共识对于所有commands来说并没有一个全局的顺序, 所以这其实是违反了严格可序列化的要求的, CURP的偏序关系事实上是只保证了可序列化。因此, 在Xline的测试中(非etcd兼容), 我们不能直接使用Append测试。或者说编写Xline的Jepsen测试应当将一致性模型更改为更弱的可序列化模型。
05、References
[1]P. B. Gibbons and E. Korach, “Testing shared memories,”_Siam journal on computing_, vol. 26, no. 4, pp. 1208–1244, 1997.[2]C. H. Papadimitriou, “The serializability of concurrent database updates,”_Journal of theacm(jacm)_, vol. 26, no. 4, pp. 631–653, 1979.[3]“Jepsen consistency.” Available:https://jepsen.io/consistency[4]S. J. Park and J. Ousterhout, “Exploiting commutativity for practical fast replication,” in_16th usenix symposium on networked systems design and implementation (nsdi 19)_, Boston, MA: USENIX Association, Feb. 2019, pp. 47–64. Available:https://www.usenix.org/conference/nsdi19/presentation/park[5]L. Lamport, “Generalized consensus and paxos,” 2005.
06、往期回顾
Xline v0.6.1: 一个用于元数据管理的分布式KV存储
Xline command 去重机制(一)—— RIFL 介绍
Xline于2023年6月加入CNCF 沙箱计划,是一个用于元数据管理的分布式KV存储。Xline项目以Rust语言写就。感谢每一位参与的社区伙伴对Xline的帮助和支持,也欢迎更多使用者和开发者参与体验和使用Xline。
GitHub链接:
https://github.com/xline-kv/Xline
Xline官网:www.xline.cloud
Xline Discord:
如果您有兴趣加入达坦科技Rust前沿技术交流群,请添加小助手微信:DatenLord_Tech
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。