什么是 Xline
Xline是一个开源的分布式KV存储引擎,其核心目的是实现跨数据中心的高性能强一致性,提供跨数据中心的元数据管理。那么Xline是如何实现这种跨数据中心的高性能强一致性呢?本文将带领您一探究竟。
Xline整体架构
我们先看一下Xline的整体架构,如下图:

从上到下,Xline大致可以分为三层,即
接入层:使用gRPC框架实现,负责接收客户端的请求。
中间层:可分为CURP共识模块(左)和业务服务器模块(右),其中:
CURP共识模块:实现CURP共识算法,代码对应Xline中的curp crate,而对应的RPC服务定义在curp/proto中。
业务服务器模块:负责实现Xline的上层业务逻辑,比如KV相关请求的KvServer、认证请求的AuthServer等。代码对应xline crate,而对应的RPC服务定义在xlineapi 箱。
存储层:负责Xline内部数据和元数据的持久化,向上层提供抽象接口,代码对应引擎crate。
CURP协议简介
什么是 CURP?
Xline中使用的共识协议不是Paxos或Raft,而是一种名为CURP的新共识协议,它被称为“一致无序复制协议(CURP)”。它起源于 NSDI 2019 的论文《Exploiting Commutativity For Practical Fast Replication》,该论文的作者是斯坦福大学博士生 Seo Jin Park 和 Raft 算法的作者 John Ousterhout 教授。
为什么选择 CURP 协议?
为什么 Xline 使用 CURP 这样的新协议而不是 Raft 或 Multi-Paxos 作为底层共识协议?为了说明这一点,我们来看一下 Raft 和 Multi-Paxos 的问题。
下图展示了Raft的共识流程:

在这个时序图中,我们可以看到Raft协议是如何达成共识的:
客户向领导提出建议请求。
领导者接收来自客户端的提议请求,将其附加到其状态机日志中,并将 AppendEntries 请求广播给集群中的其他追随者。
follower收到leader的AppendEntries请求后,会进行日志一致性检查,以确定是否可以添加到自己的状态机日志中。如果检查成功,则返回成功消息;如果检查失败,则返回失败消息。
Leader 统计收到的成功响应的数量,如果超过集群节点数的一半,则认为达成共识,提案成功,否则认为提案失败,并将结果返回给客户端。
下图展示了Multi-Paxos协议达成共识的流程:

在这个时序图中,我们可以了解Multi-Paxos协议达成共识的流程:
客户向领导提出建议请求。
领导者在其状态机日志中找到第一个未批准的日志条目的索引,然后执行Basic Paxos算法以客户端请求的建议值提议索引处的日志。
follower接收leader发送的proposal值,并决定是接受proposal值并返回成功的响应,还是返回失败的响应。
Leader统计收到的成功响应的数量,如果超过集群节点数的一半,则认为已达成共识,提案成功,否则认为提案失败,返回结果给客户。
无论是Multi-Paxos还是Raft,达成共识都不可避免地需要2个RTT。两者都基于一个核心假设:在命令批准或日志提交后必须满足持久存储和排序的标准。因此,状态机可以直接执行批准的命令或应用提交的日志。由于网络固有的异步性,确保有序性具有挑战性。因此,领导者需要强制执行不同命令的执行顺序,并通过广播获得多数人的复制来实现持久化。此过程无法在单个 RTT 内完成。
这就是为什么Xline没有选择Raft或者Multi-Paxos作为底层共识算法。Xline 主要设计用于管理跨数据中心的元数据。众所周知,对于单个数据中心来说,其内网的延迟往往很低,只有几毫秒甚至不到1毫秒,而对于跨数据中心的广域网来说,网络延迟可以达到几十毫秒或几十毫秒。甚至数百毫秒。传统共识算法,如 Raft 或 Multi-Paxos,无论共识状态如何,都需要 2 个 RTT 才能达成共识,这在此类高延迟网络环境中往往会导致严重的性能瓶颈。这让我们想知道是否需要两次或多次 RTT 才能在任何情况下达成共识。
CURP算法是一种无序复制算法,将共识场景分为以下两类:
快速路径:在不冲突的场景下,在持久化存储的前提下,放宽共识的排序要求,不影响最终的共识。由于快速路径只需要存储持久性,因此只需要1个RTT即可达成共识。我们将快速路径称为协议的前端。
慢路径:冲突场景下,需要同时满足有序并发请求和持久化存储的需求,需要2个RTT才能达成共识。我们将慢速路径称为协议的后端。
那么读者可能会想,这里到底有什么冲突呢?我们以一个简单的KV操作为例。在分布式系统的节点中,我们对状态机进行的操作只是读写,而对状态机进行并发操作的情况下,有四种场景:read-after-read、read-after-write 、先读后写和先写后写。显然,对于read-after-read这种只读操作,没有任何副作用,在任何情况下都不会发生冲突,而且无论是先读还是后读,最终的结果总是相同的。当对不同的按键进行操作时,例如PUT A=1,PUT B=2,那么对于状态机的最终状态,无论是先执行PUT A=1,再执行PUT B=2,还是反之亦然,从状态机读取的最终结果是A=1,B=2。对于读写混合的场景也是如此。因此,当一个状态机上同时执行的多个操作的键之间不存在交集时,我们说这些操作是不冲突的。相反,如果并发操作包括至少一个写操作,并且这些操作的键相交,则这些操作是冲突的。
快速路径与慢速路径
CURP如何实现快路径和慢路径?下面是 CURP 算法中集群拓扑的草图。
让我们看看这张图中发生了什么:

