热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

深入解析Raft模块在ZNBase中的优化改造(下)

深入,解析,raft,模块,在,znbase,

作者:管延信

上期回顾:深入解析 Raft 模块在 ZNBase 中的优化改造(上)

导读

云溪数据库 ZNBase 是由浪潮开源的一款 NewSQL 分布式数据库,具备 HTAP 特性,拥有强一致、高可用的分布式架构。其中,ZNBase 各方面的强一致性都依靠 Raft 算法实现。我们在上一篇文章中介绍了 Raft 一致性算法在分布式数据库中发挥的重要作用,以及 ZNBase 根据自身需求对 Raft 算法进行了优化改造,为其新增了三种角色设计。本文将继续介绍 ZNBase 研发团队在落地 Raft 模块的过程中对其进行的其他优化改进。

前文提到,在项目开发前期,ZNBase 中的 Raft 算法采用的是开源的 etcd-raft 模块,但是在后续的生产实践中,ZNBase 研发团队逐渐发现 etcd-raft 的模块仍存在诸多限制,于是陆续开展了如下多个方面的优化工作,具体包括:

  1. 新增 Raft 角色
  2. 新增 Leader 亲和选举
  3. 混合序列化
  4. Raft Log 分离与定制存储
  5. Raft 心跳与数据分离

本文将着重介绍新增 Leader 亲和选举、混合序列化、Raft Log 分离与定制存储、Raft 心跳与数据分离这四大优化改进。

ZNBase 对 Raft 模块的改进

新增 Leader 亲和选举

ZNBase 基于 Raft 算法对外提供强一致,对于存储的所有读写操作均需要通过 Raft Leader 处理多地多数据中心模式部署的情况下,客户端希望经常访问的数据经过本地网络就可以访问到 Raft Leader,对数据进行访问,避免跨地区访问较高的网络延迟。因此,项目团队针对 ZNBase 中的 etcd-raft 模块为其增加了亲和选举功能,即选举 Raft Leader 时,根据亲和配置干预选举,使亲和性更高的副本当选为 Raft Leader。 

在原本的 etcd-raft 模块中,进行 Leader 选举之前,Raft group 中所有副本均为 Follower,当某个节点的选举计时器超时后会发起一次选举。NewSQL 中选举计时器超时的时间单位为 Tick,每个 Tick 默认是 200ms (defaultRaftTickInterval),默认选举超时 Ticks 为 15 (defaultRaftElectionTimeoutTicks),最后得出超时时间为

[15 * 200ms, (2*15 – 1) * 200ms] == [3000ms, 5800ms]

区间内随机的200ms的倍数。由于选举超时时间是完全随机的,那么先发起选举的节点也是完全随机的。

基于原有逻辑,研发团队提出了“增加一轮选举超时时间”的策略(如图 3 所示):

图3:增加一轮选举超时时间示意图

即当选举计时器超时后,在发起选举前对亲和配置进行检查,检查的策略如下:

  1. 如果没有亲和配置,直接发起选举。
  2. 如果有亲和配置,亲和配置与当前节点 locality 标签相符,得出当前节点为亲和节点,直接发起选举。
  3. 如果有亲和配置,亲和配置与当前节点 locality 标签不符,得出当前节点为非亲和节点,重置选举计时器,不发起选举,等待下一轮选举超时后,即使不是亲和节点也立即发起选举。

等待一轮选举超时后,即使不是亲和节点也立即发起选举的目的是:保证集群一定的可用性,一般情况下,在两次选举计时器超时间隔可以选出 Raft Leader。考虑到可能存在的其他极端情况,提出“控制随机选举超时时间范围”的补充策略(如图 4 所示),即在选举超时时间的随机范围内,亲和节点选举超时时间的区间小于非亲和节点,亲和节点会先超时发起选举。通过约束亲和与非亲和副本的随机选举超时时间范围,使得亲和副本选举计时器先超时,提高优先发起 Raft Leader 选举的概率。这样就保证了亲和副本比非亲和副本先发起一轮选举(在亲和副本能当选为 Leader 时,优先当选为 Leader),即使亲和的副本由于日志较旧,无法当选为 Leader, 非亲和的副本在两次选举计时器超时时间内能当选为 Leader,保障了可用性。

