☰
KingbaseES误操作恢复实战:用WalMiner解析WAL日志生成UNDO SQL
2026/10/21 23:11:07 网站建设 项目流程

干这行最怕的一件事,就是凌晨两点的电话:“刚有个同事在生产库里跑错了一条update,没带where,整个表的数据全变了,能不能想办法回滚?”

备份恢复?全量备份在凌晨四点,增量备份到昨晚十点,中间这四小时的数据照样丢。闪回查询?有些场景根本闪不回去。这时候,能不能直接从最底层的WAL日志里,把当时的操作完整地挖出来,哪怕没有备份,也能把误操作还原成一条条看得见的SQL,再执行回去?

这就是金仓数据库KingbaseES里WalMiner要做的事。简单说,它就像Oracle的LogMiner,能读取数据库的WAL日志,把已经发生过的DML操作翻译成原始SQL,甚至生成UNDO SQL,直接把误删、误改的数据反向恢复回来。我先后在不同的国产数据库项目上折腾过这个工具,踩过不少坑,这次干脆把完整的使用过程整理出来,从原理、安装配置到实操恢复,再到各种常见报错的处理,一次性讲透。

1. 为什么要用WalMiner:误删数据后的最后一根救命稻草

1.1 闪回手段各有各的局限

先说说我遇到的典型案例。有一回是张订单明细表,总共也就八十多万行,某位开发在测试环境把主键当成筛选条件外的所有行都更新了一遍,直接把“订单渠道”字段全部改成了同一个值。发现的时候已经过了快三个小时。

当时我第一时间想到的方案就那么几个:

  • 从物理备份恢复,然后再做日志回放。问题在于,全量备份加归档日志都在对象存储里,拉回来加恢复,至少两三个小时起步,业务等不起。
  • 数据库自带闪回查询。金仓兼容Oracle的一些闪回特性,但普通开发库默认不一定开了对应参数,而且闪回查询对DDL和已提交事务的处理能力有限。
  • 手动写反向更新SQL。需要知道原来每一行的值是什么,但在连备份都不完整的情况下,没人能回答这个问题。

WalMiner恰好解决了这个死角:WAL日志记录的是数据库最底层的数据变更,哪怕数据文件已经改得面目全非,只要日志还在,就能解析出每一行数据在事务执行前后的样子,还原出“更新前”的值。这相当于把整个数据库的操作录像回放了一遍,还能随时暂停、倒带。

1.2 工具定位:不是替代备份,而是备份体系的最后一道保险

必须把话说清楚,WalMiner不是让你不备份的理由。备份仍然是第一位的,但备份永远存在“恢复点目标”的问题,备份之后到事故发生时这段时间的日志,是靠归档日志和在线日志来补的。如果归档断了,或者在线日志被覆盖了,WalMiner再强也读不出已经被覆盖的WAL文件。

所以更准确的说法是:WalMiner解决的是“备份可用,但备份点到故障点之间有数据断层”的问题,以及“没有做过闪回配置、但WAL日志还在”的问题。

我在实践里一般把它当成审计和事故追查的第二道防线来用,而且最好在系统上线时就提前部署好,不要等出了事才想起装插件。

2. WalMiner的核心原理:它是怎么从日志里“变”出SQL的

2.1 WAL日志里到底存了什么

理解WalMiner,得先理解WAL日志的记录机制。金仓KingbaseES和PostgreSQL同源,数据页的每次修改,不管是insert、delete、update,还是create index这种DDL操作,都会产生对应的WAL记录,里面包含哪些信息?

  • LSN(Log Sequence Number):日志的唯一序列号,相当于日志文件的偏移地址,它标明了每条变更记录在WAL日志流中的精确位置。
  • 事务号(Transaction ID):标识这条记录属于哪一个事务。
  • 数据页信息:被修改的页面编号、块内偏移量。
  • 变更前后的tuple数据:对于update操作,WAL记录里同时包含旧版本的tuple数据和新版本的tuple数据;对于delete,包含被删除行的数据;对于insert,包含新插入行的数据。

