Skip to content

TiDB GC 机制与长事务影响

⭐⭐⭐TiDB 分布式优化TiDBGCMVCC长事务safe pointLock View

删了 1 万行磁盘反增:TiDB GC 延迟回收

DBA 发现 TiDB 集群的磁盘使用率持续上升,但业务并没有大量写入。检查发现最近执行了一批清理任务——对一张 10 万行的表做了 DELETE FROM t WHERE status = 0(约 1 万行),按理说磁盘应该释放空间,但实际上磁盘使用量不减反增。

sql
-- DBA 执行了数据清理
DELETE FROM t_gc_test WHERE status = 0;  -- 删除约 1 万行
-- 预期: 磁盘空间释放
-- 实际: 磁盘使用率持续增长

排查发现问题根源:应用层有一个持续数小时未关闭的事务——它在清理任务之前就已 BEGIN,期间一直持有快照,导致 GC Safe Point 无法推进。DELETE 产生的旧版本无法被 GC 回收,加上新写入的数据版本叠加,磁盘空间被双重挤压。

真实场景

TiDB 的磁盘空间问题与 MySQL 完全不同。MySQL 中 DELETE 操作立即标记行并放入 undo log,事务提交后 undopurge 回收。而 TiDB 的 DELETE 写入的是"数据版本"而非 undo log,旧版本由后台 GC 周期性清理。GC Safe Point 是这整个机制的关键闸门——如果它被长事务卡住,旧版本就会大量堆积,磁盘空间不会释放。

核心认知:TiDB GC 与 MySQL undo log 的差异

TiDB 的 GC 机制与 MySQL InnoDB 的 undo purge 在理念上有本质差异,理解这些差异是 DBA 管理 TiDB 的必修课。

MySQL InnoDB 的旧版本回收

MySQL InnoDB:
  UPDATE/DELETE → 写 undo log(回滚段)
  → 事务提交后 undo log 仍保留一段时间(供 MVCC 快照读)
  → InnoDB History List 长度监控
  → purge 线程自动清理
  → DBA 通常无需关注(除非 History List 过长)

TiDB 的旧版本回收

TiDB (Percolator 模型):
  UPDATE/DELETE → 写新数据版本(不写 undo log)
  → 旧版本保留在 TiKV 的 RocksDB 中
  → GC Safe Point 标记"在此之前的事务都已结束"
  → GC Worker 周期性扫描 TiKV,删除 Safe Point 之前的旧版本
  → DBA 需要主动关注 GC Safe Point 推进状态

关键差异对照

维度MySQL InnoDBTiDB
旧版本存储undo log(单独表空间)数据版本(与数据一起存在 RocksDB)
清理机制purge 线程自动处理GC Worker 周期性扫描清理
长事务影响undo log 膨胀,History List 堆积GC Safe Point 阻塞,所有旧版本冻结
DBA 关注点History list lengthSHOW ENGINE INNODB STATUStikv_gc_safe_pointmysql.tidb 系统表)
清理粒度事务级别时间戳级别(Safe Point)
磁盘回收文件收缩(ibdata1 / undo 表空间)RocksDB compaction 逐步回收

问题分析

bad.sql:长事务阻塞 GC

sql
-- bad.sql: 长事务阻塞 GC

-- 1. 查看当前 GC 配置
SHOW VARIABLES LIKE '%gc%';
SELECT * FROM mysql.tidb WHERE variable_name = 'tikv_gc_life_time';
SELECT * FROM mysql.tidb WHERE variable_name = 'tikv_gc_safe_point';

-- 2. 模拟长事务(在另一个会话保持 BEGIN 不提交)
-- 在 Session A: BEGIN; SELECT * FROM t_gc_test WHERE id = 1;

-- 3. 在主会话做大量 DELETE
DELETE FROM t_gc_test WHERE status = 0;
-- 但这些旧版本无法被 GC 回收,因为 Safe Point 被长事务阻塞

-- 4. 查看当前事务
SELECT * FROM information_schema.cluster_processlist WHERE command != 'Sleep' AND time > 10;