图4 控制选举超时时间范围图

亲和选举对 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 协议性能对比

benchmark

iter

time/iter

bytes/op

allocs/op

ProtobufMarshal

1761278

674 ns/op

52

152

ProtobufUnmarshal

1916198

627 ns/op

52

192

GogoprotobufMarshal

9089631

131 ns/op

53

64

GogoprotobufUnmarshal

6390816

190 ns/op

53

96

ColferMarshal

10938900

108 ns/op

51

64

ColferUnmarshal

7260112

166 ns/op

52

112

为了提高序列化的效率,根据实验结果采用 protobuf+colfer 的混合序列化方式,当需要序列化操作时,调用 protobuf 的序列化方式,当将数据进行序列化时则采用 colfer 方式加快序列化的效率,这比单独使用 protobuf 序列化提高了 40% 的性能,表 2 是 gogoprotobuf 和混合序列化在相同测试环境(Intel Core i7 CPU@1.8GHz × 4 、内存 8G、Go1.14 linux/amd64)下的性能对比。

表2 gogoprotobuf和混合序列化性能对比

benchmark

iter

time/iter

bytes/op

allocs/op

GogoprotobufMarshal

9799002

121 ns/op

32

1

GogoprotobufUnmarshal

6607788

182 ns/op

120

2

混合序列化Marshal

16514504

69.6 ns/op

32

1

混合序列化Unmarshal

10472240

103 ns/op

85

2

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)上启动单节点 ZNBase 服务,系统稳定后 init 6000 仓 TPCC 数据,观察整个过程中业务数据与 Raft Log 写入量的大小,测试结果如表 3 所示。 

表3 TPCC 初始化过程数据量统计

TPCC仓数

业务数据量

Raft Log数据量

Raft Log写入量

6000

458GiB

11GiB

1.7TiB

同时开展了 TPCC 场景下针对 Raft Log 各项操作数量的测试,测试场景如下:启动 3 个节点的 ZNBase 集群,系统稳定后 init 40 仓 TPCC 数据,观察网关节点在整个初始化过程中 Raft Log 各项操作数量的变化,测试结果如表 4 所示。将 RaftEntryCache 的大小从 16MiB 增加到 1GiB 后,相同场景下 Term 与 Entry 的查询数量下降到 0。 

表4 TPCC 初始化过程Raft Log各项操作统计

插入

删除

查询RaftLogSize

查询LastIndex

查询Term

查询Entry

6.616M

6.423M

160K

0

1.264K

27

根据上述测试以及测试结果,可将 ZNBase 中 Raft Log 的操作特点总结如下: 

  1. 在正常运行过程中,插入和删除操作是最多的且数量也很接近,说明 Raft Log 持久化的时间很短。查询 RaftLogSize 也是较为常规操作,其他操作都是在特殊场景下触发的,几乎可以忽略不计。
  2. 查询 Term 与查询 Entry 的操作次数取决于 RaftEntryCache 的大小,是由 ZNBase 内部实现机制决定的,Entry和Term的查询一般先去Unstable中查找,查找不到再去RaftEntryCache中查找,还是查找不到就到底层存储中查找。通常情况下RaftEntryCache大小设置合理的话可以命中所有查找。
  3. ZNBase 实际运行过程中产生的 Raft Log 比真正持久化的业务数据多很多(5~10倍),而且只要数据库持续运行(即使没有任何用户查询)就会源源不断的产生 Raft Log。Raft Log 是用户数据的载体,为了保证数据完整性和一致性 Raft Log 必须持久化。
  4. ZNBase 中存储 raft Log 的引擎面临的真正挑战是频繁写入、删除以及短暂的存储给系统带来的性能损耗。

