TiKV RocksDB 读写原理整理
作者: 苏州刘三枪原文来源:https://tidb.net/blog/614b0fe3
笔记有点乱,整理一下,以备查阅,如有错误请指出。
一、RocksDB
1.1 RocksDB 简介
RocksDB 作为 TiKV 的核心存储引擎,每个 TiKV 实例中有两个 RocksDB 实例,但是共享同一个 WAL 文件。
raftdb:存储 Raft 日志
kvdb:存储用户数据以及 MVCC 信息
kvdb 中有四个 ColumnFamily:raft、lock、default、write:
raft 列:用于存储各个 Region 的元信息。仅占极少量空间,用户可以不必关注。
lock 列:用于存储悲观事务的悲观锁以及分布式事务的一阶段 Prewrite 锁。当用户的事务提交之后, lock cf 中对应的数据会很快删除掉,因此大部分情况下 lock cf 中的数据也很少(少于 1GB)。如果 lock cf 中的数据大量增加,说明有大量事务等待提交,系统出现了 bug 或者故障。
write 列:用于存储用户真实的写入数据以及 MVCC 信息(该数据所属事务的开始时间以及提交时间)。当用户写入了一行数据时,如果该行数据长度小于 255 字节,那么会被存储 write 列中,否则的话该行数据会被存入到 default 列中。由于 TiDB 的非 unique 索引存储的 value 为空,unique 索引存储的 value 为主键索引,因此二级索引只会占用 writecf 的空间。
default 列:用于存储超过 255 字节长度的数据。
1.2 RocksDB 空间占用
多版本:RocksDB 作为一个 LSM-tree 结构的键值存储引擎,MemTable 中的数据会首先被刷到 L0。L0 层的 SST 之间的范围可能存在重叠(因为文件顺序是按照生成的顺序排列),因此同一个 key 在 L0 中可能存在多个版本。当文件从 L0 合并到 L1 的时候,会按照一定大小(默认是 8MB)切割为多个文件,同一层的文件的范围互不重叠,所以 L1 及其以后的层每一层的 key 都只有一个版本。
空间放大:RocksDB 的每一层文件总大小都是上一层的 x 倍,在 TiKV 中这个配置默认是 10,因此 90% 的数据存储在最后一层,这也意味着 RocksDB 的空间放大不超过 1.11 (L0 层的数据较少,可以忽略不计)
TiKV 的空间放大:TiKV 在 RocksDB 之上还有一层自己的 MVCC,当用户写入一个 key 的时候,实际上写入到 RocksDB 的是 key + commit_ts,也就是说,用户的更新和删除都是会写入新的 key 到 RocksDB。TiKV 每隔一段时间会删除旧版本的数据(通过 RocksDB 的 Delete 接口),因此可以认为用户存储在 TiKV 上的数据的实际空间放大为,1.11 加最近 10 分钟内写入的数据(假设 TiKV 回收旧版本数据足够及时)。详情见《TiDB in Action》。
1.3 LSM-Tree 写入流程图
1.4 LSM-Tree 读取流程图
日志先行,先写 WAL 位于 kv 节点的 db 目录 (.log 后缀就是 wal 文件),再写入 Memtable,Memtable 写满以后,将数据写入磁盘中的 SST 文件,对应 logfile 里的 log 会被安全删除。
二、RocksDB 写入流程
2.1 写入流程
1、产生写入请求 (put\delete)
2、写入操作系统缓存。如果配置 sync-log=true,则同时执行刷盘操作 fsync 写入本地文件
3、将请求写入到 memtable 中,并返回写入成功信息给客户端。数据后台进行 compact
至此数据已经写入完毕并返回客户端执行成功,剩下的就是 flush 与 compact 操作
2.2 memtable flush
当一个 MemTable 的大小超过 128MB 时,会切换到一个新的 MemTable 来提供写入。写满之后的转化为 immutable,然后进行刷盘 flush。当达到 memtable 最大个数限制,就会触发 RocksDB 的 write stall 。
TiKV-Details -> RocksDB KV/RocksDB raft -> Write Stall Reason 或者 RocksDB 日志(查找 Stalling 关键字)确认是否是 level0 sst 文件过多导致 write stall
2.3 immutable compaction 到 L0 层
immutable 数量达到 min-write-buffer-number-to-merge 之后就会触发 flush。
L0 层上包含的文件,是由内存中的 memtable dump 到磁盘上生成的,单个文件内部按 key 有序,文件之间无序。可能存在多个相同的 key 在 L0 层
L1~L6 层上的文件都是按照 key 有序的。也就是每层只会存在一个 key。
SST 文件命名格式
SST 文件以 storeID_regionID_regionEpoch_keyHash_cf
的格式命名。格式名的解释如下:
storeID:TiKV 节点编号
regionID:Region 编号
regionEpoch:Region 版本号
keyHash:Range startKey 的 Hash (sha256) 值,确保唯一性
cf:RocksDB 的 ColumnFamily(默认为
default
或write
)
2.4 L0 层 compaction 到 L1 层
2.5 L1~L6 层 compaction
L1~Ln 层是否需要 Compaction 是依据每一层 SST 文件大小是否超过阈值。可根据以下参数进行配置。
2.5.1 Compaction 策略
每层大小当超过以下阈值时则会进行 Compaction ,把数据合并到下一层。
| Level | L1 | L2 | L3 | L4 | L5 | L6 | || —– | —– | — | —- | —– | — | —- | - || Size | 512MB | 5GB | 50GB | 500GB | 5TB | 50TB | |
当多个 Level 都满足触发 Compaction 的条件,该如何选择?
- 对于 L1-L6,score = 该 level 文件的总长度 / 阈值。已经正在做 Compaction 的文件不计入总长度中
对于 L0,score = max{文件数量 / level0-file-num-compaction-trigger, L0 文件总长度 / max-bytes-for-level-base} 并且 L0 文件数量 > level0-file-num-compaction-trigger
2.5.2 数据存放策略
RocksDB 默认开启 dynamic-level-bytes, 所以数据文件会优先放更底层。
如果当前数据总大小低于 max-bytes-for-level-base(默认为 512MB),则所有数据都会在 L6。此时 L6 实际上相当于 L1。
如果当前数据总大小低于 max-bytes-for-level-base * max-bytes-for-level-multiplier , 则 L6 视作 L2,L5 视作 L1。
但是无论如何,除了 L0 以外的各层数据比例都按照上层比下层 1:10 进行分布。
2.5.3 compaction 关键参数
TiKV-Details -> RocksDB KV/RocksDB raft -> Compaction operations
记录的是 compaction 和 flush 操作的数量,immutable 刷到 L0 是 flush ,L0~L6 刷到下一层是 compaction。
TiKV-Details -> RocksDB KV/RocksDB raft -> Compaction reason
TiKV-Details -> RocksDB KV/RocksDB raft -> Compaction duration
TiKV-Details -> RocksDB KV/RocksDB raft -> Compaction flow
TiKV-Details -> RocksDB KV/RocksDB raft -> Compaction pending bytes
等待 compaction 的大小。Compaction pending bytes 太多会导致 stall
但是这个监控数据可能不太准确,可以根据 TiKV-Details -> RocksDB KV/RocksDB raft -> Write Stall Reason 或者 RocksDB 日志(查找 Stalling 关键字)确认是否有 compaction 导致 write stall 以及具体原因
2.6 其他关键参数
TiKV 的读取请求分为两类:
一类是指定查询某一行或者某几行的简单查询,这类查询会运行在 Storage Read Pool 中。
另一类是复杂的聚合计算、范围查询,这类请求会运行在 Coprocessor Read Pool 中。
从 TiKV 5.0 版本起,默认所有的读取请求都通过统一的线程池进行查询。
从 TiKV 4.0 升级上来的 TiKV 集群且升级前未打开 readpool.storage 的 use-unified-pool 配置,则升级后所有的读取请求仍然继续使用独立的线程池进行查询,可以将 readpool.storage.use-unified-pool 设置为 true 使所有的读取请求通过统一的线程池进行查询。
在 UnifyRead Pool 线程池,读取操作将分为 3 个不同的优先级 L0、L1、L2,执行快占用资源少的将会优先执行。在集群负载较高的时候,会发现一些慢 SQL 执行的更慢了。
三、共享 block-cache
所有 CF 共享一个 Block-cache,用于缓存数据块,加速 RocksDB 的读取速度。当开启时,为每个 CF 单独配置的 Block-cache 将无效。
3.1 内存占用计算
占用内存 =block-cache.capacity + ( write-buffer-size * max-write-buffer-number * 4 )
kvdb 中有 4 个 CF 所以乘以 4,默认情况下每个 CF 的中参数配置不一样,单独计算更精确。配置的时候给系统预留 25% 以上的内存,避免 OOM。
3.2 线程池介绍
在 TiKV 中,线程池主要由 gRPC、Scheduler、UnifyReadPool、Raftstore、Apply、RocksDB 以及其它一些占用 CPU 不多的定时任务与检测组件组成,这里主要介绍几个占用 CPU 比较多且会对用户读写请求的性能产生影响的线程池。
gRPC 线程池:负责处理所有网络请求,它会把不同任务类型的请求转发给不同的线程池。
Scheduler 线程池:负责检测写事务冲突,把事务的两阶段提交、悲观锁上锁、事务回滚等请求转化为 key-value 对数组,然后交给 Raftstore 线程进行 Raft 日志复制。
Raftstore 线程池:负责处理所有的 Raft 消息以及添加新日志的提议 (Propose)、将日志写入到磁盘,当日志在多数副本中达成一致后,它就会把该日志发送给 Apply 线程。
Apply 线程池:当收到从 Raftstore 线程池发来的已提交日志后,负责将其解析为 key-value 请求,然后写入 RocksDB 并且调用回调函数通知 gRPC 线程池中的写请求完成,返回结果给客户端。
RocksDB 线程池:RocksDB 进行 Compact 和 Flush 任务的线程池,关于 RocksDB 的架构与 Compact 操作请参考 RocksDB: A Persistent Key-Value Store for Flash and RAM Storage。
UnifyReadPool 线程池:由 Coprocessor 线程池与 Storage Read Pool 合并而来,所有的读取请求包括 kv get、kv batch get、raw kv get、coprocessor 等都会在这个线程池中执行。
gRPC 线程是 TiKV 所有请求的入口,他会将外界的请求转发给各个模块
写请求,转发给 Scheduler 线程
读请求,转发给 UnifyReadPool 线程
Scheduler 负责检测事务冲突,将复杂的事务操作转换为简单的 key-value 插入、删除,并送给 Raftstore 线程
Raftstore 负责执行 raft 日志复制,将数据复制给多个副本。当日志在多个副本上达成一致后,会发送给 Apply 线程
Apply 线程负责把写入 raft 日志的数据,再写入 kvdb。然后通知 gRPC 线程返回结果给客户端
四、Raftsotre 工作机制
官图镇楼,退退退~~~
4.1 Scheduler
1、流量控制
storage.scheduler-pending-write-threshold: 100MB # 写入数据队列的最大值,超过该值之后对于新的写入 TiKV 会返回 Server Is Busy 错误。监控指标:Scheduler writing bytes
2、获取 Latches
storage.scheduler-concurrency: 2048000 # scheduler 内置一个内存锁机制,防止同时对一个 key 进行操作。
3、对比 snapshot
4.2 Raftstore 的工作流程
一个 TiKV 实例上有多个 Region。Region 消息是通过 Raftstore 模块驱动 Raft 状态机来处理的。这些消息包括 Region 上读写请求的处理、Raft log 的持久化和复制、Raft 的心跳处理等。但是,Region 数量增多会影响整个集群的性能。
从 TiDB 发来的请求会通过 gRPC 和 storage 模块变成最终的 KV 读写消息,并被发往相应的 Region,而这些消息并不会被立即处理而是被暂存下来。Raftstore 会轮询检查每个 Region 是否有需要处理的消息。如果 Region 有需要处理的消息,那么 Raftstore 会驱动 Raft 状态机去处理这些消息,并根据这些消息所产生的状态变更去进行后续操作。例如,在有写请求时,Raft 状态机需要将日志落盘并且将日志发送给其他 Region 副本;在达到心跳间隔时,Raft 状态机需要将心跳信息发送给其他 Region 副本。
Propose 将写请求转化为 raft log
Append log 将 raft log 写入 raftdb 持久化
Replicate 将 raft log 并发复制到其他节点
Commit log 大多数节点都 Append log 后才会进行此步骤 (raft log Commit,到这一步用户仍然不能读取当前数据)
Apply 将 commit raft log 写入 kvdb ,客户端的 Commit 才算成功 (会写入一条释放锁的日志,至此用户才能读取到数据)
4.3 raftstore 关键参数
五、 监控面板
Raft propose -> Raft log speed:发送到 raftstore 的流量?
Scheduler - prewrite -> Scheduler keys written:prewrite 命令写入 key 的个数
5.1 读取慢排查
【302-Lession 29】
Thread CPU –> Unified read pool CPU
TiKV Detail –> Coprocessor Detail –> Wait Duration 请求被调度 + 获取 snapshot+ 构建 handler 的时间总和
TiKV Detail –> Coprocessor Detail –> Handle Duration 执行扫描数据的耗时
TiKV Detail –> Coprocessor Detail –> 95% Handle duration by store 查看哪个 store 慢
TiKV Detail –> Coprocessor Detail –> Total Ops Details (Index Scan)
Total Ops Details (Table Scan):coprocessor 中请求为 select 的 scan 过程中每秒钟各种事件发生的次数
Total Ops Details (Index Scan):coprocessor 中请求为 index 的 scan 过程中每秒钟各种事件发生的次数
5.2 写入慢排查
【302-Lession 28】
TiDB -> Duration 包括三部分耗时: TiDB、网络、TiKV 的耗时。
TiDB -> KV Requst:TiDB 发送请求到 TiKV 执行后并返回 (不包括 TiDB 耗时)。
如果上面俩个监控指标耗时接近,那就可以排除掉 TiDB,问题在 TiKV。
TiKV Detail -> gRPC -> 99% gRPC message duration 耗时: TiKV 的耗时。
这个指标可以查看是哪种操作比较耗时
如果这个指标耗时和 Duration 接近,那就可以定位问题在 TiKV。
TiDB –> DistSQL –> Coprocessor Seconds 999 耗时: 扫描 tikv 数据耗时,读取。
TiKV Detail -> Scheduler prewrite -> Scheduler command duration 包括两部分耗时:latch、 Async write
持续完善中。
版权声明: 本文为 InfoQ 作者【TiDB 社区干货传送门】的原创文章。
原文链接:【http://xie.infoq.cn/article/61c7af6e9b0cb80574b426a61】。文章转载请联系作者。
评论