多版本带来的空间代价
为了支持快照读取和一致性读,分布式数据库普遍采用多版本并发控制:一次更新不会原地覆盖数据,而是写入一个新版本,旧版本仍然保留。这样读事务总能看到一个稳定的历史快照,不会被并发写入干扰。
代价是存储空间会随更新次数持续增长。一次全表更新相当于把整表数据写了两遍。如果没有回收机制,磁盘很快就会被历史版本填满,而且这些版本绝大多数已经不会被任何活跃事务读取。
GC 要解决的三类清理
完整的垃圾回收通常需要覆盖以下三类残留:
- 过期历史版本:基于数据生命周期,清理早于安全点的旧版本数据;
- 删除操作的残留:执行删除表、删除索引、删除数据范围时,元数据变更很快,但底层数据的实际清理需要异步进行,包括批量范围删除操作;
- 过期锁记录:分布式事务在提交或回滚异常时可能留下锁信息,需要定期扫描全库找出生命周期已过期的锁并清理,否则会阻塞后续读写。
这三类清理通常由后台的回收组件周期性执行。
安全点是回收的前提
回收历史版本有个硬约束:不能删掉任何仍在被使用的时间点之后的数据。因此系统需要维护一个「安全点」,取所有活跃事务中最早的时间戳,只有早于安全点的版本才允许回收。
这就引出一个重要结论:长事务会直接阻碍垃圾回收。一个运行数小时的事务会把安全点一直压在很早的位置,导致期间产生的所有历史版本都无法回收,存储水位随之快速上升。运维中见到的「空间莫名增长」,很多时候根因就是一个被遗忘的长事务或长查询。
治理顺序应该是先找长事务,再调 GC 参数。反过来的话,参数调得再激进也回收不掉被长事务钉住的数据。
生命周期参数的取舍
历史版本保留多久,本质上是「读历史快照的需求」与「存储成本」之间的权衡:
- 保留时间过短:稍长的分析查询或延迟的从库读可能因版本已被回收而失败;
- 保留时间过长:存储占用持续偏高,回收压力集中;
- 合理做法:按业务最长查询时长与备份策略来确定,而不是照搬默认值;
- 故障期间:若曾出现长时间不可用,恢复后应关注积压的待回收数据量,避免清理任务把 IO 打满。
空间治理的完整视角
GC 只是空间治理的一环,完整的存储健康度还需要关注:
- 容量水位:磁盘使用率持续监测,多副本架构下实际占用远高于逻辑数据量;
- 数据分布:单个节点或 Region 过大可能导致空间倾斜,需要调度介入;
- 删除后的空间回收:确认删除操作对应的空间是否真正释放,而不是停留在待清理队列;
- 回收任务监控:回收本身也会消耗 IO,需要观测其进度与对在线业务的影响。
运维要点小结
- 把长事务监控纳入日常巡检,设置超时告警并定期清理;
- 根据业务查询特征设置合理的版本保留时间,并记录配置变更原因;
- 大范围删除(如按时间分区归档)后主动观察空间释放情况;
- 集群经历长时间故障恢复后,关注回收积压对 IO 的冲击;
- 定期核对逻辑数据量与物理占用的比例,异常放大通常意味着回收未跟上。
| 历史版本 | 基于数据生命周期清理早于安全点的旧版本,释放多版本带来的额外占用 |
|---|---|
| 删除残留 | 删除表、索引与数据范围后的底层数据清理,通过异步范围删除完成 |
| 过期锁 | 定期全库扫描,清理生命周期已过期的分布式事务锁记录 |
| 安全点 | 取活跃事务最早时间戳,早于该点的版本才允许回收 |
| 主要阻碍 | 长事务与长查询会压低安全点,使历史版本无法回收,空间持续增长 |