这里有一个关键点:WAL日志不是为了“恢复现场”而存在的,它存在的首要目的是崩溃恢复。但正因为它在物理层面把足够多的信息记录了下来,我们才能通过逻辑解析,把它翻译回SQL语句。

2.2 从日志记录到可执行SQL的重放逻辑

WalMiner的解析流程,本质上做了这样几件事:

第一,扫描WAL文件,逐条读取WAL记录。第二,过滤出与数据变更相关的记录类型,跳过检查点、页面校验等内部记录。第三,根据记录中的tuple信息,还原出一条逻辑SQL。

比如update操作,日志里有旧值和新值,WalMiner就能生成两条SQL:

  • 真正的原始SQL,即“当时执行的那条update语句”
  • 反向SQL,即“把这个update撤销掉”的语句,也就是把新值改回旧值的update

同理,delete操作会生成对应的insert作为反向SQL,insert操作会生成delete作为反向SQL。这就是为什么它能用来做误操作恢复。

我打个比方,就好比你客厅里装了摄像头,普通情况下你不会留意那段录像,但当家里东西被挪动了,你把录像倒回去,就能看清是谁在什么时间动过什么地方。WalMiner就是那个能倒放的播放器,而且还能把画面里的动作翻译成一步一步的操作指令。

2.3 与Oracle LogMiner的异同点

有过Oracle经验的DBA,上手WalMiner会非常快,因为概念几乎是一一对应的。Oracle有redo log,金仓有WAL;Oracle有LogMiner包来分析redo,金仓有WalMiner来分析WAL;Oracle的LogMiner能生成undo SQL,金仓的WalMiner同样能生成undo SQL。

但两者的差别也不少:

  • Oracle的LogMiner有两种模式,一种在线分析,一种从归档分析;WalMiner同样可以加载在线WAL和归档WAL,但加载方式更接近文件级别,需要你告诉它日志目录在哪。
  • 金仓的具体SQL方言和Oracle不一样,所以生成的SQL在细节上也会有差异,比如函数名、类型转换写法。
  • 对中文和字符集的处理上,金仓更贴近国内使用习惯,解析出来的中文字段值基本不会乱码,但前提是数据库的字符集设置正确。

想理解WalMiner的语法,不必完全照搬Oracle经验,把它当成一个独立的日志解析工具来学,反而少走弯路。

3. 安装与初始化:把WalMiner装进KingbaseES

3.1 安装前的版本与参数检查

这是最容易踩坑的第一步。WalMiner和数据库内核版本强相关,如果你下载的插件版本和数据库版本对不上,最常见的结果就是create extension直接报错,或者装上了但解析日志时出现莫名奇妙的崩溃。

我建议按这个顺序做检查:

  1. 确认KingbaseES的大版本号和小版本号,比如V8系列和V9系列的插件包不通用。
  2. 确认内核是否支持WalMiner,有些定制版内核已经内置了相关功能,有些则需要在官网或代码仓库单独获取扩展包。
  3. 确认编译环境,如果从源码编译插件,需要和数据库二进制配套的pg_config工具,避免头文件版本不一致。

另外,还有一个关键的数据库参数需要提前确认:wal_level。

在PostgreSQL生态里,wal_level有minimal、replica和logical三个级别。WalMiner要完整解析出所有DML操作,尤其是要还原UPDATE的旧值,必须保证WAL日志里记录了足够详细的行数据。如果数据库的wal_level是minimal,很多事务的旧值信息会被省略,解析结果会残缺不全,恢复的可靠性就得打折扣。

提示:如果你已经决定要使用WalMiner做事故追查,建议把wal_level设置为logical或者至少replica。logical级别产生的日志量比replica大一些,但信息最完整。如果对日志量敏感,replica级别通常也够解析DML了。可以先用测试环境做一次完整演练,对比解析结果的完整性,再决定生产环境用哪个级别。

