摘要: 本系列文章主要面向 TiKV 社区开发者,重点介绍 TiKV 的系统架构,源码结构,流程解析。目的是使得开发者阅读以后,能对 TiKV 项目有一个初步了解,更好的参与进入 TiKV 的开发中。本文是本系列文章的第六章节。重点介绍 TiKV 中 Raft 的优化。(做者:唐刘)git
在分布式领域,为了保证数据的一致性,一般都会使用 Paxos 或者 Raft 来实现。但 Paxos 以其复杂难懂著称,相反 Raft 则是很是简单易懂,因此如今不少新兴的数据库都采用 Raft 做为其底层一致性算法,包括咱们的 TiKV。github
固然,Raft 虽然简单,但若是单纯的按照 Paper 的方式去实现,性能是不够的。因此还须要作不少的优化措施。本文假定用户已经熟悉并了解过 Raft 算法,因此对 Raft 不会作过多说明。算法
这里首先介绍一下一次简单的 Raft 流程:数据库
能够看到,上面的流程是一个典型的顺序操做,若是真的按照这样的方式来写,那性能是彻底不行的。缓存
首先能够作的就是 batch,你们知道,在不少状况下面,使用 batch 能明显提高性能,譬如对于 RocksDB 的写入来讲,咱们一般不会每次写入一个值,而是会用一个 WriteBatch 缓存一批修改,而后在整个写入。 对于 Raft 来讲,Leader 能够一次收集多个 requests,而后一批发送给 Follower。固然,咱们也须要有一个最大发送 size 来限制每次最多能够发送多少数据。性能优化
若是只是用 batch,Leader 仍是须要等待 Follower 返回才能继续后面的流程,咱们这里还可使用 Pipeline 来进行加速。你们知道,Leader 会维护一个 NextIndex 的变量来表示下一个给 Follower 发送的 log 位置,一般状况下面,只要 Leader 跟 Follower 创建起了链接,咱们都会认为网络是稳定互通的。因此当 Leader 给 Follower 发送了一批 log 以后,它能够直接更新 NextIndex,而且马上发送后面的 log,不须要等待 Follower 的返回。若是网络出现了错误,或者 Follower 返回一些错误,Leader 就须要从新调整 NextIndex,而后从新发送 log 了。微信
对于上面提到的一次 request 简易 Raft 流程来讲,咱们能够将 2 和 3 并行处理,也就是 Leader 能够先并行的将 log 发送给 Followers,而后再将 log append。为何能够这么作,主要是由于在 Raft 里面,若是一个 log 被大多数的节点append,咱们就能够认为这个 log 是被 committed 了,因此即便 Leader 再给 Follower 发送 log 以后,本身 append log 失败 panic 了,只要 N / 2 + 1
个 Follower 能接收到这个 log 并成功 append,咱们仍然能够认为这个 log 是被 committed 了,被 committed 的 log 后续就必定能被成功 apply。网络
那为何咱们要这么作呢?主要是由于 append log 会涉及到落盘,有开销,因此咱们彻底能够在 Leader 落盘的同时让 Follower 也尽快的收到 log 并 append。架构
这里咱们还须要注意,虽然 Leader 能在 append log 以前给 Follower 发 log,可是 Follower 却不能在 append log 以前告诉 Leader 已经成功 append 这个 log。若是 Follower 提早告诉 Leader 说已经成功 append,但实际后面 append log 的时候失败了,Leader 仍然会认为这个 log 是被 committed 了,这样系统就有丢失数据的风险了。并发
上面提到,当一个 log 被大部分节点 append 以后,咱们就能够认为这个 log 被 committed 了,被 committed 的 log 在何时被 apply 都不会再影响数据的一致性。因此当一个 log 被 committed 以后,咱们能够用另外一个线程去异步的 apply 这个 log。
因此整个 Raft 流程就能够变成:
使用 asychronous apply 的好处在于咱们如今能够彻底的并行处理 append log 和 apply log,虽然对于一个 client 来讲,它的一次 request 仍然要走完完整的 Raft 流程,但对于多个 clients 来讲,总体的并发和吞吐量是上去了。
在 Raft 里面,若是 Follower 落后 Leader 太多,Leader 就可能会给 Follower 直接发送 snapshot。在 TiKV,PD 也有时候会直接将一个 Raft Group 里面的一些副本调度到其余机器上面。上面这些都会涉及到 Snapshot 的处理。
在如今的实现中,一个 Snapshot 流程是这样的:
若是一个节点上面同时有多个 Raft Group 的 Follower 在处理 snapshot file,RocksDB 的写入压力会很是的大,而后极易引发 RocksDB 由于 compaction 处理不过来致使的总体写入 slow 或者 stall。
幸运的是,RocksDB 提供了 SST 机制,咱们能够直接生成一个 SST 的 snapshot file,而后 Follower 经过 injest 接口直接将 SST file load 进入 RocksDB。
在以前的 Lease Read 文章中,我提到过 TiKV 使用 ReadIndex 和 Lease Read 优化了 Raft Read 操做,但这两个操做如今仍然是在 Raft 本身线程里面处理的,也就是跟 Raft 的 append log 流程在一个线程。不管 append log 写入 RocksDB 有多么的快,这个流程仍然会 delay Lease Read 操做。
因此现阶段咱们正在作的一个比较大的优化就是在另外一个线程异步实现 Lease Read。也就是咱们会将 Leader Lease 的判断移到另外一个线程异步进行,Raft 这边的线程会按期的经过消息去更新 Lease,这样咱们就能保证 Raft 的 write 流程不会影响到 read。
虽然外面有声音说 Raft 性能很差,但既然咱们选择了 Raft,因此就须要对它持续的进行优化。并且现阶段看起来,成果仍是很不错的。相比于 RC1,最近发布的 RC2 不管在读写性能上面,性能都有了极大的提高。但咱们知道,后面还有不少困难和挑战在等着咱们,同时咱们也急需在性能优化上面有经验的大牛过来帮助咱们一块儿改进。若是你对咱们作的东西感兴趣,想让 Raft 快的飞起,欢迎联系咱们,邮箱:info@pingcap.com,做者我的微信:siddontang