了解PostgreSQL逻辑复制中的历史快照

  • A+
所属分类:安全开发

微信又改版了,为了我们能一直相见

你的加星在看对我们非常重要

点击“长亭安全课堂”——主页右上角——设为星标🌟

期待与你的每次见面~




01
前言




基于 PostgreSQL 12.0 的代码实现。初步认识在逻辑复制中所使用的历史系统表快照(Historic Catalog Snapshot)。这部分的代码位于 src/backend/replication/logical/snapbuild.c。PostgreSQL 拥有十分良好的系统使用手册与源代码注释,为了尽可能避免让系列文章成为堆砌注释翻译及其源代码的“源码分析”类文章,一般文档对于关键性细节和重要实现逻辑都有所描述的,本文将不再进行赘述。因此推荐配合 PostgreSQL 源码及其注释一同进行阅读,相信读者能够从源码和系统使用手册中收获颇丰。


01
逻辑复制


使用数据库的流复制(Stream Replication)进行主从同步相信对于使用 PostgreSQL 的 DBA 或是系统开发者而言并不陌生。而从 9.4 版本开始,PostgreSQL 又正式支持了逻辑复制功能(Logical Replication),关于主从同步的更多不同的解决方案比较的扩展阅读可参阅这篇官方手册(www.postgresql.org/docs/12/different-replication-solutions.html)。我们目前在 HA 下的数据库主从同步便是使用了逻辑复制的方案,可以更为灵活的复制特定的表,自定义分发规则等。


更为复杂的情况下逻辑复制可以自行开发逻辑解码插件,文档中称为 Output Plugin。除了插件的初始化、自定义选项外,主要实现 src/include/replication/output_plugin.h 中定义的 OutputPluginCallbacks。参考这篇官方示例(www.postgresql.org/docs/12/logicaldecoding-example.html),我们可以使用 test_decoding 来进行一些实验,方便地观察逻辑复制的基本操作流程和效果等。一般我们使用 PostgreSQL 标准的解码插件 pgoutput 做同步。


02
从一个问题入手


起初在我们开始使用 PostgreSQL 提供的逻辑复制功能进行开发时,执行下列 SQL 分别创建复制槽及一个 Publication:


SELECT * FROM pg_create_logical_replication_slot('ha_sync_sub','pgoutput'); 

CREATE PUBLICATION ha_sync_pub FOR ALL TABLES;

从数据库进行订阅:


CREATE SUBSCRIPTION sub CONNECTION 'port=5432' PUBLICATION ha_sync_pub WITH (create_slot=false, slot_name=ha_sync_sub);

两个数据库开始同步时出现了错误:


2019-03-04 08:26:09.999 UTC [2811] LOG: logical replication apply worker for subscription "ha_sync_sub" has started

2019-03-04 08:26:09.999 UTC [2811] LOG: logical replication apply worker for subscription "ha_sync_sub" has started

2019-03-04 08:26:10.005 UTC [2811] ERROR: could not receive data from WAL stream: ERROR: publication "ha_sync_pub" does not exist CONTEXT: slot "ha_sync_sub", output plugin "pgoutput", in the change callback, associated LSN 0/1AC2070

2019-03-04 08:26:10.006 UTC [1] LOG: background worker "logical replication worker" (PID 2811) exited with exit code 1

同步无法继续进行,日志说明其错误发生在 pgoutput 插件的 LogicalDecodeChangeCB 回调中,找不到我们先前定义的名为 “ha_sync_pub” 的 Publication。可我们通过查询 pg_publication 表发现该条目是存在的:


postgres=# select pubname from pg_publication;

ha_sync_sub

这是为什么呢?




02
系统表历史快照




逻辑复制过程需要构建一种历史快照,在执行变更回放时 Reorder Buffer 创建了一个事务并应用它,确保在当前回放位置上访问到的系统表记录是一致的,如不能在当前位置上访问到“未来”的 pg_publication 中的条目,snapbuild.c 源码文件开头包含了一份简洁的注释来解释为何需要这样的快照,可以参照阅读


系统表历史快照(Historic Catalog Snapshot),内部的快照类型为 SNAPSHOT_HISTORIC_MVCC,我们可以在 src/include/utils/snapshot.h 头文件中声明的 SnapshotType 看到。当从一个复制槽开始进行逻辑复制时,从槽所记录的 restart_lsn 位置开始读取 WAL,SnapBuild 将根据解码的 WAL 记录构建或更新历史快照。与普通快照相反的是,历史快照中的 xip 列表包含的是在 xmin 和 xmax 之间已提交的事务,基于全部事务变更构建的历史快照可以转换为普通快照并导出。系统表历史快照在构建时只跟踪涉及系统表变更的事务。

SnapBuild 的钩子函数


SnapBuild 有几种比较重要的钩子函数,在 LogicalDecodingProcessRecord 函数(位于 src/backend/replication/logical/decode.c)读到相应类型的 WAL 记录时被触发,使 SnapBuild 能够更新自己的状态,构建并应用历史快照。

LogicalDecodingProcessRecord 函数大致上通过 WAL 的 RmgrId 委派给不同的解码函数,比如我们来看到处理 STANDBY_ID 的 DecodeStandbyOp,这里面我们看到构建快照时所需要的 XLOG_RUNNING_XACTS 类型的 WAL,其中描述了此时正在进行中事务等信息。递归进入 SnapBuildProcessRunningXacts、SnapBuildFindSnapshot,此时我们便看到了对快照的状态进行转移和数据更新的实现逻辑。这样,我们可以运用调试技术可以从调用 DecodingContextFindStartpoint 开始进入,观察一个初始快照是如何建立起来并到达一个一致性点的。