3.2 插件部署实操步骤

部署的完整步骤大概是这样的,我以常见的Linux环境为例:

  1. 把插件包上传到服务器,解压到数据库安装目录的lib和share/extension目录下。注意路径一定要匹配当前实例的运行用户,别放到别的用户的目录下。
  2. 编辑数据目录下的配置文件kingbase.conf,修改或追加以下内容:
wal_level = logical shared_preload_libraries = 'walminer'

这里有一个细节,shared_preload_libraries如果原来已经有其他插件,要用逗号隔开追加,不要直接覆盖掉原来的配置。修改完配置后重启数据库实例,否则预加载不生效。

  1. 重启后,用超级用户连接到数据库,执行创建扩展命令:
CREATE EXTENSION walminer;

如果这一步不报错,说明插件加载成功了。这时可以验证一下插件是否出现在当前数据库中:

SELECT * FROM pg_extension WHERE extname = 'walminer';

正常情况下会看到一行记录。如果CREATE EXTENSION报错,比如提示找不到控制文件,优先检查插件文件是否真的复制到了extension目录,以及目录权限是否允许读取。

3.3 日志保留参数怎么配才不容易出事

装好插件只是第一步,真正让WalMiner在关键时刻能派上用场,日志保留策略才是重头戏。我见过太多案例是:插件早就装好了,但等真出事了,发现该覆盖的WAL日志已经没了,啥也解析不出来。

在线日志的数量由这些参数控制:

参数作用我的推荐值
wal_keep_size保留在线WAL的最小容量,防止日志被过早复用建议结合实际业务量,至少保留24小时以上的日志量
max_wal_size触发自动检查点前WAL增长的目标值可以适当调大,减少检查点频率,让日志存活时间更长
archive_mode是否开启归档强烈建议开启,归档日志是WalMiner可靠工作的基础
archive_command归档命令配合成熟的备份工具或简单地拷贝到归档目录

有一点非常容易被忽视:就算你开启了归档,如果归档命令本身经常失败,或者归档目录磁盘满了,日志照样断档。我一般会加一个监控脚本,定时检查归档目录的大小、归档文件的时间戳,一旦发现最近几分钟没有新的归档文件生成,马上告警。

同时,我还建议设置log_min_messages在测试阶段开到debug1,确认WalMiner在解析时到底加载了哪些文件、有没有跳过日志段,这样可以快速定位“解析结果少了一段”的问题。生产环境再调回warning即可。

4. 上手实操:一次完整的误删除恢复演练

4.1 构造测试环境与误操作场景

为了演示,我在测试库里建了一张模拟业务表,名字就叫test_orders,表结构如下:

CREATE TABLE test_orders ( order_id bigint PRIMARY KEY, product_name varchar(200), order_amount numeric(12,2), create_time timestamp DEFAULT now() );

插入一批测试数据,模拟正常业务的发生过程。这里有个细节,建议把每次插入的时间节点记录下来,这样后面做时间过滤时,可以更精确地验证WalMiner解析结果。

INSERT INTO test_orders (order_id, product_name, order_amount, create_time) SELECT generate_series(1, 5000), '产品-' || generate_series(1, 5000), (random() * 1000)::numeric(12,2), now();

然后模拟一次典型的误操作:把整张表的product_name字段统一改成了一个固定值,相当于业务人员在update时忘了加where条件。

UPDATE test_orders SET product_name = '误操作统一值';

事故发生之后,我们先别急着做任何恢复动作,先记录一下当前可以得到的有效LSN范围。WalMiner通常会提供一个函数用来查看当前WAL文件列表,我可以用它确认日志没有被覆盖:

SELECT * FROM walminer_walfile_list();

结果会列出服务器上当前可用的WAL文件路径、起始LSN、结束LSN、时间范围等信息。这一步非常关键,它能直观地告诉你:事故发生时的日志,到底还在不在服务器上。

4.2 初始化WalMiner并加载日志

使用WalMiner的第一步,是初始化工作目录和元数据信息。不同版本的函数名称略有差异,我这边常用的是:

SELECT walminer_init();

它会创建一张结果表(通常叫walminer_contents,具体以版本为准),用来存放后续解析出来的SQL记录。如果之前已经执行过,重复执行可能会报“已存在”之类的提示,可以用清理函数先重置,例如:

SELECT walminer_clean();

初始化完成后,就可以加载WAL日志了。加载有两种常见方式:

  • 按LSN范围加载:适合明确知道事故发生在哪个时间段、哪个LSN区间的情况。调用类似walminer_load(起始LSN, 结束LSN)的函数。
  • 按WAL文件路径加载:适合直接指定一个具体的日志文件或目录。

我在实操中更推荐先查一下待加载日志的时间范围,再用LSN范围加载。这样做的好处是,解析出来的结果天然就限定在目标区间内,后续筛选数据的工作量小很多,也不会把无关事务混进来。

4.3 解析WAL日志并查看结果

日志加载完成后,执行解析函数,把WAL中的变更记录转换成SQL:

SELECT walminer_execute(开始LSN, 结束LSN);

执行完之后,就可以去结果表里查数据了。结果表的核心字段通常包括:

  • sql_type:SQL类型,是INSERT、UPDATE还是DELETE
  • table_name:对应的表名
  • op_text:原始SQL,即还原出来的原执行语句
  • undo_text:反向SQL,用于撤销该操作
  • xid:事务号
  • timestamp:事务提交时间

为了验证,我可以先初步整个表,看看目前的解析结果规模:

SELECT count(*), sql_type FROM walminer_contents GROUP BY sql_type;

理论上,更新5000行会产生5000条UPDATE的op_text记录,同时也有5000条对应的undo_text记录。

我还可以进一步确认解析结果是否精确包含了刚才的误操作时间点。最直接的办法,是查时间戳范围和事务号范围,确认里面是否有我执行UPDATE test_orders SET product_name = '误操作统一值'这个事务的痕迹。

4.4 用UNDO SQL把数据救回来

走到这一步,恢复方案就有了。我们只需要把undo_text字段里的SQL拿出来执行,就可以把这5000行的product_name恢复成原来的值。

先把恢复SQL导出到一个文件,审查一下再执行,我一般是这样操作的:

SELECT undo_text FROM walminer_contents WHERE table_name = 'test_orders' ORDER BY lsn DESC;

把输出结果拷贝成SQL脚本,执行前务必注意几点:

  1. 确认undo_text里只有UPDATE语句,没有混入别的业务事务的变更。如果你加载的LSN范围过大,可能会把误操作前后无关事务的SQL也带出来,需要再按时间或者事务号做一轮过滤。
  2. 执行前最好先统计目标表的行数、抽样查看几条恢复后的结果,不要在没验证的情况下直接跑全量脚本。
  3. 如果原表上有外键、触发器、序列等依赖对象,恢复顺序要慎重。比如触发器可能因为UPDATE再次触发产生新的业务副作用,必要时可以临时禁用触发器和外键约束,恢复完成后再启用。

我实际执行时,会把脚本拆成小批次,比如每次只恢复500行的数据,观察一下没有异常,再继续跑下一批,防止一次性大批量执行引发锁竞争或者阻塞正常业务。

恢复完成后,直接验证:

SELECT product_name, count(*) FROM test_orders GROUP BY product_name;

如果看到数据已经还原成了原来的各种不同产品名称,而没有统一的“误操作统一值”了,说明恢复成功。

注意:整个过程里,我建议先用测试库完整演练一遍,确认你的WalMiner函数名称、结果表字段、UNDO SQL的执行方式与你的版本完全匹配后,再考虑在生产环境复用同一套流程。千万不要在生产环境第一次使用WalMiner,那样风险很大。

5. 典型场景:除了误删恢复,WalMiner还能做这些事

5.1 事务审计与操作追查

