Fork me on GitHub

如何应对大数据量挑战?分布式事务型 KV 数据库 TiKV 的实现和实践

以下文章来源于 https://zhuanlan.zhihu.com/p/667429407

导读 本次分享将带领大家探索分布式事务型 KV 数据库 TiKV 的实现和实践。文中会对 TiKV 的特性和适用场景进行介绍,并分享项目迭代过程中所面临的挑战和解决方案。

本次分享主要内容包括:

  1. TiKV 的特性和使用场景

  2. TiKV 的架构和实现

  3. 大数据量的挑战和实践

分享嘉宾|沈泰宁 平凯星辰(北京)科技有限公司,PingCAP 高级软件工程师

编辑整理|杨哲

内容校对|李瑶

出品社区|DataFun

01TiKV 是什么



TiKV 是一款分布式事务型 KV 数据库,开源在 github 上,有 395 个贡献者,13k star。作为 TiDB 的分布式存储引擎,TiKV 由 PingCAP 发起并持续推动发展。TiKV 还是 CNCF 基金会的毕业项目。

TiKV 的分布式架构可以无缝扩展集群规模,能够根据业务需求灵活调整容量,并且不会影响到业务的读写请求。这意味着开发者可以放心地使用 TiKV,因为其对外提供了强一致性(即线性一致性)保证,减轻了开发过程中的心智负担。同时,由于采用了分布式架构,TiKV 具备高可用性,即使少数节点发生故障或网络异常,也能快速恢复服务。

作为 TiDB 的存储层,TiKV 提供了事务 KV API,支持 ACID,提供快照隔离级别。我们也提供了配套的客户端,开发者可以使用该客户端快速开发自己的应用。



TiKV 作为有状态的存储层,非常适用于大数据量的存储场景。开发者可以把精力放在无状态的计算层,把数据放到 TiKV 中,下面是三个典型的应用场景。

TiKV 与 TiDB 的配合使用,就可以构建可水平扩展的分布式 MySQL 服务。TiDB 还利用了 TiKV 的计算能力,支持分布式的 SQL 查询,比如将 SQL 中算子下推到 TiKV 中计算。

TiKV 与 TiSpark 配合使用,就能将 TiKV 接入到 Spark 平台中,让 Spark 查询处理 TiKV 中的数据,与 TiDB 类似,TiSpark 也支持调用 TiKV 的分布式计算能力,将 Spark 中的查询下推到 TiKV 中执行。

TiKV 与 Titan 配合使用,可以提供一个支持强一致的分布式 Redis 服务,在服务高可用的同时也能无痛扩缩容。

总的来说,开发者只需关心业务逻辑,把状态放在 TiKV 后,便可以轻松实现高可用性和水平扩展等需求。

02TiKV 的架构和实现

1. TiKV 的集群架构



从集群角度看下 TiKV 架构。上图示意了一个典型的 TiKV 集群,中间有 4 个对等的 TiKV 节点,负责存放用户数据。右边是 Placement Driver 集群,简称 PD 集群,负责提供集群的元数据服务,比如 TiKV 节点的信息和用户数据的路由信息,即用户数据存放在哪个 TiKV 节点上。

2. TiKV 的数据架构



再从数据分布的角度来看下 TiKV 的架构。TiKV 通过按范围分片的方式来划分数据,每个分片被一个 Raft Group 所管理,在 TiKV 中我们称之为 Region。默认情况下,每个 Region 包含三个分布在不同 TiKV 节点上的 Raft 副本。按范围分片所面临的最大挑战是热点问题,即业务数据的写入和读取可能会集中在某一个 Region 上。因此,当一个 Region 的数据量超过阈值时,TiKV 自动将其分裂成多个更小的 Region;当一个 Region 的数据量低于阈值时,TiKV 自动将其与相邻的 Region 合并。

PD 具有对 TiKV 集群的全局视角,根据 Region 的大小和读写流量情况,对 Region 进行调度,以期尽可能均衡各个 TiKV 节点之间的磁盘使用率和 CPU 使用率。

客户端根据要访问的 key 从 PD 中查询该 key 所在的 Region 和其 Raft Leader 所在的 TiKV 节点地址,然后将通过 gRPC 协议访问 TiKV 的API。

3. TiKV 的分层设计



TiKV 内部采用了分层设计,将功能划分为四个层级,每一层都只负责自己的事情,RocksDB 负责数据的存储,Raft 负责节点间数据同步,保证数据的安全性,Transaction 负责数据的读写冲突和事务的隔离性,TiKV API 负责 gRPC kv API 逻辑,Coprocessor API 负责 TiDB 的算子下推计算。

层级之间只通过各自定义好的接口进行交互。每一层的功能都是建立在下一层提供的接口之上的,不会直接跨层调用。

这样的分层设计有利于降低代码的耦合度,每个层级只关注自己的责任,提高了维护和理解代码的易用性。同时,也方便进行单元测试,确保代码的质量,并提供了接口抽象以适应不同功能的扩展和优化。

