工作中使用了raft用来做数据高可用,raft框架是使用百度开源的braft。
1. 更有效的snapshot
工作中使用了braft,数据状态机使用了rocksdb,是很常见的一种搭配形式。
braft是定时会做一次snapshot,对于braft而言,snapshot就是将数据状态机中的全量数据backup一份到braft文件夹中管理起来,在follower需要时发送给远端,或者重启时使用。对于rocksdb而言将数据backup一份到其他文件夹中也非常方便,rocksdb会使用硬链接将当前的所有sst文件链过去,没啥开销。
但是有这样一个问题,我们在braft中使用rocksdb时,将rocksdb的 WAL日志关闭了,因为braft的raft log就已经可以相当于WAL了,没必要再写一次rocksdb的WAL,减少写盘。这就导致了一个问题,每次braft做snapshot时,就必须要将rocksdb memtable中的所有数据flush到磁盘,然后再link sst文件到braft文件夹中。在实践中就发现这会引发一些问题:
- memtable未被写满就flush到磁盘,会生成一些sst小问题。由于我们某些rocksdb的compaction参数设置得相当激进,一个小的sst刷到level0可能会触发非常多轮的compaction,占用磁盘IO,写放大非常大,影响了用于正常读写
- memtable的flush本身也会switch 新的memtable,这个瞬间对的写延迟可能会增高。而且braft是一个multi raft,一个进程中可能有成百上千个raft node,braft是定时执行snapshot,即一个进程中的所有raft node可能在相近的时间进行snapshot,进而所对应的多个rocksdb实例都会flush 数据,占用磁盘带宽,影响用户正常读写。
我们尝试优化这个问题。其实在我们的使用中,raft log就被当作了数据库的redo log。对于数据库的redo log,数据库执行checkpoint之后就可以丢掉不需要的redo log,但是由于braft和rocksdb是割裂的,braft在snapshot时就会丢掉一些旧日志,但是我们希望,braft可以在rocksdb刷盘时才compact日志。
所以我们对braft做了一个小改动,内部新增了一个变量,pin_log_index。大于这个log index的日志都不允许被回收掉,并向外部暴露一个借口set该值。然后在我们的程序中,每次apply raft log时,会记录raft log index 和rocksdb sequenceNumber的对应关系(抽样记录也行)。然后借助rocksdb的listener,在rocksdb 完成flush memtable时,会通过listener告知已经刷盘的最大sequenceNumber,再借助我们记录的对应关系,可以找到对应的raft log index,将其set到braft中,告知在这个index之前的日志可以被回收掉。在我们的实现中其实是采取了抽样记录的方式(每条raft log都记对应关系数据太多了,顶不住),比如每1000条记录一次,rocksdb刷盘后找到小于刷盘seq中的最大对应关系即可,但是这就必须要求状态机的变更都是幂等的。
2. 坑人的log compact机制
braft会定期执行一次snapshot,然后删除上一次snapshot以前的所有日志。为什么不删除这次snapshot以前的日志,因为本次snapshot完成后直接删除之前的,可能会导致follower拉不到日志,触发 installSnapshot。braft对于这个考虑是很好的,但是在工程中还是出现了一些问题。我们一个进程中有近百个raft node,共计TB级的数据,当这个进程停止后,过了很长一段时间重启,该进程上的所有raft node都需要拉取snapshot,意味着,需要拉取TB级数据,但是我们又没开braft的限流,导致上百个raft node同时拉取,进而网卡和磁盘带宽都被打到非常高,而所有raft node在竞争,都无法快速完成,大概过去了十几分钟,所有raft node完成了install snapshot,但是这时,位于其他进程上的leader node已经又经过了几次snapshot,将过去十分钟的旧日志给删除了。导致之前刚拉取完snaptshot的node,又需要拉取snapshot,无限循环。
查看了一些其他raft框架对于这个问题的解决方案:
- etcd,日志超过一定量时候会compact,保留本次snapshot之前5000条的日志,etcd存储的数据较小,而且qps也不高,用这个方案就够了,但是不解我们的问题
- 这个问题cockroackDB也遇到过。参考了他们的代码进行修改,cockroachDB认为,通过raft log追增量,总是代价很低的,而通过snapshot同步数据,代价是非常高的,所以需要尽可能的为follower节点保持更多的log,避免触发installSnapshot。
修改如下:
leader node知道每个follower的next index,区所有近期(1分钟)响应过心跳的follower的next index,然后取个min,再减去一个固定值,在此以后的log都不能被compact。
简单的想法就是为近期活跃的节点保存尽可能多的日志,减少出发install snapshot,还减去一个固定值的原因是减少一个边界问题。这样就解决了install snapshot 时间太长,而出发重复install snapshot的问题。