alitrack

MariaDB 的 DuckDB 引擎,备份成功了,数据没进去

上一篇写的是速度:同一份报表,把订单表和明细表挪到 DuckDB 存储引擎,快 22 倍。文章发出去,有位读者留言问了一句:「这很实用。备份要不要改?」

我把备份、恢复、日志这三条链路在官方镜像里跑了一遍。答案是要改,而且是静默漏掉的那种——mariabackup 跑完 exit 0、日志结尾 completed OK!,DuckDB 表的数据一个字节都没进备份。

链路
现状
物理备份(mariabackup)
静默漏掉 DuckDB 表的数据
逻辑备份(mariadb-dump)
能用,恢复端要装插件
binlog 行式日志
UPDATE/DELETE 直接报错,工单未修
复制(从库)
有两条未修工单,本文不背书

● ● ●

mariabackup 说它成功了

跑的就是标准命令,一个参数都没少:

mariabackup --backup --target-dir=/tmp/bk2 --user=root

退出码 0,日志 441 行,结尾是那句熟悉的 completed OK!。441 行里,除了一张表的 .frm 被拷走,没有任何一处提到 DuckDB。

备份目录总共 309 MB。InnoDB 那两张对照表一份没少:orders_innodb.ibd 67 MB、items_innodb.ibd 155 MB。DuckDB 的两张表,只有 orders_duck.frm(1521 字节)和 items_duck.frm(1216 字节)——表结构的描述文件进去了,数据文件没进去。

源侧的现场是这样:

文件
大小
duckdb.db
46,673,920 字节(约 44.5 MiB)
duckdb.db.wal
48,207,621 字节(约 46 MiB)

这两个文件加起来 90 MB,在备份里连一个字节都没有。

mariabackup 备份里有什么

mariabackup 备份里有什么

原因不复杂:这个引擎把所有 DuckDB 表的数据都放在 datadir 根目录下的单一共享文件里(引擎自带的架构文档写着,DuckdbManager::CreateInstance() 会在数据目录里打开一个 duckdb.db)。mariabackup 是按「库目录里的表文件」枚举拷贝的,这个文件既不属于任何一张表,也不在任何引擎的文件清单里,于是就被跳过了。

我把这份备份还原成一个新实例,看看到底丢到什么程度:

  • InnoDB 对照表 bench.orders_innodb:1,000,000 行,完好。
  • DuckDB 表:SHOW CREATE TABLE 正常(因为 .frm 在),一查就报 ERROR 1296: Got error 122 'Catalog Error: Table with name orders_duck does not exist!'。重启之后,数据目录里只有一个引擎新建的空壳 duckdb.db,12,288 字节。

表定义在、数据不在,这是最难受的一种失败:监控看不出来,备份日志看不出来,等到真去恢复才发现。

● ● ●

逻辑备份这条路能走

mariadb-dump 是能带走的:

mariadb-dump -uroot --databases bench --result-file=/tmp/dump_bench.sql

导出的 SQL 是 212,526,496 字节(212 MB),里面 CREATE TABLE ... ENGINE=DUCKDB 原样保留,数据是一条条 INSERT。先删掉恢复端那两个空壳表,再回灌,11 秒跑完,orders_duck 1,000,000 行、items_duck 2,000,000 行,逐一对上。

按行重灌,体积会比列式原文件大得多,这个代价得认。另外两个坑:

恢复端必须带 ha_duckdb 插件。 dump 文件里的建表语句写着 ENGINE=DUCKDB,目标实例没有这个插件,表根本建不出来。备份能不能恢复,取决于恢复端有没有那个插件——这件事值得在备份清单上单独写一行。

数据默认先落在 WAL 里,只拷主文件等于拷空壳。 回灌完之后我去看了一眼:主文件 duckdb.db 还是 12 KB,duckdb.db.wal 涨到了 96 MB。引擎的 WAL 攒到 256 MB 才自动落盘(这是 duckdb_checkpoint_threshold 的默认值),所以任何文件级拷贝都必须把 .db 和 .db.wal 一起抓,而且要在停库之后、或者执行过 SELECT run_in_duckdb('FORCE CHECKPOINT') 之后再抓。

● ● ●

binlog_format=ROW 下,改一行数据都报错

备份之外,我把 DML 也按矩阵跑了一遍,每格一个独立容器:

binlog_format
表上有触发器
UPDATE / DELETE
默认
否
正常
STATEMENT
否
正常
MIXED
否
正常
MIXED是报 1031
ROW
否
报 1031
ROW是报 1031
DuckDB 表上 UPDATE/DELETE 的触发矩阵

DuckDB 表上 UPDATE/DELETE 的触发矩阵

报错原文是这句:

ERROR 1031 (HY000): Storage engine DUCKDB of the table `m`.`t1` doesn't have this option

同一台容器里,同样的语句打在 InnoDB 表上完全正常,所以不是语句写法的问题。log_bin 关着,只在会话里 SET binlog_format=ROW,一样触发。

触发器那一格是这轮跑出来的新条件:只要表上有触发器,MIXED 也会踩进去。MariaDB 的开发者在这个工单的讨论里提过一句,「你环境里的某些因素会强制走 ha_delete_row 调用,而不是更可取的直接删除」——触发器正好就是那个因素。

