所有文章是按github的markdown格式书写的,如此页面显示不正常,请跳转到github版
PostgreSQL WAL解析与闪回的一些想法
1. 背景
最近在walminer基础做了不少修改,以支持我们的使用场景。详细参考
修改也花了不少精力和时间,这个过程中有些东西想记录下来,方便以后查阅。
所以,这篇东西有点像流水账。
2. WAL文件格式
解析WAL的第一步是要了解WAL的文件格式,我觉得最详细易懂最值得看的资料是下面这个。
- http://www.interdb.jp/pg/pgsql09.html
但是,以上还不够。细节的东西还是要看源码。我主要看的是写WAL记录的地方。
walminer作者李传成的博客里也有不少WAL解析相关的文章,是后来才发现的,我还没有看过。
- https://my.oschina.net/lcc1990
3. walminer的解析流程
walminer解析WAL的入口是pg_minerXlog()函数。其主要过程如下
- 加载数据字典
- 起点搜索解析阶段
遍历WAL,根据输入的起始时间和起始xid找到匹配的第一个事务。 这个阶段只解析事务类型的WAL记录,其他WAL记录快速跳过。 - 完全解析阶段
紧接着2的位置,继续往下进行完整的解析。 这个阶段,会收集所有FPI(FULL PAGE IMAGE)并反映它们的变更, 还会收集所有DML(insert/update/delete)类型的WAL记录, 并且在遇到事务提交WAL时输出该事务对应的DML,事务回滚时清空该事务对应的DML。
walminer把WAL记录中tuple变成SQL的过程比较有意思,中间用了一个VALUES的临时格式。
以下面这个UPDATE语句为例
update tb1 set c1='3xy' where id=3;
其解析过程中涉及到的一些调用点如下:
pg_minerXlog()
->sqlParser()
->XLogMinerRecord()
->XLogMinerRecord_heap()
->minerHeapUpdate(XLogReaderState *record, XLogMinerSQL *sql_simple, uint8 info)
1. 获取更新前后的tuple值(字符串格式)
->getTupleInfoByRecord()
->getTupleData_Update()
->mentalTup()
->mentalTup_nulldata()
->mentalTup_valuedata()
tupleInfo:VALUES(-3, '3x')"
tupleInfo_old:VALUES(3, NULL)
2. 生成中间redo SQL
->getUpdateSQL(sql_simple, tupleInfo, tupleInfo_old,...)
sql_simple:UPDATE \"public\".\"tb1\" SET VALUES(3, '3xy') WHERE VALUES(3, '3x')
3. 生成中间undo SQL
->getUpdateSQL(&srctl.sql_undo, tupleInfo_old, tupleInfo,...)
srctl.sql_undo:UPDATE \"public\".\"tb1\" SET VALUES(3, '3x') WHERE VALUES(-3, '3xy')"
4. 生成最终undo SQL
将中间中" VALUES"之后部分抹去,从rrctl.values,rrctl.nulls,rrctl.values_old,rrctl.nulls_old重新生成SQL后半部分。
->reAssembleUpdateSql(&srctl.sql_undo,true);
srctl.sql_undo:UPDATE "public"."tb1" SET "c1" = '3x' WHERE "id"=3 AND "c1"='3xy' AND ctid = '(0,10)';
pg_minerXlog()
->sqlParser()
->parserUpdateSql()
4. 生成最终redo SQL
->reAssembleUpdateSql(sql_ori, false)
sql_ori:UPDATE "public"."tb1" SET "c1" = '3xy' WHERE "id"=3 AND "c1"='3x';
4. walminer存在的问题
walminer是个非常棒的工具,填补了PG的一个空白。 但是,在我们准备把它推向生产时发现了一些问题。
- 资源消耗和解析速度
- 粗测了一下,解析一个16MB的WAL文件大概需要15秒。不得不说实在太慢了。
- 解析大量WAL文件还容易把内存撑爆。
- 正确性和可靠性
- 对并发事务产生的WAL记录,解析的结果不对。
- 缺少回归测试集
- 其他的小问题
- 易用性
- 不支持基于LSN位置的过滤
- 解析一次WAL要调用好几个函数,我觉得没有必要,一个就够了。
对这些已知的问题,都进行了改进。主要有下面几点
- 使用单个wal2sql()函数执行WAL解析任务
- 支持指定起始和结束LSN位置过滤事务
- 支持从WAL记录的old tuple或old key tuple中解析old元组构造where条件
- 增加lsn和commit_end_lsn结果输出字段
- 添加FPI(FULL PAGE IMAGE)解析开关,默认关闭image解析
- 优化WAL解析速度,大约提升10倍
- 给定LSN起始位置后,支持根据WAL文件名筛选,避免大量冗余的文件读取。
- 修复多个解析BUG
- 增加回归测试集 10.合并PG10/11/12支持到一个分支
修改后的walminer参考
https://gitee.com/skykiker/XLogMiner
后续希望这些修改能合到源库里。
5. 后续改进思路
walminer在功能和使用场景上和MySQL的binlog2sql是非常接近的。
binlog2sql对自己的场景描述如下:
https://github.com/danfengcao/binlog2sql
- 数据快速回滚(闪回)
- 主从切换后新master丢数据的修复
- 从binlog生成标准SQL,带来的衍生功能
binlog2sql已经有很多生产部署的案例,但是walminer好像还没有。 其中原因,我想除了修改版已经解决的那些问题,walminer作为闪回工具,还有进一步改进的空间。
我考虑主要有以下几点可以改进的
- 以fdw的形式提供接口
和函数相比fdw的好处是明显的- 不需要等所有WAL都解析完了再输出,因此可以结合limit进行多次快速探测
- 不需要创建临时表,解析过程中不需要产生WAL(产生WAL可能会触发WAL清理)。
- 可以在备库执行
使用fdw后,过滤条件直接通过where条件传递,接口更清晰。无法通过where条件传递东西,比如WAL存储目录,可以通过设置参数解决。
- 把解析过程分成事务匹配探测和完全解析2个部分
完全解析时,需要从匹配的事务往前回溯一部分,确保该事务的SQL甚至所需FPI都被解析到。 单纯从匹配的事务后面开始完全解析,会丢失SQL的。 - 增加DDL解析
其实并不需要解析出完整的DDL和逆向的闪回DDL,这个任务也很难实现。 只需要能知道什么时间,在WAL的哪个位点,哪个表发生了定义变更即可。 - 代码重构
从性能和可维护性考虑,有必要进行代码重构。 - 工具命名
既然这个工具的功能是从WAL中解析出原始SQL和undo SQL,walminer这个名称就显得不合适了。 因为从字面上理解,walminer应该是解析WAL本身包含的信息,包括很多与SQL无关的的信息, 但是不应该包含undo SQL这种WAL里没有而完全是被构造出来的东西。 所以,既然聚焦的功能是从WAL中提取SQL,这个东西可以叫wal2sql。就像MySQL的binlog2sql一样更加直观。
6. 参考
April 4, 2020