SnapBuildProcessChange


检查位于 (xid,lsn) 的变更记录是否可以被解码,当 SnapBuild 状态达到 FULL_SNAPSHOT 之前,或达到 FULL_SNAPSHOT 但事务却开始于达到该状态前的将不能被解码。另外当 Reorder Buffer 在该事务上还没有基础快照时构造一个。


SnapBuildProcessNewCid


当读取到一个 xl_heap_new_cid 日志时,意味着该事务中的一条语句涉及系统表变更(因为只有在这种情况下才会在 WAL 中记录 xl_heap_new_cid 日志),这里涉及了与 Reorder Buffer 的数据操作,如 ReorderBufferXidSetCatalogChanges 函数(位于 src/backend/repolication/logical/reorderbuffer.c)标记所属事务包含系统表变更,本篇文章暂不展开。


SnapBuildCommitTxn


当读取到一个事务提交记录时,其主要的动作是检查当前事务及其子事务中是否涉及系统表的变更(相关 Flag 变量为 needs_timetravel / sub_needs_timetravel),如果涉及则需要添加至 SnapBuild 的 xip 已提交事务列表中,最后通过 SnapBuildDistributeNewCatalogSnapshot 函数将新的快照添加到所有当前正在进行的事务中。其中实际上对遍历的每个事务执行 ReorderBufferAddSnapshot,排入 ReorderBufferTXN 变更队列尾部。在执行事务及其未被中止的子事务的重放时(ReorderBufferCommit),看到处理 REODER_BUFFER_CHANGE_INTERNAL_SNAPSHOT 的 Switch Case 通过一对 TeardownHistoricSnapshot、SetupHistoricSnapshot 更换到了另一个历史快照上。就像 SetupHistoricSnapshot 函数或是其他相关地方的的注释所表述的那样,我们在访问系统表时“时光倒流”到了某个历史状态上。


SnapBuildProcessRunningXacts


SnapBuild 是根据 xl_running_xacts 日志中的信息更新 state、xmin 信息的。SnapBuildFindSnapshot 帮助函数实现了在 SnapBuild 在达到 SNAPBUILD_CONSISTENT 状态前的相关处理逻辑,其中主要处理 SnapBuild 的状态转移。SnapBuild 中的 state 有四种状态,分别是 START、BUILDING_SNAPSHOT、FULL_SNAPSHOT 和 SNAPBUILD_CONSISTENT。只有到 xl_running_xacts 记录时才会发生。简单来说,当第一次读到一个 xl_running_xacts (#1) 表明 initial_xmin_horizon 之前的事务都已经结束时开始构建快照,当 xl_running_xacts #1 所表示的所有进行中的事务结束后,到达 FULL_SNAPSHOT 状态,记当前读到的 xl_running_xacts 编号为 #2。在此期间可能又会开始新的事务,当这些事务也全部结束时,即 #2 所表示的所有进行中的事务结束后,达到一致状态(SNAPBUILD_CONSISTENT)。一个简单的示意图如下所示,我们可以看到深灰色的事务(A、B)所涉及到的变更 SnapBuild 还没开始跟踪,浅灰色阴影部分中的改动不能被解码,因为还没有历史快照可供使用:


了解PostgreSQL逻辑复制中的历史快照

  

此外,该函数中还可能触发 SnapBuild 序列化落盘、清理 xip 中旧的项、更新复制槽的 restart_decoding_lsn 等操作。




03
回顾




当逻辑复制回放变更操作时,涉及系统表变更的事务提交时会生成新的系统表历史快照,并通过 SnapBuildDistributeNewCatalogSnapshot 传播出去,这样处理后续的回放才能被“看到”。回到一开始引出历史系统表快照的问题,也许我们之前就能可以进行推测,在创建复制槽时所设置的 restart_lsn 比创建 replication 后的 lsn 要早,可以使用以下命令来查看 pd_lsn。


SELECT lsn FROM page_header(get_raw_page('pg_publication', 0));


我们可以从 ReplicationSlotReserveWal 函数(位于 src/backend/replication/slot.c)中看到在最初没有给定一个 restart_lsn(也即为一个 InvalidXLogPecPtr 值)时如何设置一个有效的 restart_lsn 的,当然我们也可以继续跟踪流程看到设置 XLogCtl→replicationSlotMinLSN 的值记录对于复制槽来说多久之前的 WAL 是能够被删除的(或某个最小 LSN 之后的 WAL 是需要保留的)。因此我们可以使用 pg_replication_slot_advance 函数向前推进复制槽的位置,同步又能正常开始了。但现在我们可以了解到在逻辑解码过程中根据读取的 WAL 所构建使用的历史系统表快照之后,也明白了在问题中这种情况下 replication 为什么会是不可见的,PG 是如何在重放的过程中如何保证一致性的。


我们对于逻辑复制还有其他问题,如历史快照与 Reorder Buffer 联系紧密,这些历史快照具体是如何被使用的等,看来如果需要了解逻辑复制的全貌,我们还需要对其他模块的实现进行分析。




04
参考内容




PostgreSQL 源码:https://github.com/postgres/postgres/tree/REL_12_0



了解PostgreSQL逻辑复制中的历史快照
点分享
了解PostgreSQL逻辑复制中的历史快照
点点赞
了解PostgreSQL逻辑复制中的历史快照
点在看


本文始发于微信公众号(长亭安全课堂):了解PostgreSQL逻辑复制中的历史快照

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: