黄东旭解析 TiDB 的核心优势
847
2024-03-28
笔记有点乱,整理一下,以备查阅,如有错误请指出。
RocksDB 作为 TiKV 的核心存储引擎,每个 TiKV 实例中有两个 RocksDB 实例。RocksDB 实例独占 WAL 文件,RocksDB 实例内不同的 CF 是独立的 LSM tree,但是共享 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 字节长度的数据。
多版本: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》。
日志先行,先写 WAL 位于kv节点的db目录(.log后缀就是wal文件),再写入 Memtable,Memtable 写满以后,将数据写入磁盘中的 SST 文件,对应 logfile 里的 log 会被安全删除。
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操作
当一个 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 达到多少个则进行刷盘flush rocksdb.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
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)
L1~Ln 层是否需要 Compaction 是依据每一层 SST 文件大小是否超过阈值。可根据以下参数进行配置。
### 可配置到 rocksdb.writecf 、rocksdb.defaultcfrocksdb.writecf.target-file-size-base: 8MB # SSTable 文件大小,文件从 L0 合并到 L1 rocksdb.writecf.max-bytes-for-level-base: 512MB # base LEVEL (L1) 最大字节数,一般设置为 memtable 大小 4 倍 rocksdb.writecf.max-bytes-for-level-multiplier: 10 # 每一层的默认放大倍数。默认值 : 10 rocksdb.writecf.num-levels: 7 # 文件最大层数。默认值 : 7 rocksdb.writecf.compression-per-level: ["no","no","lz4","lz4","lz4","zstd","zstd"] # 每层压缩算法2.5.1 Compaction 策略每层大小当超过以下阈值时则会进行 Compaction ,把数据合并到下一层。
LevelL1L2L3L4L5L6Size512MB5GB50GB500GB5TB50TB当多个 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,默认falseTiKV-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 以及具体原因
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执行的更慢了。
所有 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占用内存=block-cache.capacity + ( write-buffer-size * max-write-buffer-number * 4 )
kvdb中有4个CF所以乘以4,默认情况下每个CF的中参数配置不一样,单独计算更精确。配置的时候给系统预留25%以上的内存,避免OOM。
在 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 线程返回结果给客户端
官图镇楼,退退退~~~
1、流量控制
storage.scheduler-pending-write-threshold: 100MB # 写入数据队列的最大值,超过该值之后对于新的写入 TiKV 会返回 Server Is Busy 错误。监控指标:Scheduler writing bytes
2、获取Latches
storage.scheduler-concurrency: 2048000 # scheduler 内置一个内存锁机制,防止同时对一个 key 进行操作。
3、对比 snapshot
一个 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 才算成功(会写入一条释放锁的日志,至此用户才能读取到数据)
Raft propose -> Raft log speed:发送到 raftstore 的流量?
Scheduler - prewrite -> Scheduler keys written:prewrite 命令写入 key 的个数
【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 过程中每秒钟各种事件发生的次数
【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
持续完善中。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。