每一层的实现都和 TiKV 的特性有紧密关联。下面自上而下地来介绍。

(1)网络层

首先是网络层,TiKV 使用了高性能的 gRPC 作为通信框架,这不仅提供了良好的性能,还具有广泛的语言支持,方便与更多生态系统进行集成。TiKV 提供了多种形式的服务接口,包括支持事务的 KV 服务、高性能但不支持事务的纯 KV 服务,还有用于加速 SQL 查询的计算下推服务。

(2)事务层

在网络层之下,是事务层。TiKV 实现了一个基于 Percolator 算法的事务处理机制,支持乐观事务。此外,TiKV 还在 Percolator 的基础上做了一些改进,加入了对悲观事务的支持。用户可以根据业务负载特点,灵活选择事务模式:如果业务依赖于 MySQL 事务的行为,可以选择悲观事务模式;如果业务冲突较少,则可以选择乐观事务,以获得更高的吞吐量和较低的延迟。事务层提供了快照隔离的特性和事务 ACID 属性中的 ACI(原子性、一致性、隔离性)特性,而 D(持久性)特性由下一层实现。

(3)一致性层

接下来是一致性层,该层提供了最基本的键值操作接口,如 kv put/kv delete/kv get/snapshot。在一致性层内部,TiKV 实现了 Raft 一致性算法,并提供了强一致性(即线性一致性)保证。此外,TiKV 还扩展了 Raft 算法,并引入了 multi-raft 算法,使数据能够自动分片。通过 multi-raft 算法,每个 Region 的大小可以保持在大约 96MB,而 PD(Placement Driver)则可以通过调度实现水平扩展。

(4)RocksDB

最底层是 RocksDB,作为高效的键值存储引擎,它是 TiKV 真正存储数据的地方。RocksDB 提供了持久化存储的能力,并被 TiKV 内部的各个层次使用来进行数据的读写操作。

03大数据量的挑战和实践



大数据量意味着什么?

对于 TiKV 来说,一个 Region 就是一个 Raft group,管理 96MB 的数据。那么 100TB 就会有大约 109 万个 Region。在这个截图中是一套实际的 TiKV 集群,集群中有大约 100TB 的数据,由于 Raft 的 3 副本,实际磁盘数据有 267 TB,由 112 万个 Region 管理。这些 Region 运行在 100 个TiKV 节点上。平均每个 TiKV 节点需要驱动 3.4 万个 Raft 状态机。

在这种场景下 TiKV 遇到的挑战主要集中在以下三方面:

  • 线程瓶颈问题
  • 资源消耗问题
  • 性能抖动问题

TiKV 解决这几个问题花了不少时间,最后的效果也达到了预期。

1. 线程瓶颈



先来看下线程瓶颈。这个问题是大数据量场景中首先遇到的,因为它的影响非常显著。如果不解决线程瓶颈,后续的其他问题将无法得到有效暴露。

我们先关注驱动一个 Raft 状态机所需要的工作量。这个状态机的输入事件可以分为两类:内部输入和外部输入。内部输入包括 Raft 定时任务和 Raft 之间的消息,例如逻辑时钟的 tick 和 Raft 心跳。而外部输入则包括来自客户端的读写请求。这些事件按顺序通过一个队列进行执行。

该状态机的输出则包括保存 Raft 日志和发送 Raft 消息。

此外,当 Raft 日志 commit 之后,还需要执行 Raft 日志,从日志中反序列化出来自客户端的 KV 操作,然后将 KV 操作应用到 RocksDB 中。

在之前的 TiKV 版本中,一个节点只有一个线程用于驱动 Raft 状态机,因此当 Region 数量增多时,该单一线程很容易被耗尽,从而导致明显的延迟问题。由于仅存在一个线程,即使节点上拥有更多的 CPU 资源,也无法完全发挥其作用。



在解决这个问题的时候,考虑到 Raft 的实现使用了状态机设计模式,所以 TiKV 选择了 Actor 模式来实现多线程驱动 Raft 状态机。

TiKV 从一开始在实现 Multi-Raft 的时候就尽可能地避免让 Raft 状态机之间共享状态,所以在实现 Actor 多线程驱动的时候也会很自然,将之前所有 Raft 状态机共享一个队列改成一个 Raft 状态机独占一个队列(就是 Actor 中的 Mailbox),这样就可以并发驱动不同的 Raft 状态机了。

在保持之前效率的前提下,实现多线程之后,明显提高了 CPU 使用率,整体的吞吐也对应上,平均延时相应降低。

2. 资源消耗



在解决单线程问题之后,资源消耗成了棘手的挑战,体现在没有读写的 Region 依旧消耗 CPU 和网络资源,因为每个 Raft 状态机有定时任务,驱动内部逻辑时钟,Raft Leader 需要发送心跳,Raft Follower 需要处理心跳。即便单个定时任务只消耗极少量的 CPU,即便单个心跳消息只使用极少网络流量,但在 Region 数量多了之后,这些消耗叠加在一起就会非常明显。

