为什么要亲手删一次表
起因是我发现自己答不上来一个问题:这台机器上的 KES 真出了事,我多久能把数据找回来?配置里 archive_mode 是开着的,理论上全备加 WAL 归档就能做按时间点恢复(PITR,Point-In-Time Recovery)。可"理论上能"和"我亲手做成过",中间隔着一次真刀真枪的演练。
所以有了这篇文章:在一台鲲鹏云服务器(麒麟 V10 + KingbaseES V9)上建好备份防线,让业务正常写一阵,然后亲手 DROP 掉一张 30.7 万行的订单表,再把它按时间点捞回来。先把结果放这,免得你划到最后:132MB 的库全备 8 秒、增备 1.5 秒;表删掉之后按删除前一刻恢复,restore 本体 571 毫秒,30.7 万行一行不少,连增量备份之后才写进去、只存在于 WAL 归档里的 2000 行也回来了。过程里还踩了一个跟now()有关的坑,差点丢数据,后面细说。
整个演习的时间线长这样:
图 1 说明:从建立防线到验证恢复的全过程时间线,时刻与数字均来自图 4 至图 9 的实测输出。
环境
先交底。机器是华为云的 4 核 ECS,7.1G 内存,HiSilicon 鲲鹏(aarch64),系统麒麟 V10,数据库 KingbaseES V009R001C010 跑在 54321 端口,数据目录 /data,盘上还剩 27G 可用。备份工具用 KES 自带的 sys_rman,就在 Server/bin 里,不用额外装:
图 2 说明:麒麟 V10 aarch64,KingbaseES V009R001C010,sys_rman 同版本随库自带,系统盘可用 27G。
简单介绍下这个工具。sys_rman 是物理备份管理器,管全量/增量备份、备份保留策略、WAL 归档,也管按时间点恢复。它的组织方式是"仓库 + stanza":仓库是备份落盘的地方,stanza 是实例的配置单元,一个仓库可以同时管好几个实例。
搭防线
防线要干三件事:数据库开归档、sys_rman 配好仓库、跑一次端到端体检。
数据库这边改两个参数。archive_mode = on要重启才生效,我在实例初始化完就顺手设了;archive_command指到 sys_rman 的 archive-push,之后每个写满的 WAL 段会自动推进仓库:
archive_mode = on archive_command = '/opt/Kingbase/ES/V9/KESRealPro/V009R001C010/Server/bin/sys_rman --stanza=demo archive-push %p'sys_rman 这边的配置在/etc/sys_rman/sys_rman.conf,十来行。其中三行我要多嘴:start-fast=y,备份开始时立即做检查点,不等自然检查点周期。我一开始没设,全量备份跑了 158 秒,设上之后 8 秒,差的那 150 秒基本全在干等检查点。repo1-retention-full=2,只留最近两次全备,更早的自动清,不设的话每次备份都弹一条"仓库可能爆盘"的 WARN。日志开了双通道,控制台 info、文件 detail,出问题好翻旧账。
然后stanza-create初始化,check体检:
图 3 说明:归档参数、完整配置文件、stanza-create(28ms)与 check 全过程;check 主动触发了一次 WAL 归档并确认段文件落进 /kbbak/archive 目录,端到端链路验证通过。
check 这步别省。它不是读读配置就完事,是真触发一次 WAL 切换、真等归档落盘,把"数据库到仓库"这条管道整个过了一遍水。链路有毛病,这一步就炸出来了,总好过恢复的时候才发现备份是残的。
第一次全备
库里 300000 行订单,开备:
图 4 说明:--type=full全量备份,132.2MB、2811 个文件,8049ms 完成,备份标签 20260806-154957F;结束后 retention 策略自动检查(expire 6ms)。
8 秒出头,132MB 拿下。输出里我盯了两行:一行backup begins after the requested immediate checkpoint completes,这是 start-fast 在干活;另一行wait for all WAL segments to archive,备份收尾前会确保对应的 WAL 全部归档到位,备份集的自洽就靠它。
业务不停,增备跟上
真实系统不会为了备份停机。往 orders 里追加 5000 行已支付订单,总量到 305000,然后做增量:
图 5 说明:插入 5000 行后执行--type=incr,sys_rman 自动找到上次全备做基底(last backup label = 20260806-154957F),逐文件比对后只备份变化的 10 个文件共 27.5MB,1513ms 完成,标签 20260806-154957F_20260806-155157I。
增备的输出把机制摆得明明白白:逐个列出实际拷贝的文件,大头是 orders 的两个数据文件(20.4MB 加 6.6MB),剩下是些事务日志相关的小件。132MB 的库,增量只动 27.5MB,耗时从 8 秒缩到 1.5 秒。
info看一眼全景:
图 6 说明:一全一增两个备份集。全备在仓库占 21.8MB(132.2MB 压缩后),增备占 7.5MB;增备的 reference list 指向全备,恢复时会自动组合。
出事了
防线就位,现在制造事故。为了贴近真实,先让业务再写 2000 行待支付订单,总量 307000。注意这 2000 行的处境:它们在任何备份集里都不存在,只活在 WAL 归档里。然后,手一抖:
图 7 说明:追加 2000 行后,先用单独的一条select now()把当前时间记进 /tmp/rescue_point(16:13:22.897812+08),确认 307000 行在库,然后 DROP TABLE orders。再查,relation "orders" does not exist,30.7 万行没了。
图里那条单独执行的select now(),是我用血泪换来的写法。预演的时候我图省事,把now()跟 insert 塞在同一条命令里,结果恢复出来只有 305000 行,最后 2000 行没了。查了半天才想通:now()给的是事务开始时间,可按时间点恢复认的是事务的提交时间。同一个事务里,提交必然晚于now(),恢复目标点就落在了这笔提交之前,整批数据被排除。改成写入提交之后再单独取时间戳,坑就绕开了。
多说一句,真出事故的时候没人会贴心地帮你记好删除前的时间点。实际上这个时间来自业务报障、数据库日志里 DROP 语句的时间戳,或者 sys_stat_statements 的记录。来源不同,用途一样:给恢复找一个"事故之前"的落点。
捞回来
恢复三步:停库、restore、起库。
图 8 说明:确认恢复点文件内容后停库;sys_rman --stanza=demo --delta --type=time --target="$(cat /tmp/rescue_point | xargs)" --target-action=promote restore执行恢复,输出显示它自动选择增量备份集 20260806-154957F_20260806-155157I 作为基底(recovery will start at 2026-08-06 15:51:57),571ms 完成 2811 个文件的处理;随后一条 start,数据库回放 WAL 至目标点并提升对外服务。
参数拆开讲。--delta是在现有数据目录上做差异恢复,只重写有出入的文件,571ms 处理完 132.6MB 靠的就是它。--type=time --target=...指定按时间点恢复,目标就是刚才存在 /tmp/rescue_point 里的那个时间戳。--target-action=promote让数据库回放到目标点后直接提升为可读写。restore 自己会改好 kingbase.auto.conf 里的恢复参数,起库之后的回放不用人管。
有条 WARN 得如实交代。restore 一开始打了一行archived files within repo-1 have issues, check it as soon as possible,我翻了 detail 级日志,也没有更具体的上下文。本次恢复照常完成,数据全量验证通过(下一节)。恢复完我按提示跑了一次 check,新时间线上的归档链路正常:
INFO: WAL segment 000000020000000000000010 successfully archived to '/kbbak/archive/demo/12-1/0000000200000000/...' on repo1 INFO: check command end: completed successfully (126ms)碰到这类提示,恢复完跑一次 check、确认新时间线归档正常,处置上比较稳。
对账
恢复成功的标准不是"表回来了",是数据经得起对:
图 9 说明:count 返回 307000;按状态分组,pending 精确为 77000(原始 75000 + 删除前最后写入的 2000),paid 80000(原始 75000 + 增备前写入的 5000);timeline_id 变为 2;最后执行恢复后的新基线全备,20260806-161631F,8110ms。
总量 307000,说明恢复点选对了。pending 等于 77000,这是全文我最看重的一个数:那 2000 行待支付订单不在任何备份集里,只在 WAL 归档里存着,它们完整回来了,说明"备份打底、归档回放"这条链是真通的,不是纸面功夫。timeline_id 从 1 变 2,数据库在目标点分叉出了新的历史线,这是一次正经的时间点恢复该有的样子。
最后那次全备不是仪式感。恢复后数据库走上新时间线,老时间线上的增量链对新历史帮不上什么忙,第一时间重建全量基线,下一次出事才有干净的起点。这时候 retention 策略也开始转起来:新基线进来,最老的全备自动出局。
数字都在这
本轮演习的关键数字,同一环境同一数据集,出处都在对应截图里:
| 操作 | 耗时 | 数据量 | 仓库占用 | 出处 |
|---|---|---|---|---|
| 全量备份(300000 行时) | 8049ms | 132.2MB / 2811 文件 | 21.8MB | 图 4、图 6 |
| 增量备份(305000 行时) | 1513ms | 27.5MB / 10 个变化文件 | 7.5MB | 图 5、图 6 |
| PITR restore 本体(–delta) | 571ms | 132.6MB / 2811 文件 | — | 图 8 |
| 恢复后新基线全备(307000 行时) | 8110ms | 132.8MB / 2813 文件 | — | 图 9 |
| stanza-create / check / expire | 28ms / 秒级 / 6ms | — | — | 图 3、图 4 |
图 10 说明:左图为四类操作耗时(基线为 start-fast=y 配置,不开该选项时全量备份实测 158 秒);右图为备份数据量与仓库实际占用对比,gz 压缩后约为原始量的 1/6 至 1/4。数据来源:图 4、5、8、9 的 sys_rman 输出。
恢复完的数据对账单:
| 订单状态 | 恢复后行数 | 构成 |
|---|---|---|
| paid | 80000 | 原始 75000 + 增备前写入 5000 |
| pending | 77000 | 原始 75000 +仅存在于 WAL 中的最后 2000 |
| cancelled / refunded | 各 75000 | 原始数据 |
| 合计 | 307000 | 与删除前记录值一致,零丢失 |
出处:图 7(删除前)、图 9(恢复后)。
最后说几句
回到开头的问题:真出了事,多久能把数据找回来?这台机器现在有实测答案了。从进救援会话到数据库带着全部数据重新对外服务,分钟级,restore 本体半秒多,大头其实是人敲命令的时间。
策略上我也有了更具体的数。全备加增备这套组合,成本低到没理由不做:8 秒和 1.5 秒的动作对多数业务无感,仓库空间压到原始量的两成以下,retention 自动控制总量。按时间点恢复的边界也摸清了:能精确到微秒级时间戳,能救回只活在 WAL 归档里的数据;代价是目标点之后的写入全部作废。发现得越晚,作废得越多,所以它适合的是"误操作之后尽快发现"的场景。
坑也记下了两个。恢复点时间戳必须在目标事务提交后单独取,now()是事务开始时间,这个上面讲过了。restore 时那条归档 WARN 的确切含义,我到现在没从日志里挖到底,先按"恢复后 check 验证"处置着;后续版本要是能把 WARN 的原因打印得更具体些,排查会省不少事。
接下来我打算把这场演习脚本化,挂上定时任务做月度自动演练:自动起临时实例、恢复最近的备份、跑数据校验、出报告。备份可不可靠,不该靠"上次演练时是好的"撑着,得是"上个月的演练报告说它是好的"。