共计 6565 个字符,预计需要花费 17 分钟才能阅读完成。
导读
云溪数据库是由浪潮开源的一款 NewSQL 分布式数据库,具备 HTAP 个性,领有强统一、高可用的分布式架构。其中,云溪数据库各方面的强一致性都依附 Raft 算法实现。咱们在上一篇文章中介绍了 Raft 一致性算法在分布式数据库中施展的重要作用,以及云溪数据库依据本身需要对 Raft 算法进行了优化革新,为其新增了三种角色设计。本文将持续介绍云溪数据库研发团队在落地 Raft 模块的过程中对其进行的其余优化改良。
上篇提到,在我的项目开发后期,云溪数据库中的 Raft 算法采纳的是开源的 etcd-raft 模块,然而在后续的生产实践中,研发团队逐步发现 etcd-raft 的模块仍存在诸多限度,于是陆续发展了如下多个方面的优化工作,具体包含:
1. 新增 Raft 角色
2. 新增 Leader 亲和选举
3. 混合序列化
4.Raft Log 拆散与定制存储
5.Raft 心跳与数据拆散
本文将着重介绍 新增 Leader 亲和选举 、 混合序列化、Raft Log 拆散与定制存储、Raft 心跳与数据拆散 这四大优化改良。
云溪数据库对 Raft 模块的改良
新增 Leader 亲和选举
云溪数据库基于 Raft 算法对外提供强统一,对于存储的所有读写操作,均须要通过 Raft Leader 解决。在以多地多数据中心模式部署的状况下,客户端心愿常常拜访的数据通过本地的网络就能够拜访到 Raft Leader,对数据进行拜访,防止跨地区拜访较高的网络提早。因而,我的项目团队针对云溪数据库中的 etcd-raft 模块为其减少了亲和选举性能,即选举 Raft Leader 时,依据亲和配置干涉选举,使亲和性更高的正本入选为 Raft Leader。
在本来的 etcd-raft 模块中,进行 Leader 选举之前,Raft group 中所有正本均为 Follower,当某个节点的选举计时器超时后会发动一次选举。NewSQL 中选举计时器超时的工夫单位为 Tick,每个 Tick 默认是 200ms (defaultRaftTickInterval),默认选举超时 Ticks 为 15 (defaultRaftElectionTimeoutTicks),最初得出超时的工夫为
[15 200ms, (215 – 1) * 200ms] == [3000ms, 5800ms]
区间内随机的 200ms 的倍数。因为选举超时的工夫是齐全随机的,那么优先发动选举的节点也是齐全随机的。
基于原有逻辑,研发团队提出了“减少一轮选举超时的工夫”的策略(如图 1 所示):
图 1:减少一轮选举超时的工夫示意图
即当选举计时器超时后,在发动选举前对亲和配置进行查看,查看的策略如下:
- 如果没有亲和配置,间接发动选举。
- 如果有亲和配置,亲和配置与以后节点 locality 标签相符,得出以后节点为亲和节点,间接发动选举。
- 如果有亲和配置,亲和配置与以后节点 locality 标签不符,得出以后节点为非亲和节点,重置选举计时器,不发动选举,期待下一轮选举超时后,即便不是亲和节点也立刻发动选举。
期待一轮选举超时后,即便不是亲和节点也立刻发动选举的目标是:保障集群肯定的可用性,个别状况下,在两次选举计时器超时距离内能够选出 Raft Leader。思考到可能存在的其余极其状况,提出“管制随机选举超时的工夫范畴”的补充策略(如图 2 所示),即在选举超时的工夫的随机范畴内,亲和节点选举超时的工夫的区间小于非亲和节点,亲和节点会先超时并发动选举。通过束缚亲和与非亲和正本的随机选举超时的工夫范畴,使得亲和正本选举计时器先超时,进步优先发动 Raft Leader 选举的概率。这样就保障了亲和正本比非亲和正本先发动一轮选举(在亲和副本能入选为 Leader 时,优先入选为 Leader),即便亲和的正本因为日志较旧,无奈入选为 Leader, 非亲和的正本在两次选举计时器超时的工夫内能入选为 Leader,保障了可用性。
图 2:管制选举超时的工夫范围图
亲和选举对 Raft 的影响有:减少一轮选举超时的工夫能够看似为一次选举失败,选举计时器重置,期待下一次选举;管制随机选举超时的工夫范畴是在原有许可范畴内,对亲和与非亲和的节点进行更细的范畴划分,划分后仍是在原有的范畴内。
混合序列化
在 etcd-raft 模块中节点之间通过 gRPC 协定实现通信,序列化形式则采纳 protobuf 进行序列化,绝对 colfer 而言,protobuf 是一种较慢的序列化形式。应用给定配置的机器(Intel Core i5 CPU@2.9 GHz、内存 8GB、Go1.15.7 darwin/amd64)利用 protobuf、gogoprotobuf 和 colfer 协定别离对给定数据进行了在序列化和反序列化,具体的试验数据如下表所示:
表 1 protobuf、gogoprotobuf 以及 colfer 协定性能比照
为了进步序列化的效率,依据试验后果采纳 protobuf+colfer 的混合序列化形式,当须要序列化操作时,调用 protobuf 的序列化形式,当将数据进行序列化时则采纳 colfer 形式放慢序列化的效率,这比独自应用 protobuf 序列化进步了 40% 的性能,表 2 是 gogoprotobuf 和混合序列化在雷同测试环境(Intel Core i7 CPU@1.8GHz × 4、内存 8G、Go1.14 linux/amd64)下的性能比照。
表 2 gogoprotobuf 和混合序列化性能比照
Raft Log 拆散与定制存储
在 etcd-raft 模块中,Raft Group 中的 Leader 节点接管客户端发来的 Request,将 Request 封装成 Raft Entry(Raft Log 的根本组成单元)追加到本地,并通过 gRPC 将 Raft Entry 发送给 Raft Group 中其余 Follower 节点,当 Follower 节点收到 Raft Entry 后进行追加、刷盘以及回复处理结果的同时,Leader 将本地 Raft Entry 进行刷盘,两者同步进行。等 Leader 节点收到过半数节点的必定回复后,提交 Raft Entry 并将其利用到状态机(将 Raft Entry 中蕴含的业务数据进行长久化),而后将处理结果返回客户端。
从下面的剖析中能够看出 Raft Log 在正本之间达成共识、节点重启以及节点故障复原等环节都起到至关重要的作用,Raft Log 与业务数据独特存储在同一个 RocksDB 中,在查问高峰期必然会产生磁盘 I/O 资源争抢,减少查问期待时延,升高数据库的整体性能。在 TPCC 场景下进行了 Raft Log 与业务数据写入量的测试,测试场景如下:在物理机(CPU:6240,72 核,内存:384G,零碎硬盘:480G,数据盘:375G+SSD,硬盘:2T*7)上启动单节点云溪数据库服务,零碎稳固后 init 6000 仓 TPCC 数据,察看整个过程中业务数据与 Raft Log 写入量的大小,测试后果如表 3 所示。
表 3 TPCC 初始化过程数据量统计
同时发展了 TPCC 场景下针对 Raft Log 各项操作数量的测试,测试场景如下:启动 3 个节点的云溪数据库集群,零碎稳固后 init 40 仓 TPCC 数据,察看网关节点在整个初始化过程中 Raft Log 各项操作数量的变动,测试后果如表 4 所示。将 RaftEntryCache 的大小从 16MiB 减少到 1GiB 后,雷同场景下 Term 与 Entry 的查问数量降落到 0。
表 4 TPCC 初始化过程 Raft Log 各项操作统计
根据上述测试以及测试后果,可将云溪数据库中 Raft Log 的操作特点总结如下:
- 在失常运行过程中,插入和删除操作是最多的且数量也很靠近,阐明 Raft Log 长久化的工夫很短。查问 RaftLogSize 也是较为惯例操作,其余操作都是在非凡场景下触发的,简直能够忽略不计。
- 查问 Term 与查问 Entry 的操作次数取决于 RaftEntryCache 的大小,是由云溪数据库外部实现机制决定的,Entry 和 Term 的查问个别先去 Unstable 中查找,查找不到再去 RaftEntryCache 中查找,还是查找不到就到底层存储中查找。通常状况下 RaftEntryCache 大小设置正当的话能够命中所有查找。
- 云溪数据库理论运行过程中产生的 Raft Log 比真正长久化的业务数据多很多(5~10 倍),而且只有数据库继续运行(即便没有任何用户查问)就会源源不断的产生 Raft Log。Raft Log 是用户数据的载体,为了保障数据完整性和一致性 Raft Log 必须长久化。
- 云溪数据库中存储 raft Log 的引擎面临的真正挑战是频繁写入、删除以及短暂的存储给零碎带来的性能损耗。
通过对云溪数据库中 Raft Log 操作场景的详细分析,总结 Raft Log 存储引擎应该满足如下特色:
· 尽可能将待查问数据保留在内存中,缩小不必要磁盘 I /O;
· 写入的数据可能及时落盘,保障故障复原后数据的完整性和一致性;
· 可能及时的清理被删除数据或是提早清理被删除数据,缩小不必要的资源占用。
针对这个问题,业内别离有不同的解决方案。以 TiDB 为例,目前 TiDB 的解决方案是:每个 TiKV 实例中有两个 RocksDB 实例,一个用于存储 Raft 日志(通常被称为 raftdb),另一个用于存储用户数据以及 MVCC 信息(通常被称为 kvdb)。同时,TiDB 团队还开发了基于 RocksDB 的高性能单机 Key-Value 存储引擎 —— Titan。
而在云溪数据库中间接应用 RocksDB 来存储 Raft Log 是不适合的,不能很好地满足 Raft Log 的具体应用场景。RocksDB 外部采纳 LSMTree 存储数据,在 Raft Log 频繁写入疾速删除并且还会继续进行随机查问的场景下,造成重大读放大和写放大,不可能充分发挥出 RocksDB 的劣势,也对系统整体性能造成不利影响。
研发团队在具体调研剖析 LevelDB、RocksDB、Titan、BadgerDB、FlashKey 以及 Aerospike 的具体架构与特色后,决定在 BadgerDB v2.0 的根底上进行定制优化,作为云溪数据库中 Raft Log 的专用存储引擎。Raft Log 定制存储实现了以下基本功能:
· Raft Log 的批量写入与长久化;
· Raft Log 的程序删除与提早 GC;
· Raft Log 的迭代查问,包含:RaftLogSize 查问、Term 查问、LastIndex 查问以及 Entry 查问;
· 相干 Metrics 的可视化;
· 多引擎场景下的用户数据残缺与统一保障策略。
云溪数据库中 Raft Log 定制存储整体部署架构如图 3 所示:
图 3:Raft Log 定制存储整体部署架构
部署时 BadgerDB 须要与 RocksDB 并列进行部署,即一个 Node 上部署相等数量的 RocksDB 实例和 BadgerDB 实例(由目前云溪数据库中正本均衡策略所决定)。
查问申请的大抵解决流程是先将 Raft Log 写入 BadgerDB,期待集群过半数节点达成共识后,再将 Raft Log 利用到状态机,行将 Raft Log 转化成用户数据写入到 RocksDB,用户写入胜利后再将 BadgerDB 中已利用的 Raft Log 删除,同时将状态数据更新到 RocksDB 中。
云溪数据库中 Raft Log 定制存储的写、读流程如图 4 所示:
图 4:RaftLog 定制存储写、读流程
因为 Raft Log 定制存储采纳 Key-Value 拆散的策略,残缺的 Key-Value 数据首先写入 VLog 并落盘(如果是删除操作则在落盘胜利后由 IfDiscardStats 更新内存中保护各 VLog File 的删除数据的统计信息,这些统计信息也会定期落盘,防止了 BadgerDB 中 SSTable 压缩不及时导致统计信息滞后的问题),而后将 Key 以及元数据信息写入 Memtable(skiplist)。将 Level 0 SSTable 放入内存,同时将须要频繁查问的信息(RaftLogSize、Term 等)记录到元数据放入内存,放慢随机读取的效率,缩小不必要的 I/O。
已删除 Key 的清理依赖 SSTable 的压缩,对应 Value 的清理则须要云溪数据库周期性调用接口,首先依据拜访 IfDiscardStats 在内存中保护的 VLog file 的 discardStats,对备选文件进行排序,程序遍历进行采样,若能够进行 GC 则遍历 VLog File 中的 Entry,同时到 Mentable (或 SSTable)查看最新元数据信息确定是否须要进行重写,须要重写则写入新的 VLog File,不须要则间接跳过,Raft Log 定制存储中 GC 解决流程如图 5 所示:
图 5:RaftLog 定制存储 VLog GC 流程
在将 Raft Log 进行独立贮存后,必须要思考多个存储引擎数据放弃一致性的策略。Raft Log 存在的目标是为了保障业务数据的残缺,因而在 Raft Log 与业务数据离开存储后不谋求两者完全一致,而是 Raft Log 放弃肯定 的“冗余”。具体策略是每个 range 上的 Raft log 在被利用到状态机之后不会立即被删除,会保留一段时间(例如:默认每个 range 默认保留 50 个 Raft Entry),进而满足用户数据完整性的各项要求。同时,如果发现所须要的 Raft Log 在本地存储中找不到,则发送音讯给 Leader 去申请,通过 MsgApp 或是 Snapshot 获取所需的 Raft Log。
研发团队实现上述优化后,发展了“迭代查问性能比照测试”与“TPCC 场景性能测试”。在“迭代查问性能比照测试”中,测试场景如下:启动单机单节点云溪数据库服务,零碎稳固运行后 init 40 仓 TPCC 数据,记录迭代器查问 RaftLogSize 与 Term 的总耗时。Raft Log 别离存储在 RocksDB、BadgerDB 以及 Raft Log 定制存储中,其中 ValueThreshold 设置为 1KB,其余设置均采纳默认值。
表 5 迭代查问测试后果汇总
从上述测试后果来看,在对 Badger 迭代器进行优化后,针对元数据的迭代查问速率失去大幅晋升,相比 RocksDB 迭代查问提早升高了约 90%。
在“TPCC 场景性能测试”中,测试场景如下:在物理机(CPU:6240 72 核 内存:384G 零碎硬盘:480G 数据盘:375G+SSD 硬盘:2T*7)启动单节点云溪数据库服务,零碎稳固后 init 6000 仓 TPCC 数据,察看整个过程中相应监控指标。
表 6 TPCC 压测监控数据汇总表
从上述测试后果来看,Raft Log 采纳定制存储后,raft Log 提交提早降落约 60%,raft Log 利用提早降落约 25%,RocksDB 读放大降落约 60%(高负载),同时没有明显增加资源耗费。
综上,利用键值拆散的思维优化 LSM 树,借助索引模块晋升迭代查问性能,应用统计前置的策略晋升零碎 GC 的效率,可能很好满足 ZNBase 中 Raft Log 在各种操作场景下的性能要求。
Raft 心跳与数据拆散
在 etcd-raft 模块的实现逻辑中,负责解决节点间心跳申请以及负责解决用户、零碎申请的 Processor 共享同一个资源池,因为贮存音讯申请的队列采纳 FIFO 执行形式,这样就可能会导致所有的资源被用户、零碎申请占用从而导致节点间心跳申请被提早期待解决,过长的提早解决工夫可能会导致集群之间因为无奈及时响应心跳申请呈现节点生效状况的产生。
因而,为了保障集群的稳定性,在此将 Raft Processor 的解决逻辑进行拆散,负责解决节点间心跳申请的 Processor 将被调配肯定份额的资源,这一部分资源只用于 Processor 解决节点间心跳音讯。
拆散 Raft Scheduler 为 Tick、ReadyRequest(Ready 与 Request)两类,两类 Scheduler 各自领有本人的资源池 (Gouroutine) 以及 RangeID 音讯队列,同时对 Raft Scheduler 解决音讯流程进行拆散,将解决 tick 申请的流程从之前的总流程中拆分,从而无效升高 Raft 心跳提早,测试结果显示优化后 Tick 解决的均匀提早降落 30%,具体如下图所示:
Tick 解决的均匀提早(优化前)
Tick 解决的均匀提早(优化后)
总结
本系列文章介绍了 Raft 一致性算法在分布式 NewSQL 云溪数据库中施展的重要作用,以及我的项目团队依据本身业务个性与需要,在落地 Raft 一致性协定的过程中对其做出的五大优化革新,心愿能对开发者进一步学习 Raft 一致性协定在分布式数据库场景下的实际过程有所帮忙。