通过对 ZNBase 中 Raft Log 操作场景的详细分析,总结 Raft Log 存储引擎应该满足如下特征:

  • 尽可能将待查询数据保存在内存中,减少不必要磁盘I/O;
  • 写入的数据能够及时落盘,保证故障恢复后数据的完整性和一致性;
  • 能够及时的清理被删除数据或是延迟清理被删除数据,减少不必要的资源占用。 

针对这个问题,业内分别有不同的解决方案。以 TiDB 为例,目前 TiDB 的解决方案是:每个 TiKV 实例中有两个 RocksDB 实例,一个用于存储 Raft 日志(通常被称为 raftdb),另一个用于存储用户数据以及 MVCC 信息(通常被称为 kvdb)。同时,TiDB 团队还开发了基于 RocksDB 的高性能单机 Key-Value 存储引擎 —— Titan。 

而在 ZNBase 中直接使用 RocksDB 来存储 Raft Log 是不合适的,不能很好地满足 Raft Log 的具体使用场景。RocksDB 内部采用 LSMTree 存储数据,在 Raft Log 频繁写入快速删除并且还会持续进行随机查询的场景下,造成严重读放大和写放大,不能够充分发挥出 RocksDB 的优势,也对系统整体性能造成不利影响。 

ZNBase 研发团队在详细调研分析 LevelDB、RocksDB、Titan、BadgerDB、FlashKey 以及 Aerospike 的具体架构与特征后,决定在 BadgerDB v2.0 的基础上进行定制优化,作为ZNBase 中 Raft Log 的专用存储引擎。Raft Log 定制存储实现了以下基本功能: 

  • Raft Log 的批量写入与持久化;
  • Raft Log 的顺序删除与延迟 GC;
  • Raft Log 的迭代查询,包括:RaftLogSize 查询、Term 查询、LastIndex 查询以及 Entry 查询;
  • 相关 Metrics 的可视化;
  • 多引擎场景下的用户数据完整与一致保证策略。

ZNBase 中 Raft Log 定制存储整体部署架构如图 5 所示:

图 5:Raft Log 定制存储整体部署架构

部署时 BadgerDB 需要与 RocksDB 并列进行部署,即一个 Node 上部署相等数量的 RocksDB 实例和 BadgerDB 实例(由目前 ZNBase 中副本平衡策略所决定)。

查询请求的大致处理流程是先将 Raft Log 写入 BadgerDB,等待集群过半数节点达成共识后,再将 Raft Log 应用到状态机,即将 Raft Log 转化成用户数据写入到 RocksDB,用户写入成功后再将 BadgerDB 中已应用的 Raft Log 删除,同时将状态数据更新到 RocksDB 中。

ZNBase 中 Raft Log 定制存储的写、读流程如图 6 所示:

图 6: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 的清理则需要 ZNBase 周期性调用接口,首先根据访问 IfDiscardStats 在内存中维护的 VLog file 的 discardStats,对备选文件进行排序,顺序遍历进行采样,若可以进行 GC 则遍历 VLog File 中的 Entry,同时到 Mentable (或 SSTable)查看最新元数据信息确定是否需要进行重写,需要重写则写入新的 VLog File,不需要则直接跳过,Raft Log 定制存储中 GC 处理流程如图 7 所示:

图 7 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。 

ZNBase 研发团队完成上述优化后,开展了“迭代查询性能对比测试”与“TPCC 场景性能测试”。在“迭代查询性能对比测试”中,测试场景如下:启动单机单节点 ZNBase 服务,系统稳定运行后 init 40 仓 TPCC 数据,记录迭代器查询 RaftLogSize 与 Term 的总耗时。Raft Log 分别存储在 RocksDB、BadgerDB 以及 Raft Log 定制存储中,其中 ValueThreshold 设置为1KB,其他设置均采用默认值。

表5 迭代查询测试结果汇总

指标

RocksDB存储Raft Log

BadgerDB存储Raft Log

定制存储Raft Log

迭代查询总延迟

463.6ms

549.9ms

48.5ms

RocksDB读放大

约10

约7

从上述测试结果来看,在对 Badger 迭代器进行优化后,针对元数据的迭代查询速率得到大幅提升,相比 RocksDB 迭代查询延迟降低了约 90% 

