前言
前面两篇分别做了 BR 快照恢复和 PITR。
快照恢复那篇比较简单,备份在哪个时间点,恢复后就回到哪个时间点。后面又加上日志备份,继续测试了 PITR,把集群恢复到了误操作之前。
做到这里以后,我本来想继续看看别的功能,结果 MinIO 里的备份目录先提醒了我一个问题:
日志一直在写,空间也一直在涨。
日志备份和普通的全量备份不太一样。全量备份一般是一份一份的,哪天做的、占了多少空间,看目录就能大概判断。日志备份是连续写的,只要业务还在跑,文件就会不断增加。
测试环境数据量不大,短时间内当然没什么压力。但如果放到长期运行的环境里,不做清理肯定不现实。
问题是,这些日志又不能看着日期旧了就直接删。
PITR 恢复时,需要先找到目标时间之前的一份全量备份,然后再把这份全量备份之后的日志继续应用到目标时间。如果中间哪一段日志被删掉了,前后的文件都还在也没有意义。
所以这次没再折腾恢复,而是把注意力放到一个更实际的问题上:
日志备份跑久了以后,到底该怎么清理,既能省空间,又不把后面的恢复搞坏。
一、先看看现在有多少备份
测试环境还是前面那套,没有换。
| 项目 | 配置 |
|---|---|
| TiDB / BR | v7.5.7 |
| 源集群 PD | 192.168.56.101:2379 |
| BR 中控机 | 192.168.56.110 |
| MinIO | 192.168.56.120:9000 |
| Bucket | tidb-br |
| 日志备份任务 | pitr_lab |
| 日志目录 | pitr-lab/log-backup |
| 时区 | Asia/Shanghai |
日志备份是从 8 月 6 日开始的,后面每两天做一次全量快照。
到 8 月 14 日时,MinIO 里大概是这个样子:
s3://tidb-br/pitr-lab/
├── log-backup/
├── snapshot-20260806-093000/
├── snapshot-20260808-093000/
├── snapshot-20260810-093000/
├── snapshot-20260812-093000/
└── snapshot-20260814-093000/
日志目录一直没停,全量备份也在不断往后增加。
BR 中控机上还是使用之前的环境变量:
export LOG_STORAGE='s3://tidb-br/pitr-lab/log-backup?endpoint=http://192.168.56.120:9000&force-path-style=true'
先看一下日志任务:
tiup br:v7.5.7 log status \
--task-name pitr_lab \
--pd "192.168.56.101:2379"
输出里有两个东西我现在会习惯性多看一眼:
status: ● NORMAL
checkpoint[global]: 2026-08-14 18:08:42.317 +0800; gap=3m21s
NORMAL 说明任务还在正常跑。
但真正让我更关心的是下面这个 checkpoint[global]。
它往前走,才说明日志确实还在持续写到备份存储里。要是哪天任务显示 NORMAL,但是 checkpoint 半个小时都没动,那就不能只看着绿色状态放心了。
这一点和传统备份任务有点像。很多时候“任务在运行”和“数据已经安全落盘”不是完全一回事。
二、我先给测试环境定了 5 天保留时间
这是个人测试环境,没必要留一个月。
我先按 5 天算。
当前时间是:
2026-08-14 18:20:00 +0800
按 5 天往前推,理论上只需要保留:
2026-08-09 18:20:00 +0800
之后的恢复能力。
一开始我看到这个时间,第一反应也挺直接:
那就把 8 月 9 日以前的日志和全量备份都删掉。
但真准备动手的时候,发现不能这么算。
比如现在要恢复到:
2026-08-09 20:00:00 +0800
这个时间之前最近的一份全量备份是哪一份?
不是 8 月 10 日,因为 8 月 10 日已经晚了。
只能用:
snapshot-20260808-093000
也就是说,虽然这份 8 月 8 日的快照已经超过 5 天,但它现在还不能删。
因为想恢复到 8 月 9 日,就必须先从 8 月 8 日这份全量备份开始,再接着应用后面的日志。
这个地方如果只是按“保留 5 天”去机械删目录,很容易把最前面的恢复能力一起删掉。
所以实际判断时,不能只看“5 天以前有什么”。
应该反过来想:
我最早想恢复到哪一天?那个时间点之前最近的一份全量备份是哪一份?
找到这份全量备份以后,它就是当前必须留下来的起点。
三、先把要保留的全量快照找出来
当前最早需要恢复的时间是:
2026-08-09 18:20:00 +0800
往前找最近的一份全量快照:
snapshot-20260808-093000
这份不能删。
先定义一下地址:
export SNAPSHOT_KEEP='s3://tidb-br/pitr-lab/snapshot-20260808-093000?endpoint=http://192.168.56.120:9000&force-path-style=true'
接下来不是直接用目录名字里的 20260808-093000 当清理时间,而是读取 BR 这份备份真正的 BackupTS:
FULL_BACKUP_TS=$(
tiup br:v7.5.7 validate decode \
--field="end-version" \
--storage "${SNAPSHOT_KEEP}" \
| tail -n 1
)
echo "${FULL_BACKUP_TS}"
我觉得这里还是值得注意一下。
目录名是自己起的,主要方便人看。真正决定备份时间点的是 BR 元信息里的 TS。
所以后面删日志,我也直接用这个 BackupTS,不自己手工拼一个时间。
这样出错概率小一点。
四、第一次清日志,我还是先跑 dry-run
真正执行删除之前,我先跑:
tiup br:v7.5.7 log truncate \
--until="${FULL_BACKUP_TS}" \
--storage="${LOG_STORAGE}" \
--dry-run
--dry-run 不会真的删除日志,只是先计算一下哪些内容会被清掉。
测试环境里其实没多少数据,就算删错了,大不了重新来。
但我还是觉得这个习惯有必要保留。
生产环境里日志备份可能已经几十 TB,真要是哪次 until 时间写错了,再想回来就麻烦了。
我主要确认两件事。
一个是 until 对应的是 8 月 8 日这份要保留的快照,不是 8 月 9 日那个“5 天保留”的时间。
另一个是清理范围没有越过这份快照。
没问题以后,再正式执行。
五、正式截断旧日志
命令也很简单:
tiup br:v7.5.7 log truncate \
--until="${FULL_BACKUP_TS}" \
--storage="${LOG_STORAGE}" \
--yes
--yes 只是跳过交互确认。
这个操作不需要连接 TiDB 源集群,BR 直接操作备份存储里的日志数据。
清理完以后,我第一件事不是去看 MinIO 少了多少 GB,而是继续检查日志元数据:
tiup br:v7.5.7 log metadata \
--storage="${LOG_STORAGE}"
清理前,日志最早是从 8 月 6 日开始的:
log-min-date="2026-08-06 09:20:14.100 +0800"
log-max-date="2026-08-14 18:08:42.317 +0800"
清理以后,最早时间已经往后推到了 8 月 8 日这份快照附近:
log-min-date="2026-08-08 09:30:xx.xxx +0800"
log-max-date="2026-08-14 18:08:42.317 +0800"
这里我其实不太在意最后几位时间是多少。
只要确认两个点就行:
旧日志确实删掉了,新的日志还在。
后面的日志任务也没有受到影响。
六、为什么不能直接进 MinIO 把旧日志目录删掉
日志文件多起来以后,直接进 MinIO 按日期删,看起来是最省事的。
比如:
log-backup/20260806
log-backup/20260807
看到旧目录就删,好像也没什么问题。
但 BR 的日志目录不是普通的归档目录。
除了实际数据文件,还有元数据和 checkpoint 相关信息。BR 恢复时是按这些元数据去找对应日志的。
如果自己直接把某些文件删掉,目录表面上可能还在,真正恢复时才发现文件缺失,那就比较难受了。
所以我这里坚持一个原则:
日志备份目录内部不手工删。
只用:
br log truncate
处理。
全量备份目录不一样。
比如:
snapshot-20260806-093000/
snapshot-20260808-093000/
这些本身就是一份一份独立的全量备份。
确认某份已经不属于当前恢复范围以后,可以直接把整个快照目录删除。
这样处理起来比较清楚,也不容易把 BR 内部日志结构破坏掉。
七、旧日志清掉以后,再删旧全量备份
现在当前恢复窗口最早依赖的是:
snapshot-20260808-093000
所以这份必须留。
再往前的:
snapshot-20260806-093000
已经没有继续保留的必要。
先看 MinIO:
aws \
--endpoint-url http://192.168.56.120:9000 \
s3 ls s3://tidb-br/pitr-lab/
当前是:
PRE snapshot-20260806-093000/
PRE snapshot-20260808-093000/
PRE snapshot-20260810-093000/
PRE snapshot-20260812-093000/
PRE snapshot-20260814-093000/
删除 8 月 6 日这份:
aws \
--endpoint-url http://192.168.56.120:9000 \
s3 rm \
s3://tidb-br/pitr-lab/snapshot-20260806-093000/ \
--recursive
最后保留下来的内容:
s3://tidb-br/pitr-lab/
├── log-backup/
├── snapshot-20260808-093000/
├── snapshot-20260810-093000/
├── snapshot-20260812-093000/
└── snapshot-20260814-093000/
现在再看这套备份,就比较顺眼了。
如果要恢复到 8 月 9 日,可以从 8 月 8 日快照开始。
如果恢复到 8 月 11 日,可以从 8 月 10 日快照开始。
后面的时间也是一样。
只要全量快照和它后面的日志是连续的,就还能正常恢复。
八、删完以后,我还是重新检查了一遍
备份清理最怕的不是删不掉,而是删得太干净。
空间确实回来了,等真正恢复的时候才发现中间断了一截,这种情况就比较尴尬。
所以清理以后,我又重新检查了一遍。
先看日志元数据:
tiup br:v7.5.7 log metadata \
--storage="${LOG_STORAGE}"
确认最早日志时间还在 8 月 8 日快照附近,最新日志也还在继续往前。
再读取保留的快照:
tiup br:v7.5.7 validate decode \
--field="end-version" \
--storage "${SNAPSHOT_KEEP}"
确保这份全量备份本身还能正常读取。
最后看日志任务:
tiup br:v7.5.7 log status \
--task-name pitr_lab \
--pd "192.168.56.101:2379"
任务还是:
status: ● NORMAL
checkpoint 也在继续变化。
做到这里,我才算放心。
严格来说,最完整的检查当然还是再找一套空集群做一次 PITR。
不过前面已经专门做过一篇 PITR 恢复,这次主要是验证清理过程,所以没有再把恢复步骤完整重复一遍。
如果以后在生产环境调整备份保留策略,我还是会建议实际恢复一次。
这种事情只看“备份状态正常”意义不大。
九、日志备份长期跑,还有几个地方不能忘
1. NORMAL 不是万能的
最开始我也容易只盯着:
status: ● NORMAL
看。
跑久了以后会发现,checkpoint 更重要。
如果:
checkpoint[global]
一直停在那里不动,虽然任务还显示正常,但最新数据其实没有及时进入备份存储。
对生产环境来说,这直接影响 RPO。
TiDB 也提供了:
tidb_log_backup_last_checkpoint
这类指标,用来观察最新 checkpoint。
Grafana 里的 Backup Log 面板也可以直接看。
2. 临时停任务,别上来就 stop
如果只是临时暂停日志备份,比如维护存储或者排查网络问题,应该用:
br log pause
恢复时:
br log resume
不要动不动:
br log stop
因为 stop 会把任务本身停掉并清掉相关任务信息。
这个和 pause 不是一个概念。
另外 pause 也不能无限暂停。
暂停时间太长以后,历史 MVCC 数据被 GC 掉,再 resume 时就可能出现缺日志的问题。
测试环境里感觉不到,生产上就要特别注意。
3. 日志一直增长是正常的
日志备份不是异常日志,不是越少越好。
只要数据库还在有写入,备份目录增长就是正常现象。
真正需要管理的是:
- 存储空间还能撑多久;
- checkpoint 有没有持续推进;
- 现有全量快照和日志是不是还能组成完整恢复范围;
- 老数据什么时候可以安全清掉。
这里最容易犯的错误,就是单独盯着存储空间。
看到 MinIO 快满了,就急着删文件。
实际上先搞清楚恢复时间范围更重要。
十、以后我准备固定这么清理
这次做完以后,我给自己整理了一个比较简单的处理顺序。
先确定最早需要恢复到什么时候。
比如:
保留最近 5 天
然后不要直接删 5 天以前的数据。
先找 5 天保留窗口之前,最近的一份全量快照。
把这份当成锚点。
读取它的 BackupTS:
FULL_BACKUP_TS=$(
tiup br:v7.5.7 validate decode \
--field="end-version" \
--storage "${SNAPSHOT_KEEP}" \
| tail -n 1
)
先 dry-run:
tiup br:v7.5.7 log truncate \
--until="${FULL_BACKUP_TS}" \
--storage="${LOG_STORAGE}" \
--dry-run
确认没问题:
tiup br:v7.5.7 log truncate \
--until="${FULL_BACKUP_TS}" \
--storage="${LOG_STORAGE}" \
--yes
日志截断完成以后,再删除这份锚点之前的旧全量备份。
最后重新看:
br log metadata
以及:
br log status
确认最早恢复点、最新 checkpoint 和任务状态都正常。
结语
这次本来只是想给 MinIO 腾点空间,最后反而把 BR 的保留逻辑又重新梳理了一遍。
我一开始也差点按照“保留 5 天”直接去删 5 天以前的数据。
实际往恢复方向倒推以后才发现,不行。
如果想保留 8 月 9 日开始的恢复能力,那 8 月 8 日最近的这份全量备份就必须留下。
日志也只能删到这份全量备份对应的 BackupTS。
这个地方如果没想清楚,文件删得很干净,恢复能力也一起删掉了。
做到现在,这套 TiDB 测试环境里的 BR 我已经连续做了三块内容:
快照备份和单表恢复,日志备份和 PITR,再到这次日志清理和备份保留。