Client:向集群发出请求的客户端。
Master:对应集群中的Leader节点,保存着状态机日志,其中绿色部分代表已经持久化到磁盘的日志,蓝色部分代表存储在内存中的日志。
Follower节点:对应上图中黄色虚线框,每个follower包含以下两个组件。
A. Witness:可以近似为一个基于内存的HashMap,一方面负责在快速路径过程中记录集群中当前的请求,另一方面CURP也会利用Witness来判断是否存在当前请求中存在冲突。Witness 中保存的所有记录都是无序的。
B. 备份:将状态机日志保存到磁盘。
接下来我们以图中PUT z=7为例,看看快速路径的执行流程:
客户端向集群中的所有节点广播 PUT z=7 的请求。
当集群中的节点收到请求时,它会根据自己的角色执行不同的逻辑。
A.Leader收到请求,立即将数据z=7写入本地(即状态机日志中的蓝色部分),并立即返回OK。
B. 当follower收到请求时,会使用witness来判断该请求是否存在冲突。由于z = 7与见证人中唯一的y = 5不冲突,因此follower将z = 7保存到见证人中,并向客户端返回OK。
客户端收集并计算收到的成功响应的数量。对于2f+1个节点的集群,当收到的成功响应数量达到f+f/2+1时,该操作被确认持久化到集群中,整个过程需要1个RTT。
接下来,在前面的快速路径示例的基础上,我们以 PUT z = 9 为例来看看慢速路径的执行流程。由于z=9与z=7冲突,客户端发起的快路径将会失败,而执行慢路径:
客户端向集群中的所有节点广播 PUT z=9 请求。
集群中的节点接收请求并根据各自的角色执行不同的逻辑。
领导者收到请求并将 z = 9 写入状态机日志。由于z = 9与z = 7冲突,因此向客户端返回KeyConflict响应,并异步发起AppendEntries请求,将状态机日志同步到集群中的其他节点。
follower 收到请求并拒绝保存提案,因为 z = 9 与见证人中的 z = 7 冲突。
客户端收集并计算收到的成功响应的数量。由于收到的拒绝响应数量超过 f/2,客户端需要等待慢速路径完成。
当步骤2中的AppendEntries执行成功后,follower将leader的所有三个状态机日志(y = 5, z = 7, z = 9)追加到Backup中,并从witness中删除相关日志,并返回成功给领导的答复。
领导者计算收到的成功响应的数量。如果超过集群节点数的一半,则认为达成共识,提案成功。否则,提案失败,结果返回给客户端。
概括
Xline是一种分布式KV存储,可提供跨数据中心的强一致性。其核心问题之一是如何在跨数据中心的高延迟广域网环境中提供高性能强一致性。传统的分布式共识算法,例如Raft和Multi-Paxos,通过使所有操作满足存储持久性和排序前提来保证状态机一致性。CURP协议,对共识场景进行了更细粒度的划分,将协议分为前端(快路径)和后端(慢路径),其中前端仅保证提案会持久化到集群,而后端不仅保证持久化,还保证所有保存了提案的节点都会按照相同的顺序执行命令,CURP协议的介绍到此结束。更多详情请参考 GitHub 链接:
https://github.com/xline-kv/Xline
推荐
随手关注或者”在看“,诚挚感谢!