在“TPCC场景性能测试”中,测试场景如下:在物理机(CPU:6240 72核 内存:384G 系统硬盘:480G 数据盘:375G+SSD硬盘:2T*7)启动单节点 ZNBase 服务,系统稳定后 init 6000 仓 TPCC 数据,观察整个过程中相应监控指标。

表6 TPCC 压测监控数据汇总表

指标

RocksDB存储Raftlog

定制存储Raftlog

定制存储Raftlog(暂停GC)

Init

Raft命令提交延迟

305ns

275ns

___

RocksDB读放大

103

35

___

无负载

Raft命令提交延迟

360ns

280ns

___

压测

Raft日志提交延迟

50ms

15ms

15ms

Raft命令提交延迟

240ns

180ns

180ns

RocksDB读放大

10

7

6

从上述测试结果来看,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 数据库 ZNBase 中发挥的重要作用,以及 ZNBase 项目团队根据自身业务特性与需求,在落地 Raft 一致性协议的过程中对其做出的五大优化改造,希望能对开发者进一步学习 Raft 一致性协议在分布式数据库场景下的实践过程有所帮助。

关于 ZNBase 的更多详情可以查看:

官方代码仓库:https://gitee.com/ZNBase/zn-kvs

ZNBase 官网:http://www.znbase.com/ 

对相关技术或产品有任何问题欢迎提 issue 或在社区中留言讨论。

同时欢迎更多对分布式数据库感兴趣的开发者加入我们的团队!

联系邮箱:haojingyi@inspur.com

往期回顾

深入解析 Raft 模块在 ZNBase 中的优化改造(上)

HTAP 数据库如何实现?浅析 ZNBase 中的列存引擎


推荐阅读
  • 一、Hadoop来历Hadoop的思想来源于Google在做搜索引擎的时候出现一个很大的问题就是这么多网页我如何才能以最快的速度来搜索到,由于这个问题Google发明 ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • Oracle分析函数first_value()和last_value()的用法及原理
    本文介绍了Oracle分析函数first_value()和last_value()的用法和原理,以及在查询销售记录日期和部门中的应用。通过示例和解释,详细说明了first_value()和last_value()的功能和不同之处。同时,对于last_value()的结果出现不一样的情况进行了解释,并提供了理解last_value()默认统计范围的方法。该文对于使用Oracle分析函数的开发人员和数据库管理员具有参考价值。 ... [详细]
  • 本文介绍了游标的使用方法,并以一个水果供应商数据库为例进行了说明。首先创建了一个名为fruits的表,包含了水果的id、供应商id、名称和价格等字段。然后使用游标查询了水果的名称和价格,并将结果输出。最后对游标进行了关闭操作。通过本文可以了解到游标在数据库操作中的应用。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • 本文讨论了如何使用IF函数从基于有限输入列表的有限输出列表中获取输出,并提出了是否有更快/更有效的执行代码的方法。作者希望了解是否有办法缩短代码,并从自我开发的角度来看是否有更好的方法。提供的代码可以按原样工作,但作者想知道是否有更好的方法来执行这样的任务。 ... [详细]
  • IjustinheritedsomewebpageswhichusesMooTools.IneverusedMooTools.NowIneedtoaddsomef ... [详细]
  • 本文由编程笔记#小编为大家整理,主要介绍了logistic回归(线性和非线性)相关的知识,包括线性logistic回归的代码和数据集的分布情况。希望对你有一定的参考价值。 ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • 电话号码的字母组合解题思路和代码示例
    本文介绍了力扣题目《电话号码的字母组合》的解题思路和代码示例。通过使用哈希表和递归求解的方法,可以将给定的电话号码转换为对应的字母组合。详细的解题思路和代码示例可以帮助读者更好地理解和实现该题目。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文讨论了在Windows 8上安装gvim中插件时出现的错误加载问题。作者将EasyMotion插件放在了正确的位置,但加载时却出现了错误。作者提供了下载链接和之前放置插件的位置,并列出了出现的错误信息。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
author-avatar
品花人生1
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有