误操作恢复只是最紧急的场景。日常运维中,还有一类非常头痛的问题:是谁在什么时候改了某张表的数据?改完之后又改回去了,两次操作之间发生了什么?

这类问题靠应用日志很难查,因为应用可能有自己的日志级别、不一定记录了SQL参数;靠数据文件更查不了,因为数据文件只保留最终状态。但WAL日志把每一次变更都记下来了。

具体操作上,只要锁定目标表和时间段,执行WalMiner解析,再在结果表里按table_name过滤,就能看到该表在目标时间段内的所有DML记录。再结合应用账号、连接信息等外部日志,基本可以还原出完整的操作链路。

我做过一次数据完整性追查:业务反馈某个统计报表连续几天的数据对不上,查了半天都定位不到原因。后来用WalMiner把涉及报表基础表的变更记录全导了出来,发现有一条在凌晨跑的批处理脚本,因为参数解析错误,把筛选条件传成了空值,相当于把全表重新刷了一遍。从WAL解析结果里能直接看到那段时间的UPDATE语句数量陡增,瞬间定位到了问题源头。

5.2 表结构变更追踪与合规取证

WalMiner不只支持DML解析,对于DDL操作,只要WAL日志里记录了,也能看到相关记录。这在合规审计场景非常有用。

比如有部门提出:某张核心业务表在过去两个月是否被调整过字段类型?是否有人删除过索引?这些变更如果没走正规的变更流程,普通审计手段很容易漏掉。但通过WAL日志解析,可以查看到对应时间点是否有DDL操作,以及具体是什么操作。

我常用的做法,是把WalMiner导出的结果定期归档到审计表里,保留一个独立的日志解析副本。这样即使未来在线WAL和归档WAL都清理了,审计记录仍然存在。

5.3 测试数据快速修复

还有一种场景容易被忽略:测试环境。测试同学经常把一套环境的数据造得乱七八糟,想恢复初始状态,但重新从生产导一份全量数据又太重。

如果在一开始就做好基线快照,并且保留WAL日志,测试过程中出现的各种脏数据,就可以通过WalMiner精确定位到某次测试脚本产生的变更,然后生成UNDO SQL把数据还原到测试开始前。虽然不是所有情况都适用,但至少为测试数据管理多了一种手段。

6. 常见问题与排查经验

6.1 日志已被覆盖,WalMiner什么都解析不出来

这是最让人头大的一类问题,通常的报错是“无法读取WAL文件”或“找不到对应的日志段”。大多数情况下,是因为在线WAL被循环复用、归档又没接上。

如果事故已经发生,那就没办法了。我们能做的只有事后优化:

  • 把wal_keep_size调大到至少容纳24小时的日志量。这个参数说白了就是“宁可磁盘多占点,也要让日志多活一阵”。
  • 确保归档命令绝对可靠,归档目录磁盘要监控。我见过因为目标盘满了,归档命令持续失败了两三天没人发现的案例。
  • 有条件的话,用带延迟恢复的备库作为日志缓冲池。金仓支持备库,备库可以从主库持续拿WAL,即使主库已经覆盖了在线日志,备库上的WAL还在。真出了事,可以从备库上把日志拷出来分析。

我个人的经验是:凡是上了WalMiner的生产环境,至少要把wal_keep_size设置为“日常24小时日志量”的大小,再配合一个每10分钟检查归档新鲜度的脚本,双保险。

6.2 CREATE EXTENSION报错或解析时崩溃

这类问题多半是版本不匹配,要么是插件版本和数据库内核版本不一致,要么是编译插件时用的头文件版本与数据库二进制不一致。

排查思路:

  1. 先确认数据库版本和插件版本是否匹配,查看官方版本兼容性说明。
  2. 查看数据库日志里有没有加载插件的详细错误。tail -f kingbase.log看一遍,往往比瞎猜有用得多。
  3. 如果是从源码编译的插件,确认pg_config --version输出的版本和当前数据库一致。
  4. 确认系统库路径有没有污染,比如LD_LIBRARY_PATH里混入了别的数据库的库文件,导致加载了错误的动态库。

