写点什么

TiKV RocksDB 读写原理整理

  • 2023-02-10
    北京
  • 本文字数:7654 字

    阅读完需:约 25 分钟

作者: 苏州刘三枪原文来源: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)


 TiKV-Details -> RocksDB KV/RocksDB raft -> **Write operations** (正常情况下with\_wal、done 的数量应该是保持一致)
复制代码


2、写入操作系统缓存。如果配置 sync-log=true,则同时执行刷盘操作 fsync 写入本地文件


 先写 WAL 日志文件,方便 crash recovery 的时候可以根据日志恢复。配置sync-log=false,把数据写进了操作系统的缓存区就返回了,进行下一步
TiKV-Details -> RocksDB KV/RocksDB raft -> **Write WAL duration (**在进行 WAL 时所花费的时间**)**
TiKV-Details -> RocksDB KV/RocksDB raft -> **WAL sync operations** (调用操作系统 fsync 的次数)
TiKV-Details -> RocksDB KV/RocksDB raft ->** WAL sync duration** (调用操作系统 fsync 将数据持久化到硬盘上耗时)
复制代码


3、将请求写入到 memtable 中,并返回写入成功信息给客户端。数据后台进行 compact


 TiKV-Details -> RocksDB KV/RocksDB raft -> **Write Durtion** (收到 put/delete 请求到完成请求返回给 client 所花费的时间)
复制代码


至此数据已经写入完毕并返回客户端执行成功,剩下的就是 flush 与 compact 操作

2.2 memtable flush

当一个 MemTable 的大小超过 128MB 时,会切换到一个新的 MemTable 来提供写入。写满之后的转化为 immutable,然后进行刷盘 flush。当达到 memtable 最大个数限制,就会触发 RocksDB 的 write stall 。


### 可配置到 rocksdb.writecf 、rocksdb.defaultcfrocksdb.writecf.write-buffer-size: 256MB              # memtable 大小rocksdb.writecf.min-write-buffer-number-to-merge: 1   # immutable 达到多少个则进行刷盘flushrocksdb.writecf.max-write-buffer-number: 24           # memtable 最大个数rocksdb.writecf.max-background-flushes: 2             # memtable 刷写的最大后台线程数
复制代码


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 层

### 可配置到 rocksdb.writecf 、rocksdb.defaultcfrocksdb.writecf.level0-file-num-compaction-trigger: 4 # 触发 L0 向 L1 合并的 L0 文件数rocksdb.writecf.level0-slowdown-writes-trigger: 32    # 触发 write stall 的 L0 文件数rocksdb.writecf.level0-stop-writes-trigger: 64        # 触发完全阻塞写入的 L0 文件数
### 向 L1 的compaction不可以与其他level compaction并行。需单独配置此参数rocksdb.max-sub-compactions: 2
复制代码

2.5 L1~L6 层 compaction

L1~Ln 层是否需要 Compaction 是依据每一层 SST 文件大小是否超过阈值。可根据以下参数进行配置。


### 可配置到 rocksdb.writecf 、rocksdb.defaultcfrocksdb.writecf.target-file-size-base: 8MB            # SSTable 文件大小,文件从 L0 合并到 L1rocksdb.writecf.max-bytes-for-level-base: 512MB       # base LEVEL (L1) 最大字节数,一般设置为 memtable 大小 4 倍rocksdb.writecf.max-bytes-for-level-multiplier: 10    # 每一层的默认放大倍数。默认值 : 10rocksdb.writecf.num-levels: 7                         # 文件最大层数。默认值 : 7rocksdb.writecf.compression-per-level: ["no","no","lz4","lz4","lz4","zstd","zstd"]   # 每层压缩算法
复制代码

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 关键参数

rocksdb.max-background-jobs: 16                       # Compact 和 Flush 任务的最大线程池   rocksdb.rate-bytes-per-sec: 0KB                       # 限制后台 compaction 任务的磁盘流量,默认不限制soft-pending-compaction-bytes-limit: 64GB             # Compaction pending bytes达到限制之后,RocksDB会放慢写入速度 hard-pending-compaction-bytes-limit: 256GB            # Compaction pending bytes达到限制之后,RocksDB会停止写入 rocksdb.use-direct-io-for-flush-and-compaction: true  # 设置为 true 数据(wal不支持)读写绕过文件系统缓存 https://github.com/facebook/rocksdb/wiki/Direct-IO,默认false
复制代码


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 其他关键参数

rocksdb.bytes-per-sync: 256MB                         # 异步Sync限速速率?默认1MB,一直弄不明白???rocksdb.wal-bytes-per-sync: 256MB                     # WAL Sync限速速率gc.max-write-bytes-per-sec                            # 控制GC流量,默认不限制。storage.scheduler-concurrency: 2048000                # scheduler 内置一个内存锁机制,防止同时对一个 key 进行操作。storage.scheduler-worker-pool-size: 8                 # Scheduler 线程主要负责写入之前的事务一致性检查工作readpool.coprocessor.use-unified-pool: true           # 是否使用统一的读取线程池处理存储请求readpool.storage.use-unified-pool: true               # (clean-tidb:false,spider-tidb:true)readpool.unified.max-thread-count: 16                 # 统一处理读请求的线程池最大线程数,即UnifyReadPool线程池的大小server.snap-max-write-bytes-per-sec: 100MB            # 处理 snapshot 时最大允许使用的磁盘带宽storage.scheduler-pending-write-threshold: 100MB      # 写入数据队列的最大值,超过该值之后对于新的写入 TiKV 会返回 Server Is Busy 错误。监控指标:Scheduler writing bytes
复制代码


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 将无效。


### 配置后所有CF共享一个block-cache,以下的 block-cache-size 不再需要手动设置:### rocksdb.defaultcf.block-cache-size### rocksdb.writecf.block-cache-size### rocksdb.lockcf.block-cache-size### raftdb.defaultcf.block-cache-size storage.block-cache.capacity: 96GB
复制代码

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 关键参数

raftstore.sync-log:false                             # log 落盘是否 sync(4.0.7开始已废弃,忽略此参数)raftstore.apply-max-batch-size:8192                  # 一轮处理数据落盘的最大请求个数raftstore.store-max-batch-size:8192                  # 一轮处理的最大请求个数raftstore.apply-pool-size:8                          # 数据落盘的线程数raftstore.store-pool-size:8                          # 处理 raft 的线程数raftdb.max-background-jobs:8                         # 这是干啥的?应该不需要调整raftdb.max-sub-compactions:2                         # 这是干啥的?应该不需要调整raftstore.hibernate-regions:true                     # 如果 Region 长时间处于非活跃状态,即被自动设置为静默状态raftstore.raft-max-inflight-msgs:16384               # 待确认的日志个数,如果超过,Raft 状态机会减缓发送日志的速度
复制代码

五、 监控面板

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 


持续完善中。


发布于: 刚刚阅读数: 4
用户头像

TiDB 社区官网:https://tidb.net/ 2021-12-15 加入

TiDB 社区干货传送门是由 TiDB 社区中布道师组委会自发组织的 TiDB 社区优质内容对外宣布的栏目,旨在加深 TiDBer 之间的交流和学习。一起构建有爱、互助、共创共建的 TiDB 社区 https://tidb.net/

评论

发布
暂无评论
TiKV RocksDB读写原理整理_TiDB 底层架构_TiDB 社区干货传送门_InfoQ写作社区