前言
上一篇文章记录了 TiDB 7.5 测试集群的搭建过程。集群启动后,TiDB、PD、TiKV 都处于正常状态,基本的 SQL 读写也没有问题。
不过,对数据库来说,“能够正常运行”和“发生误操作后能够恢复”是两件事。
我平时接触 Oracle、Data Guard 和备份恢复比较多,所以看到备份任务显示成功时,通常还会多想一步:目录里的文件是否完整,真正恢复时有没有权限问题,恢复后的数据又该怎样确认。
这些问题不实际操作一次,很难得到可靠答案。
因此,集群搭好后,我没有马上进行性能测试,而是先用 BR 做了一次快照备份和单表恢复。整个过程并不复杂:准备测试数据、记录备份基线、执行快照备份,然后增加两条新数据并删除测试表,最后从备份中单独恢复这张表。
我想验证的主要是两点:
- 这份 BR 快照能否真正用于恢复;
- 恢复后的数据究竟停留在哪个时间点。
这篇记录不涉及 TiDB 集群的安装部署,主要整理这次备份、误删、恢复和校验的过程。
一、实验环境与恢复方式
这次还是沿用上一篇搭建的个人测试集群。
| 项目 | 配置 |
|---|---|
| TiDB 版本 | TiDB 7.5.x |
| TiDB Server | 2 个实例 |
| PD | 3 个实例 |
| TiKV | 3 个实例 |
| BR | 与集群相同的 7.5 分支 |
| 备份存储 | NFS 共享目录 |
| NFS 挂载路径 | /data/br_backup |
| 测试数据库 | hospital_lab |
| 测试表 | patient_visit |
为什么选择 BR
对于刚执行不久的 DROP TABLE,BR 不一定是最直接的恢复方法。
如果删除操作仍处于 GC lifetime 范围内,可以优先评估 RECOVER TABLE。这种方式能够利用 GC 尚未清理的历史数据恢复被删除的表,通常比从备份恢复更快。
这次选择 BR,是为了验证一条独立于在线集群历史版本的恢复路径。
如果误操作没有被及时发现,或者相关历史数据已经被 GC 清理,最终仍然要依赖外部备份。只有真正从备份中恢复过一次,才能确认这条恢复路径是否可用。
实际发生故障时,恢复方式需要结合误操作类型、发生时间、GC 状态、备份时间点和影响范围综合判断,而不是固定使用某一种工具。
二、准备一组方便校验的数据
测试表本身不需要太复杂,但恢复前后必须有明确的对比依据。
我创建了一个测试数据库和一张门诊记录表:
CREATE DATABASE hospital_lab;
USE hospital_lab;
CREATE TABLE patient_visit (
visit_id BIGINT NOT NULL,
patient_no VARCHAR(32) NOT NULL,
department VARCHAR(64) NOT NULL,
visit_time DATETIME NOT NULL,
total_amount DECIMAL(12,2) NOT NULL,
visit_status VARCHAR(20) NOT NULL,
PRIMARY KEY (visit_id),
KEY idx_visit_time (visit_time)
);
写入 10 条测试数据:
INSERT INTO patient_visit
(visit_id, patient_no, department, visit_time,
total_amount, visit_status)
VALUES
(10001, 'P000001', '内科', '2026-08-05 08:01:00', 18.50, 'FINISHED'),
(10002, 'P000002', '外科', '2026-08-05 08:05:00', 35.00, 'FINISHED'),
(10003, 'P000003', '儿科', '2026-08-05 08:12:00', 0.00, 'REGISTERED'),
(10004, 'P000004', '骨科', '2026-08-05 08:18:00', 72.30, 'FINISHED'),
(10005, 'P000005', '眼科', '2026-08-05 08:25:00', 16.00, 'FINISHED'),
(10006, 'P000006', '急诊科', '2026-08-05 08:31:00', 128.40, 'FINISHED'),
(10007, 'P000007', '内科', '2026-08-05 08:36:00', 25.00, 'FINISHED'),
(10008, 'P000008', '外科', '2026-08-05 08:42:00', 46.80, 'FINISHED'),
(10009, 'P000009', '儿科', '2026-08-05 08:49:00', 12.00, 'FINISHED'),
(10010, 'P000010', '眼科', '2026-08-05 08:55:00', 90.00, 'FINISHED');
备份之前,我先记录了几项基线:
SELECT
COUNT(*) AS row_count,
SUM(total_amount) AS amount_sum,
MIN(visit_id) AS min_visit_id,
MAX(visit_id) AS max_visit_id
FROM patient_visit;
结果如下:
row_count = 10
amount_sum = 444.00
min_visit_id = 10001
max_visit_id = 10010
同时保存表结构:
SHOW CREATE TABLE patient_visit\G
并抽查头尾数据:
SELECT *
FROM patient_visit
ORDER BY visit_id
LIMIT 3;
SELECT *
FROM patient_visit
ORDER BY visit_id DESC
LIMIT 3;
只记录总行数其实不太够。
恢复后即使行数相同,也可能存在金额变化、主键范围不一致、索引丢失等问题。这次数据量很小,因此我把行数、金额合计、主键范围、表结构和抽样记录都作为恢复基线。
真实业务中还应增加业务日期、状态分布、关联表关系和应用侧验证。
三、NFS 路径不能只在BR节点上检查
备份目录使用 NFS,运行 BR 的节点和所有 TiKV 节点都挂载到相同路径:
/data/br_backup
刚开始接触 BR 时,我容易把它理解成常见的备份客户端:在哪台服务器上运行命令,只要那台服务器能够访问备份目录就可以了。
BR 的情况并不完全一样。
使用 local:// 存储时,备份数据由 TiKV 节点写入。不同 Region 的 Leader 可能分布在不同 TiKV 上,因此只在运行 BR 的节点上挂载 NFS,并不能保证备份顺利完成。
如果各节点使用的是彼此独立的本地目录,SST 文件还可能分散在不同服务器上。后续恢复时,目标 TiKV 无法访问完整的备份文件,便可能出现文件不存在或下载 SST 失败的问题。
我先在所有相关节点检查挂载状态:
df -h /data/br_backup
mount | grep /data/br_backup
然后使用运行 TiKV 的用户测试读写权限:
sudo -u tidb touch /data/br_backup/.write_test_$(hostname)
ls -l /data/br_backup/.write_test_*
确认所有节点都能看到测试文件后,再进行清理:
rm -f /data/br_backup/.write_test_*
如果 TiKV 节点使用的 tidb 用户 UID 不一致,即使用户名相同,也可能在 NFS 上遇到权限问题。这一点最好在正式备份前确认,而不是等到任务执行时再排查。
四、执行快照备份
确认存储路径和权限没有问题后,创建独立目录:
mkdir -p /data/br_backup/snapshot_20260805
mkdir -p /home/tidb/br-log
执行全量快照备份:
tiup br:v7.5.7 backup full \
--pd "192.168.56.101:2379" \
--storage "local:///data/br_backup/snapshot_20260805" \
--ratelimit 64 \
--log-file "/home/tidb/br-log/backup_20260805.log"
这里没有指定 --backupts,BR 会选择任务开始时对应的快照时间点。
--ratelimit 64 用于限制单个 TiKV 向备份存储写入数据的速度,单位为 MiB/s。个人测试环境数据量很小,这个参数主要是为了避免备份任务无节制地占用测试节点资源。
备份结束后,我先查看终端输出中的任务状态、BackupTS、数据量和耗时,再检查日志:
grep -iE "error|fatal|panic" \
/home/tidb/br-log/backup_20260805.log
随后确认备份目录中已经生成文件:
find /data/br_backup/snapshot_20260805 \
-maxdepth 2 -type f | head
du -sh /data/br_backup/snapshot_20260805
如需单独读取备份快照对应的版本,可以检查备份元信息:
tiup br:v7.5.7 validate decode \
--field="end-version" \
--storage "local:///data/br_backup/snapshot_20260805" \
| tail -n 1
只有确认终端结果、日志和备份目录都没有明显异常后,我才继续进行删除和恢复测试。
五、在备份之后再写入两条数据
为了让恢复时间点更加直观,我在快照备份完成后又插入了两条记录:
INSERT INTO patient_visit
(visit_id, patient_no, department, visit_time,
total_amount, visit_status)
VALUES
(20001, 'P020001', '急诊科',
'2026-08-05 10:01:00', 66.00, 'FINISHED'),
(20002, 'P020002', '内科',
'2026-08-05 10:05:00', 88.00, 'FINISHED');
再次查询:
SELECT
COUNT(*) AS row_count,
SUM(total_amount) AS amount_sum,
MIN(visit_id) AS min_visit_id,
MAX(visit_id) AS max_visit_id
FROM patient_visit;
此时结果已经变为:
row_count = 12
amount_sum = 598.00
min_visit_id = 10001
max_visit_id = 20002
这两条数据是在快照之后写入的,因此后续恢复完成后,它们不应该存在。
六、模拟误删并恢复测试表
确认新增数据后,执行删除:
DROP TABLE hospital_lab.patient_visit;
检查测试表已经不存在:
SHOW TABLES FROM hospital_lab;
SELECT COUNT(*)
FROM hospital_lab.patient_visit;
此时 TiDB、PD 和三个 TiKV 节点都处于正常状态,集群没有发生任何节点故障,也不会因为这条 DDL 产生基础设施告警。
但表已经被正常删除,并且这个结果会同步到各个副本。
这也说明,多副本和备份解决的不是同一个问题。Raft 副本能够帮助集群应对部分节点故障,但无法保留误操作之前的数据状态。
执行单表恢复
测试表已经被删除,不存在同名对象冲突,因此可以从全量快照中筛选并恢复指定表:
tiup br:v7.5.7 restore table \
--pd "192.168.56.101:2379" \
--db "hospital_lab" \
--table "patient_visit" \
--storage "local:///data/br_backup/snapshot_20260805" \
--log-file "/home/tidb/br-log/restore_patient_visit_20260805.log"
恢复前不要手工创建同名空表。
即使字段和索引定义完全相同,手工创建的表也是一个新的表对象,可能与备份中的对象产生冲突。
恢复过程中,BR 会使用目标 TiKV 的 CPU、磁盘和网络资源。个人测试环境数据量很小,影响并不明显;生产环境中则应提前评估恢复负载,必要时恢复到独立集群或离线环境。
任务结束后,检查日志:
grep -iE "error|fatal|panic" \
/home/tidb/br-log/restore_patient_visit_20260805.log
再确认表已经重新出现:
SHOW TABLES FROM hospital_lab;
到这一步,只能说明 BR 恢复任务执行完成。表里的数据是否正确,还需要继续检查。
七、恢复后的数据如何确认
我先检查表结构:
SHOW CREATE TABLE hospital_lab.patient_visit\G
重点确认字段定义、主键、idx_visit_time 索引、字符集和表选项是否与备份前一致。
随后检查数据汇总:
SELECT
COUNT(*) AS row_count,
SUM(total_amount) AS amount_sum,
MIN(visit_id) AS min_visit_id,
MAX(visit_id) AS max_visit_id
FROM hospital_lab.patient_visit;
恢复后的结果应当回到快照备份时的状态:
row_count = 10
amount_sum = 444.00
min_visit_id = 10001
max_visit_id = 10010
再检查快照完成后插入的两条记录:
SELECT *
FROM hospital_lab.patient_visit
WHERE visit_id IN (20001, 20002);
预期结果为空。
这说明 BR 恢复的是快照时的数据,而不是执行 DROP TABLE 之前的最新状态。
最后抽查几条业务记录:
SELECT *
FROM hospital_lab.patient_visit
WHERE visit_id IN (10001, 10006, 10010)
ORDER BY visit_id;
并检查表数据和索引的一致性:
ADMIN CHECK TABLE hospital_lab.patient_visit;
BR 输出恢复成功,并不代表验证已经结束。很多数据问题不会直接写进恢复日志,最终仍需要数据库检查和业务验证共同确认。
八、这次测试里几个容易忽略的点
1. 恢复之前先判断是否可以使用 RECOVER TABLE
刚发生不久的 DROP TABLE,如果数据仍处于 GC lifetime 范围内,RECOVER TABLE 通常更直接。
BR 更适合验证独立备份以及超过在线历史数据保留范围后的恢复能力。实际处理误操作时,应先确认发生时间和 GC 状态。
2. 使用 NFS 时,需要站在 TiKV 节点检查权限
运行BR的节点能够访问 NFS,不代表备份和恢复就一定没有问题。
SST 文件由 TiKV 读写,所以路径、挂载状态、运行用户和 UID 都需要在各 TiKV 节点核对。
3. 快照恢复只能回到快照时间点
这次备份后新增的两条记录没有恢复回来,属于正常结果。
如果业务要求尽量恢复到误操作发生前,而不是回到最近一次全量快照,就需要结合日志备份和 PITR。
需要注意,TiDB 7.5 的 PITR 是集群级恢复,需要恢复到新的空集群,不能直接把单张表恢复到指定时间点。
4. 基线最好在备份前就准备好
恢复完成后再临时考虑怎样验证,往往会缺少对照依据。
至少应提前保存表结构、数据量、关键汇总指标和部分样例数据。业务越复杂,恢复校验项越应该在备份策略和恢复预案中提前定义。
结语
这次测试的数据量很小,操作也不算复杂,但它让我对 TiDB 的高可用和数据恢复有了更清晰的区分。
三个 TiKV 副本可以帮助集群应对部分节点故障,却不会保存误操作之前的数据状态。BR 快照记录的是一个明确的时间点,恢复后的数据也只会回到这个时间点。
完成这次恢复后,我实际确认了几个过去只停留在文档层面的细节:备份数据由 TiKV 写入,NFS 权限需要在各节点检查,BR 可以从全量快照中筛选单表恢复,而恢复成功之后仍然需要准备明确的数据基线进行验证。