还有两格更麻烦:

  • DELETE FROM
    (全表删除)在行式路径下报 1296 Failed to append: NOT NULL constraint failed: invoice.id,删完之后行数还是 3,一行都没删掉。
  • 参数化的 DELETE ... WHERE id=@k 报 1296 解析错误,DuckDB 把 =@ 当成了一个算子。

翻到源码,机制是清楚的。sql/handler.cc 里有一段把 HA_ERR_WRONG_COMMAND 翻译成 ER_ILLEGAL_HA(也就是 1031)的代码,而插件里会返回这个错误码的只有几处:index_read_map、index_next 这些索引读接口,还有 rnd_pos()——它的 position() 干脆是个空实现。也就是说,服务器一旦需要按位置回读行(行式日志要构造前后镜像),插件缺的能力就暴露成 1031;不需要回读时,走的是直接下推文件删除的快路径,一切正常。

这不是我们撞上的孤例。JIRA 上有一条标题几乎逐字对应的工单:MDEV-40831「UPDATE/DELETE statements on ENGINE=DuckDB fail when binlog_format is set to ROW」,状态 Open,影响 11.4、11.8、12.3、13.0、13.1。它的复现步骤和我上面那格一模一样。

顺带说一句复制。MDEV-40203(从库重启之后不再应用事务)、MDEV-40178(从库 RSS 一直涨到 OOM)都还是 Open——这两条我只引工单,没自测,也就不替它下结论。真要把这引擎放到从库上,先自己压一遍。

大多数生产库为了数据安全是按行式配的,所以上这套引擎之前,值得先查一眼 @@binlog_format。

● ● ●

三条路怎么选

DuckDB 表的备份:三条路线

DuckDB 表的备份:三条路线

逻辑备份:已经验证闭环。代价是恢复端要有插件,体积按行放大,几百万行的表回灌按秒到分钟算。

文件级拷贝:duckdb.db 和 duckdb.db.wal 同时抓,停库或强制 checkpoint 之后再抓。这条路对上了备份窗口就不好安排——一个和主文件等大的 WAL,拷它就是多拷一份数据。

按派生数据来管:这是我现在倾向于的做法。这些表如果是只读快照,真正要备份的东西是「能重新生成它的那件事」——源 InnoDB 表、快照 SQL、调度,duckdb.db 只是它的产物。恢复流程变成:先把 InnoDB 恢复起来,再把快照重放一遍(官方那个 Magento 实验里,300 万行快照建好要 34.5 秒)。前提是这些表确实只读、可再生;一旦你开始往 DuckDB 表里写业务数据,它立刻变成必须单独备份的一等公民,前两条路就绕不过去了。

把这三条路放到一起看,其实是一句更通用的话:派生数据不值得被备份,值得被重建。要备份的是生成它的那份作业。这条规则不挑引擎,MySQL 9.7 那边的 DuckDB 引擎、PostgreSQL 的 pg_duckdb,以后再有别的列式引擎挂进交易库,都会遇到同一个问题——引擎自带的数据文件、日志格式、行式日志路径、复制行为,和原来那套备份体系是不是对得上。

绕回开头。那位读者问的是「备份要不要改」,我现在的答案是:事务表那一半一个字都不用改;DuckDB 表这一半,要么按逻辑备份单独排一条链,要么干脆把它们当可再生数据来管。我拉了官方文档的限制清单看,它写满了这个引擎不支持哪些 SQL 特性,唯一没写的一行,是它不支持你现在的备份和日志流程。

这个引擎我现在只会用来放可再生快照,不会往里面写业务数据。要试的话,顺序别搞反:先验备份和 binlog,再谈提速——性能掉一档是慢一点,恢复不出来就是另一回事了。

你们那套库的 @@binlog_format,是 ROW 还是默认的 MIXED?

参考来源:

  1. 01
    MariaDB 能把订单表换成 DuckDB,报表快 22 倍
  2. 02
    MariaDB 文档 DuckDB 存储引擎:https://mariadb.com/docs/server/server-usage/storage-engines/duckdb-storage-engine.md
  3. 03
    MariaDB 源码 storage/duckdb 的架构说明:https://github.com/MariaDB/server/blob/11.4/storage/duckdb/docs/architecture.md
  4. 04
    MariaDB 源码 storage/duckdb 的空间回收说明(WAL 阈值与 FORCE CHECKPOINT):https://github.com/MariaDB/server/blob/11.4/storage/duckdb/docs/space-reclaim.md
  5. 05
    MDEV-40831 UPDATE/DELETE statements on ENGINE=DuckDB fail when binlog_format is set to ROW:https://jira.mariadb.org/browse/MDEV-40831
  6. 06
    MDEV-40203 after restart, I am not able to do replication:https://jira.mariadb.org/browse/MDEV-40203
  7. 07
    MDEV-40178 OOM at secondary node of replication because of growing RSS:https://jira.mariadb.org/browse/MDEV-40178
  8. 08
    mariabackup 文档:https://mariadb.com/kb/en/mariabackup/
  9. 09
    本地实跑:ghcr.io/mariadb/duckdb:11.4(MariaDB 11.4.13),复现脚本见 claw 仓库 wechat-articles/cases/duckdb-2026-09-18-scripts/