导读
本文介绍了 TiDB 中 MVCC(多版本并发控制)机制的原理和相关排查手段。 TiDB 使用 MVCC 机制实现事务,在写入新数据时不会直接替换旧数据,而是保留旧数据的同时以时间戳区分版本。 当历史版本堆积过多时,会导致读写性能下降。 为了解决这个问题,TiDB 使用 Garbage Collection(GC)定期清理不再需要的旧数据。 文章从 TiDB 中 MVCC 版本的生成原理、数据写入过程和 TiDB 版本堆积常见排查手段等方面进行了详细介绍 。
TiDB 的事务的实现采用了 MVCC(多版本并发控制)机制,当新写入的数据覆盖旧的数据时,旧的数据不会被替换掉,而是与新写入的数据同时保留,并以时间戳来区分版本。 Garbage Collection(GC)的任务便是清理不再需要的旧数据。
如上所述,TiDB 底层使用的是单机存储引擎 rocksdb, 为了实现分布式事务接口,TiDB 又采用 MVCC 机制,基于 rocksdb 实现了高可用分布式存储引擎 TiKV。也就是当新写入(增删改)的数据覆盖到旧数据时,旧数据不会被替换掉,而是与新写入的数据同时保留,并以时间戳来区分版本。当这些历史版本堆积越来越多时,就会引出一系列问题,最常见的便是读写变慢。TIDB 为了降低历史版本对性能的影响,会定期发起 Garbage Collection(GC) ( https://docs-archive.pingcap.com/zh/tidb/v7.2/garbage-collection-overview ) 清理不再需要的旧数据。
本文作为 TiDB GC 的前序文章,我们将详细介绍一下这些旧版本数据是如何堆积起来的,以及如何排查确认当前版本数据的堆积已经对集群性能构成了影响。
下面我们举个例子来详细讲讲在 TiDB 集群中,一个具体的写入过程。
当我们在 TiDB 中,执行以下 SQL 时:
insert into students set name="Bob",age=12,score=99
1.1 TiDB 侧 SQL table 转为 Key-Value
在 TiDB 层,我们有以上关系型表,上面这一行数据最终会变成三对 key-value(详细原理 https://book.tidb.io/session1/chapter3/tidb-kv-to-relation.html ),分别对应:
1.2 TiKV 侧 MVCC 版本写入
在 TiKV 层,分布式事务接口在收到对应的 key-value 对后,会转成对应的 MVCC key-value 写入到 raftstore. 这里我们不展开分布式事务的具体实现逻辑,只用最简单的乐观锁模型( https://zhuanlan.zhihu.com/p/87608202 )来举例。
其中锁 Lock CF(无版本号)如下:
数据 Default CF(mvcc 版本号 start_ts 在 key 的后缀里)如下:
具体实现中,会有一个优化,即当 value 值不是很大时,不会将数据单独放在 Default CF 里面(这里不展开具体介绍)。
以主键对为例子,数据会发生如下变化:
Write CF 里面写入:
Lock CF 中对应 key 被删除(注意这里是 rocksdb 的一次删除,rocksdb 底层 LSM 也是 mvcc, 即删除对 rocksdb 也是写入一个新版本):
综上,我们 以 主键所在 key 为例 ,展开 讲讲这个 key 随着增删改 mvcc 版本的变迁。
transaction 2: update where id=1
update 之后,在 raftstore 里面留下的 mvcc 信息如下:
也就是说,update 并没有直接去更新上一次写入的内容,而是重新写了一份数据到底层。
那如果我们 delete id=1 的这一行数据呢?从下面我们可以看到,delete 也是通过写入一个新版本到底层。
综上,当我们对 id=1 依次做了 insert/update/delete 之后,对于 TiDB 客户端来说,这一行数据已经删除,但是对于存储底层来说,此时在 raftstore 层留下了以下多个 mvcc 版本。
可以看到,同一行数据会随着增删改的次数,积累越来越多的版本,这里历史的 mvcc 版本如果不及时清理,不光物理磁盘空间无法释放,更会对读写产生性能影响,所以我们需要 GC 来对这些旧版本数据进行回收。
如前文所说,当 MVCC 版本出现堆积时,会对读写造成性能影响,此时,我们就需要对 GC 参数及状态进行判断,加速旧版本数据的回收,提升集群读写性能。
那么,在实际的业务场景中,如何判断我们的 MVCC 数据版本是否出现堆积,并对当前集群读写性能造成了影响呢?
2.1 Slow log 视角(具体慢 SQL 视角)
如前文所说,MVCC 版本堆积最直接的影响是读写变慢,所以我们从 slow log( https://docs-archive.pingcap.com/zh/tidb/v7.2/identify-slow-queries ) 可以来排查 SQL 执行慢的原因是否是 mvcc 历史版本是否堆积过多。
tidb slow log: scan_detail: {total_process_keys: 1139428, total_process_keys_size: 433849330, total_keys: 1139434, rocksdb: {delete_skipped_count: 0, key_skipped_count: 2278852,....
上面摘取的一段日志是 slow log 里面,与 TiDB mvcc 版本数量有关的几个字段:
当 total_keys > total_process_keys*6 时,代表着查询范围内的平均每个 key 的 mvcc 版本是 6 以上,需要注意 GC 的相关参数是否合理,检查 GC 的状态是否正常。
Rocksdb 相关指标 (rocksdb 里面的 mvcc):
下面我们举个例子来加深理解 slow log 里面的这些字段。(注意后续所有的例子 SQL 中,查询语句需要加上“explain analyze( https://docs.pingcap.com/tidb/stable/sql-statement-explain-analyze )” 才能看到具体的 mvcc 扫描详情)
可以看到,插入完成后再查询时:
Step 3:更新 ID=1 的这行数据
更新完后再查询时:
删除后执行查询时:
请尝试自行分析。
2.2 Grafana (集群)视角
因为 slow log 默认只记录 300 ms 以上的 SQL 读取细节,怎么看整个集群 mvcc 读取状态呢?这就需要我们从 grafana 级别来宏观分析了。
分布式事务 mvcc
监控地址:tikv-details->coprocessor-details-> Total Ops Details(TableScan/IndexScan)
如图所说:
Ops 具体分两种:
具体监控值分两类:
同样的,如果从上图中看到 processed_keys 所在的线如果远远小于 next, 则说明 mvcc 版本冗余对当前的读取已经构成性能影响。
Rocksdb 层看 MVCC
tikv-details->coprocessor->total rocksdb perf statistics:
这里 delete_skipped 主要是指 rocksdb 里面的 tombstone, 对应于 slow log 里面的 delete_skipped_count。
2.3 Region 视角(热点更新表视角)
在实际业务中,我们往往对某些 table 或者 table 中的某些行更新比较频繁,从集群角度看,就只有这些 table 涉及到的 region 的数据版本堆积比较严重。
同时 TiDB 在设计时,要求同一个 key 所在的所有 mvcc 版本数据只能落在一个 region 里面,所以如果 TiDB 中某一行数据更新过于频繁,会导致版本堆积过多而出现大 region 的情况(大于 1 G)。那么在遇到大 region 时,我们如何判断是否出现了这种情况呢?
tikv-ctl( https://docs.pingcap.com/tidb/stable/tikv-control#print-some-properties-about-region )工具提供了命令来查看具体 region 内 mvcc 数据的分布:
tiup ctl:v6.5.0 tikv --host 127.0.0.1:20160 region-properties -r 6493 Starting component `ctl`: /home/tidb/.tiup/components/ctl/v6.5.0/ctl tikv --host 127.0.0.1:20160 region-properties -r 6493 mvcc.min_ts: 442383585748713474 mvcc.max_ts: 442383589195644931 mvcc.num_rows: 410870 mvcc.num_puts: 410870 mvcc.num_deletes: 0 mvcc.num_versions: 410870 mvcc.max_row_versions: 1 writecf.num_entries: 410870 writecf.num_deletes: 0 writecf.num_files: 1 writecf.sst_files: 053983.sst defaultcf.num_entries: 0 defaultcf.num_files: 0 defaultcf.sst_files: region.start_key: 7480000000000000ffe75f728000000000ff3f028e0000000000fa region.end_key: 7480000000000000ffe75f728000000000ff454f410000000000fa region.middle_key_by_approximate_size: 7480000000000000ffe75f728000000000ff42250e0000000000faf9dc567895dbfffe
其中我们重点关注 mvcc 为前缀的为 mvcc 相关数据:
Rocksdb 的相关指标不详细展开,只需要关注到 *cf.num_deletes 比较高时,可以通过 手动 compaction ( https://docs.pingcap.com/tidb/stable/tikv-control#compact-data-of-each-tikv-manually )指定 CF 来解决。