-- 5. 查看锁视图
SELECT * FROM information_schema.data_lock_waits;

为什么 GC Safe Point 被阻塞

TiDB 的 GC 机制基于时间戳(TSO)。每次事务开始时获取一个 start_ts,GC 在决定哪些版本可以回收时需要满足:

Safe Point = MIN(
    NOW() - tikv_gc_life_time,
    所有活跃事务的最小 start_ts - 1
)

即:GC Safe Point 永远不能超过任何活跃事务的 start_ts。 如果有一个事务 start_ts = 100 且一直未提交,Safe Point 最高只能到 99。

GC Safe Point 阻塞示意图:

活跃事务               Safe Point 位置
───────────────────────────────────────────────
事务A: start_ts=100    ← 未提交
事务B: start_ts=500    ← 未提交
───────────────────────────────────────────────
Safe Point ≤ 99        ← 不能跨越事务A的start_ts

可以回收的版本: ts < 99
被冻结的版本:   ts 99-499(包括 DELETE 产生的大量旧版本)

MVCC 版本堆积示意

正常情况(无长事务):
  row(k) → [ts=500:当前]  [ts=400:旧]  [ts=300:旧]
                             ↑           ↑
                          Safe Point=450 → GC 回收 ts<450 的版本
  回收后: row(k) → [ts=500:当前]

长事务阻塞:
  row(k) → [ts=500:当前]  [ts=400:旧]  [ts=300:旧]  [ts=200:旧]  [ts=100:旧]

                                                              Safe Point=99(卡住)
  所有旧版本都保留 → 磁盘持续膨胀

优化方案

good.sql:GC 优化与长事务治理

sql
-- good.sql: GC 优化与长事务治理

-- 1. 调整 GC 配置
UPDATE mysql.tidb SET variable_value = '24h' WHERE variable_name = 'tikv_gc_life_time';

-- 2. 查看 GC 状态和 Safe Point
SELECT * FROM mysql.tidb WHERE variable_name IN ('tikv_gc_safe_point', 'tikv_gc_run_interval', 'tikv_gc_life_time');

-- 3. 设置事务超时,防止长事务
SET SESSION tidb_idle_transaction_timeout = 300;
SET SESSION tidb_max_execution_time_ms = 10000;  -- 10 秒(TiDB 7.x;MySQL 变量 max_execution_time 在 TiDB 中不生效)

-- 4. 查看 GC 历史
SELECT * FROM mysql.tidb WHERE variable_name LIKE 'tikv_gc%';

-- 5. 合理的事务设计:短事务 + 分批处理
-- 将大量 DELETE 拆分为小批次(与 bad.sql 同条件 status=0)
DELETE FROM t_gc_test WHERE status = 0 LIMIT 1000;
-- 每批次提交后 GC 可推进

原理

方案一:调整 GC 参数

tikv_gc_life_time 控制历史版本保留时长,默认 10m。对于生产环境,建议设置为 24h——既保证快照读(AS OF TIMESTAMP)可用,又避免版本无限堆积。

方案二:事务超时防护

  • tidb_idle_transaction_timeout:事务空闲超时。连接在事务中空闲超过指定秒数,自动回滚。防止 "BEGIN 后忘记 COMMIT" 导致的长事务。
  • tidb_max_execution_time_ms(TiDB 7.x;MySQL 8.0 的 max_execution_time 在 TiDB 中不生效):单条 SQL 最大执行时间(毫秒)。防止大 SQL 撑出长事务。

方案三:分批处理

将大 DELETE 拆分为 LIMIT 1000 的小批次,每批提交后 GC Safe Point 立即可以推进到该批提交的时间戳。GC 渐进式回收旧版本,不会出现版本集中堆积。

对比

批量 DELETE(一次性,bad)分批 DELETE(LIMIT 1000,good)
单事务时长长(数分钟-数小时)短(毫秒级)
GC 阻塞阻塞 Safe Point不影响
历史版本堆积大量集中堆积分散回收,不堆积
锁持有长时间持锁短暂持锁
磁盘释放延迟(等 GC 追上)渐进释放
中断恢复代价大(回滚耗时长)随时可暂停/恢复
指标优化前 (bad)优化后 (good)
访问类型--
使用索引--
扫描行数--
附加信息--