还有一个小坑:有些版本的WalMiner要求以超级用户或者指定权限的账号来执行walminer_init(),普通业务账号即使有表的DML权限,也没有权限创建扩展和初始化表,所以建议用运维专用的高权限账号操作。

6.3 解析结果有乱码或中文显示异常

WAL日志里存的是二进制tuple数据,WalMiner在解析时需要依赖数据库的字符集设置把它转换为可读的SQL文本。如果你连接的数据库字符集和实际存储数据的字符集不一致,就容易出现中文乱码。

解决办法很简单:用与源库相同字符集的数据库连接做解析,或者把结果先导出成文本再在正确字符集的终端里查看。

我遇到过一种情况是,使用图形化客户端时,客户端的显示字符集和数据库会话字符集不一致,结果SQL在客户端上显示成乱码,但导出为文件后内容又是正常的。遇到这种问题,先别急着判断解析出错,换一个纯命令行环境验证再说。

另外,如果业务表里本身存储的是十六进制或者二进制类型的数据,WalMiner可能无法把它翻译成可读的字符串,解析结果会显示成\x开头的一串十六进制内容。这是正常现象,不代表工具坏了,处理这类字段时需要手动转码。

6.4 大事务解析后结果量巨大,搞崩了临时表

有次我解析一个跑了十几分钟、涉及百万行数据的大事务,结果表直接膨胀到了几GB,临时表空间都快写满了。WalMiner是逐条记录解析的,大事务的每条变更都会生成一条记录,所以结果量非常大。

应对策略:

  • 尽量用LSN范围精确匹配目标事务,不要把整个WAL目录全加载进来再筛选。
  • 定期执行walminer_clean()清理之前的结果表,避免上次解析的数据占用空间。
  • 如果磁盘和内存都比较紧张,可以把解析结果直接导出到外部文件,不要在结果表里做全量聚合。先用数据库查询把目标SQL导出来,再用外部工具做后续处理。
  • 执行解析的主库,如果IO负载已经很高,建议先从备库拷贝WAL日志到单独环境解析,避免影响在线业务。

6.5 时间范围过滤不准确,解析出无关事务

网上很多教程会告诉你用时间来过滤,但WAL日志记录的时间,不同版本、不同记录类型可能会有细微偏差,尤其是事务的提交时间和语句实际执行时间不完全等价。如果你按时间范围解析,容易把前后几秒的邻近事务也带进来。

我最常用的过滤维度是LSN和事务号,而不是时间:

  1. 先从备份或pg_switch_wal()等操作确定大致LSN起点。
  2. 加载日志后,先用结果表查看每个事务的xid和涉及的表,快速定位目标事务。
  3. 再针对目标事务生成UNDO SQL,不盲目按时间整段恢复。

这样做的好处是,恢复的边界非常清晰,不会误伤其他正常事务。如果你确实只知道时间,那就把时间范围适当收紧,宁可多加载一段日志,然后根据事务号再二次过滤,也不要只靠时间一刀切。

7. 一点实战心得

用WalMiner这几年,我最大的感受是:这个工具真正考验人的地方不在工具本身,而在于你平时有没有把日志准备到位、有没有在安静的时候演练过整套流程。

我见过很多团队,把WalMiner装上去就再也没碰过,等出事了才翻出手册一边查函数一边操作,结果不是版本对不上,就是日志已经被覆盖,只能眼睁睁看着数据丢了。所以我特别建议:每个上了金仓数据库的项目,至少每个季度做一次“误删数据恢复演练”,把误操作、解析日志、生成UNDO SQL、回滚验证这套流程从测试环境完整走一遍,并把其中的函数、参数、输出格式整理成团队内部的应急预案文档。

哪怕你永远用不到它,这个预案放在那里,遇到任何紧急的数据变更问题,你心里都有一条后路。这比任何高端架构都实在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询