在正文的第一句加入“我正在参加「码上掘金挑战赛」详情请看:码上掘金挑战赛来了!”
为KV系统优化的raft协议-系统延迟两三事(二)
本文讨论在生产环境使raft一致性协议时会遇到的一些延迟问题&优化方案,后来,我们也在论文- Rethink the Linearizability Constraints of Raft for Distributed Key-Value Stores看到相同的proposal和测试以及总结。
背景&问题
在典型的分布式KV系统架构中,raft/paxos算法是目前业界使用较多的共识协议,作为分布式KV存储中最重要和最复杂的模块之一,分布式共识协议并不容易通过硬件升级来加速。即使在各种系统故障情况下,也有许多严格的规则和许多数据处理步骤(如RPC、日志、数据持久性等)来确保分布式系统的共识和一致性。

raft 协议导致的写延迟&长尾
在KV系统中,raft协议引入了过多不必要的约束。raft协议引入了logIdex、commitIndex和apply Index。
raft 写流程如下:
- append log
client写leader时,将被分配一个唯一、单调递增的index。接下来leader将wal落盘,并将日志发给follower(这里在
raft phd thesis论文中有一个优化)。 - commit follower落盘完毕后,响应leader,当leader发现超过一半的副本(可能不包括leader本身)已经成功地追加了索引I的请求,将index为i的log设置为commit状态,commitIndex=i
- apply 当日志被commit后,leader开始将committed日志应用到状态机,只有当leader完成日志应用后,才可以将数据返回客户端。根据raft论文,append、commit、apply都应该按照到达顺序串行执行,而这,就引出了本文的优化点。
commit return
下图描述了典型的raft写流程,来自client的写请求首先来到raft共识模块(step 1)。CC开始对leader的落盘本地日志进行追加,并将log发给所有follower(step2)。在log提交后,leader需要将写入的数据应用到状态机中(例如,一个KV存储(step3)。最后,leader响应client,完成写入请求(即step4)。
上图中,若将step 3改为异步apply,在此提前响应客户端,这就是本节说的commit return机制(后文简称CR)。不禁要问,他能优化多少性能?
raft-kv apply时间占比
上图展示了当value大小在1KB到1MB之间时,Apply阶段和其他所有阶段的消耗时间百分比。明显当value的大小小于16KB时,Apply实际上是很慢的,几乎占了所有写请求处理时间的40%,这对大多数应用来说是很常见的。随着value的变大,实际网络时间也变大,可以看到Apply时间占比变小。
对CR的讨论
CR实际简化了raft对写的约束,它可以显著提高KV存储的写入操作。我们在导数据场景实现了类似机制,此时raft阶段1被用作MQ一般暂存数据,被状态机缓慢消化,写入性能非常稳定,由于我们状态机使用的是rocksdb,即使遇到write stall,CR也不会导致大规模的抖动。
我未想到这个proposal可以发表论文,本文的写作技法、叙事风格、数据对比是值得学习的。 CR虽然提高了写入性能,但是有诸多其他限制,而后,作者做了一些补丁让CR机制保持线性一致性读,让论文看起来更完整了。
对GET的补丁
引出一个新的概念,read index。
read index概念非常简单,实践中read index等于业务请求到达瞬间状态机的commit index,但它却非常有用,例如,为了实现raft follower 的线性一致性读,在follower中等待apply index到达read index即刻返回即可(tikv/etcd实现了这个机制)。
对于GET操作,我们可以从后向前读取applyIndex和readIndex之间的Raft日志来搜索目标key,因为后面的日志包含较新的版本。 如果在这些日志中找到了目标key,它的相关值将立即直接返回给客户端,而无需读取状态机;如果没有找到,我们将从状态机中读取目标key。
由于Raft的日志都在内存中,只有应用索引和读取索引之间的日志需要读取,因此与等待这些日志被应用的时间相比,读取这些日志的时间非常小。
对SCAN的补丁
对于SCAN操作,我们也不需要等待应用索引赶上读取索引。然而,与GET不同的是,对于SCAN,我们应该在apply index和read index之间同时执行读取内存中的Raft日志,从前面到后面,以及状态机。原因在于,我们无法确保内存中的日志包含范围查询的所有目标数据。请注意,从Raft日志中的SCAN操作的结果是有顺序的。因此在得到内存中的日志和状态机的结果后,我们可以对这两部分结果进行合并排序,对于同一个键,来自日志的数据具有更高的优先级。
点评
CR机制只能对写不需要返回值的KV存储操作,也即是SET接口有优化左右,但凡我们需要实现类似CAS、INCR、Update操作,CR皆无法正确工作。但本文仍给我们在优化写延迟的思考上提供了一些参考。