上图中展示了某一测试集群空闲时的监控截图,上面的是 CPU 使用率监控,默认配置下一个 TiKV 节点会开启两个线程来驱动 Raft 状态机,可以看到这两个线程已经被用满了。整个集群每秒发送的消息数量也非常多,共计有 62 万。

所以针对这个问题,TiKV 开发了 Hibernate Region 功能,暂停驱动近期没有用户读取和写入的 Region,把 CPU 和网络资源让给活跃的 Region,该功能极大缓解了空闲 Region 的开销。从这个测试集群监控中也能看到,右边开了 Hibernate Region 之后 CPU 和网络消耗大幅下降,让 TiKV 只做有用功。

虽然该功能将让不活跃的 Region 休眠,但这并不会导致对这些 Region 的访问变慢,因为只要有一个读取或写入就能立即唤醒对应的 Region。



TiKV 的 multi-raft 特性中的 Region Merge 功能,通过合并小 Region 也能进一步降低资源消耗。什么时候 Region 会变小呢?除了用户主动删除数据之外,还有事务层 MVCC 的 GC 删除过期数据让 Region 变小。

一旦 PD 发生有 Region 过小就会让它合并到它的相邻 Region 中。比如 Region 1 合并到 Region 2,那么在完成合并之后,Region 2 会包含 Region 1 的全部数据,同时 Region 2 的数据范围也会相应扩大。

由于 Region Merge 涉及多个 Region,所以 TiKV 的 Merge 算法使用了一种类似两阶段提交的方式,通过两个阶段让两个 Region 安全合并到一起。以上面的 Region 1 和 Region 2 为例,首先通过 PrepareMerge 命令让 Region1 进入到 Merging 状态,处理一些 Merge 前必要的事情,这个状态也会持久化到磁盘上,所以即便中途 TiKV 重启了也能继续进行 Merge。当 Region1 准备好之后,就会向 Region2 发起 CommitMerge 命令,Region2 执行 CommitMerge 的时候就会将 Region1 合并进来,并将 Region1 的状态更改为 Tombstone 状态。如果 Region1 中途发现 Region2 不能 Merge 自己时,就会向自己发起一个 RollbackMerge 命令,取消这次 Merge,把自己变回 Normal 状态。

有了 Region Merge 功能后,TiKV 就能控制 Region 数据量在一个合理的范围内了,同时处理热点时也能采用激进写的策略,不用担心 Region 数量不受控制的问题了。

3. 性能抖动



性能抖动也是一类有趣的问题, 可能的原因也非常多。不仅来自于程序内部的 bug,也有可能来自外部。比如最近在 TiKV 上云的过程中,我们发现虽然云盘的设备接口也是 NVMe,也能提供不错的 IOPS 和带宽,但在稳定性上和本地盘相比还是差了很多。时不时会出现 IO 延时上升,特别在集群规模大了之后,抖动出现的频率也变多了。

为了适应这种相对不稳定的环境,TiKV 开发了异步处理 Raft IO 的功能,将 CPU 密集型任务(比如 Raft 心跳处理,Raft 日志处理)和 IO 密集型任务(保存 Raft Log 和 Raft 元数据,读取 Raft Log,生成 Raft Snapshot)分散到不同线程。

之前在驱动 Raft 状态机时,可能会产生 IO 数据,比如 Raft 日志,它们需要在下次驱动前持久化,这就导致后续新的请求必须要等到这次 IO 结束。这不仅会增加平均延时,也放大 IO 抖动导致的长尾请求个数。

有了异步 Raft IO 功能之后,即便上次的数据还没持久化,TiKV 还是可以继续处理新来的请求,解决了 TiKV 对磁盘抖动敏感的问题。也因为不需要等待上次的 IO 了,还降低了请求平均的延时。



还有一类抖动也很有挑战。RocksDB 是一款非常成熟,已被广泛使用的高性能 KV 存储引擎。在 TiKV 的使用场景中,我们发现单 RocksDB 在大数据量下性能会有所下降。注意这里的下降和 TiKV 的使用方式以及这个大数据量有关,并不代表其他场景。

在这个大数据量场景中,我们发现 TiKV 的 compaction 会增加,写放大也会增加。通过观察 RocksDB 的 PerfContext,我们还发现 RocksDB 内部的 mutex 操作耗时也在逐步上升,综合这些情况,TiKV 的写入速度会随着数据量的增加而降低。

为此,TiKV 正在开发一项新功能,让每个 Region 都有独立的 RocksDB 实例,同时为了控制 RocksDB 的个数不要过多,我们将 Region 的大小增加到 10GiB。经过我们的 PoC 验证,该功能显著减低了锁竞争和写放大,写入保持在一个稳定的速度上,没有出现随着数据量增加而变慢的情况。

以上就是本次分享的内容,谢谢大家。




本文地址:https://www.6aiq.com/article/1700442249909
本文版权归作者和AIQ共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出