1、重构概述
在Xline 0.7.0中,我们完成了对Xline代码库中进行了一次较大的重构。这次重构在某些性能测试中甚至使得Xline获得了近20倍的性能提升。在本文中我会讲解Xline中重构后命令执行流程的新设计,以及我们是如何优化Xline的性能的。
2、etcd的性能分析
由于Xline的实现和etcd类似,在etcd中的性能瓶颈在Xline中同样存在。因此在开始对Xline进行分析之前,让我们首先分析一下etcd的性能。
etcd命令执行流程
我们首先需要梳理一下etcd命令的执行流程,这有助于我们后面进行的性能分析。etcd使用的是Raft共识算法,命令的执行流程很简单,简要叙述如下:
- 节点接收client发送的命令
- 节点将命令写入自身的log中
- 节点将这条log复制到多数follower节点上
- 节点在自身状态机上执行这条命令,持久化到后端存储
- 节点返回执行结果到client
主要性能开销
影响一个etcd节点性能的因素有很多,要分析性能首要的问题是分析关键路径上的各类操作,这包括CPU时间和各类IO操作。接下来我会对这些操作进行逐条分析。由于在etcd集群中leader的压力是最大的,以下的性能分析中的节点都是指leader节点。
gRPC请求
在etcd中主要存在两种gRPC通信,一种是节点处理client发送的命令,另外一种是节点向其他follower节点复制log。对于etcd实现来说,第一种显然是在关键路径上的,而由于etcd需要提前复制到大多数节点上才会返回结果给client, 所以第二种同样位于关键路径上。
在go gRPC的性能测试中单核CPU通常能够每秒处理数十k的请求, 而在etcd在设计上也通常需要处理每秒数十k的请求,这说明对于etcd gRCP server的压力是非常大的。因此如果在有限的环境下,gRPC可能会导致性能瓶颈。
存储IO
对于存储设备上的IO主要有两种:
- 对于每个命令,我们需要持久化到WAL中
- 执行命令时,我们需要持久化到后端存储中
我们需要这两种操作都执行完成后才会返回给client,因此它们都是位于关键路径上
由于Raft安全性的要求,持久化到WAL是需要同步地落盘才能进行后续操作,因此性能瓶颈主要落在了fsync的性能上,因为即使在SSD上一次fsync也需要数百微妙。
而持久化到后端存储则没有很高的安全性要求,仅需要保持原子性即可,不需要每次调用都使用fsync,因此这些操作大部分都可以在内存中完成,一般情况下都是non-blocking的,不会造成明显性能瓶颈。
为什么etcd难以在多核cpu上扩展性能
etcd保证了strict serializable,所有操作必须按照一个全局的顺序来完成,这样就造成了我们的命令处理逻辑无法并发地完成。例如在处理Raft log时,我们首先需要拿到一把全局的锁,然后才能进行后续操作,同样对于后端的命令执行也需要按顺序逐个进行,无法并行执行。因此,etcd的吞吐量极大地受限于单线程的性能。
3、Xline重构概述
下面我会从整体角度介绍Xline本次重构中对性能有较大影响的部分。这其中主要涉及到Xline对于command的执行机制的修改。
Xline与etcd相似之处
Xline使用的是CURP共识算法,它和Raft最主要的区别就是分为前端commit和后端commit,后端commit和Raft相同,都需要leader复制log到大多数节点上。而前端commit是通过witness这个机制来实现的,witness的机制是client直接记录到witness上来完成快速commit。因此,要分析Xline的性能,我们需要从前端的witness性能和后端Raft的性能两方面进行分析。
CURP的冲突检测性能
在CURP的中,为了保证witness上的commands的commutativity, 我们需要对所有commands进行冲突检测。
一个直接的想法就是把所有commands放在一个列表里,然后当检查一个command是否冲突时就遍历这个列表进行检查。这就是Xline中旧实现的思路,这样导致关键路径上每次冲突检查的复杂度为 O(n) ,效率很低。同时,这个列表外层还需要加一把锁,造成严重的锁护送(lock convoy)的现象,这一现象我会稍后详细讲解。
在重写后的冲突检测机制中,我们使用了区间树来优化KV命令冲突检测的复杂度,使得冲突检测的时间复杂度降至了`O(log(n))`,使得冲突检测效率大大提升,即使是在关键路径上对性能影响也会比较小。区间树实现可以参见往期文章&