🚀 从 Safe Point 阻塞变为渐进式推进,DELETE 后空间及时回收


避坑指南

注意事项

  1. 不要随意调大 GC life_timetikv_gc_life_time 越大,历史版本保留越久,磁盘占用越高。只有在需要频繁使用 AS OF TIMESTAMP 快照读的场景才需要调大。生产环境建议不超过 24h。

  2. 长事务要及时 kill。定期通过 CLUSTER_PROCESSLIST 或 TiDB Dashboard 检查长时间运行的事务。发现异常长事务后使用 KILL TIDB <connection_id> 终止。

  3. 不要将 tikv_gc_life_time 设得比事务超时还短。如果 GC life_time = 1m 而你的业务事务可能持续 5m,事务执行期间它引用的版本可能已被 GC 清理,引发 "GC life time is shorter than transaction duration" 错误。

  4. 监控 GC Safe Point 推进。定期检查 mysql.tidb 表中 tikv_gc_safe_point 是否正常推进。如果发现 Safe Point 长时间不动,立刻排查是否有长事务。

  5. GC 参数调整要慎重tikv_gc_concurrency 调得太大可能导致 TiKV 在高负载时段额外消耗 CPU 做 GC。建议在低峰期调参,并监控 TiKV CPU 使用率。

  6. DDL 期间 GC 行为特殊。TiDB DDL 操作也可能引用历史版本。在线 DDL 期间如果 GC life_time 太短,DDL 可能失败。

  7. 使用 TiDB Dashboard 可视化监控。Dashboard 中的 GC 页面可以直观看到 GC 状态、Safe Point 位置、GC 耗时等指标,比 SQL 手动查询更方便。


GC 参数配置指南

参数默认值推荐值适用场景
tikv_gc_life_time10m24h常规业务,偶尔使用快照读
tikv_gc_run_interval10m10m所有场景(保持默认)
tikv_gc_concurrency22-4大规模集群或数据变更频繁时适当调大
tikv_gc_scan_lock_modeLEGACYPHYSICAL(v5.0+)升级到 v5.0+ 后建议切换
tikv_gc_enable_compaction_filterfalsetrue(v5.0+)写入密集型场景,利用 compaction 辅助回收
tikv_gc_auto_concurrencytruetrue所有场景(让 TiDB 自动调整)
tidb_idle_transaction_timeout0(不限制)300所有在线业务(防止忘记 COMMIT)
tidb_max_execution_time_ms0(不限制)10000在线 OLTP 业务(防止慢 SQL 撑出长事务,TiDB 7.x 变量名)

本地复现

bash
# 启动 TiDB 集群
tiup playground v7.5.1 --db 1 --kv 3

# 运行本案例
./scripts/run-case.sh 88-tidb-gc --ver tidb

# 跳过造数据重跑
./scripts/run-case.sh 88-tidb-gc --ver tidb --no-seed

执行后观察:

  • SHOW VARIABLES LIKE '%gc%' 查看 GC 相关变量
  • mysql.tidb 系统表中 tikv_gc_safe_point 的推进情况
  • 长事务阻塞时 Safe Point 是否停滞
  • 分批删除后 Safe Point 是否立即推进

模拟长事务阻塞实验

在本地复现时,可以通过两个终端模拟长事务阻塞 GC:

终端 1(模拟长事务)

sql
BEGIN;
SELECT * FROM t_gc_test WHERE id = 1;
-- 保持此事务不提交

终端 2(执行 DELETE + 查看 Safe Point)

sql
DELETE FROM t_gc_test WHERE status = 0;
COMMIT;

-- 反复执行以下查询,观察 Safe Point 是否推进
SELECT * FROM mysql.tidb WHERE variable_name = 'tikv_gc_safe_point';

只有当终端 1 的 BEGINCOMMITROLLBACK 后,Safe Point 才会恢复推进,旧版本才会被 GC 回收。

MIT Licensed