并行恢复
我的 Wiki 主页:Koichi
电子邮件:koichi.dbms_at_gmail.com
其他主题:分布式死锁检测
并行恢复
PostgreSQL 重做日志 (WAL) 概述
在 PostgreSQL 中,WAL(预写式日志)用于恢复和日志传输复制。此文档页面中描述了概述。WAL 的概述是
- 每个更新的后端进程必须在真正更新数据文件之前将其重做日志写入 WAL。
- 在数据同步到存储器之前,每条 WAL 记录(更新数据记录的一部分)都必须达到稳定存储。
WAL 用于恢复和日志传输复制。在恢复中,WAL 如下使用
- 按照写入顺序从 WAL 片段中读取每条 WAL 记录
- 读取的 WAL 记录应用于相应的数据块。这称为“重做”或“重放”。
- 为了维护重做的一致性,重做是在单个线程中完成的。
在日志传输复制的情况下,每条 WAL 记录
- 按照写入顺序传输到每个副本,并
- 按照恢复时的做法在副本中以单个线程进行重放
- 当重放达到一致的状态时,可以允许邮局总局开始读取事务,而后续 WAL 记录会到达并被重放。
关于序列化重放的问题
目前,WAL 记录按照写入顺序严格在一个线程中重放,而这些记录是由多个后端进程并行生成的。问题是
- 恢复/重放的性能远低于写入 WAL 的性能。恢复可能比撰写 WAL 记录的时间更长。
- 主机有繁重的更新工作负载时,复制延迟可能会很长。
能否并行重放 ?
如果针对恢复顺序设置一些规则,答案是肯定的。规则如下
- 对于给定的数据块,WAL 记录必须按写入顺序应用。
- 对于给定的事务,当应用终止该事务的 WAL 记录(提交/中止/准备提交)时,必须重新播放所有关联的 WAL 记录,
- 对于多块 WAL,它应该只由一个工作进程应用。为此,这种 WAL 记录被分配给负责每个所涉块的全部块工作进程。当块工作进程获取多块 WAL 并且它不是最后一个线程时,它应该等到最后一个获取此记录的线程实际重放此记录。如果某个线程最后获取多块 WAL 记录,它会重放记录并向其他等待工作进程发送同步。
- 要重放特定 WAL 记录(例如更新时间轴),它应该等到所有先前 WAL 记录重放完毕。这可能不是必需的,但在目前为某些 WAL 记录集设置此类条件看来更安全。
- 所有先前 WAL 记录重放完毕后,pg_controldata 中的重放 LSN 会被更新。
- 应用事务结束(提交/中止/准备提交)之前,属于该事务的所有 WAL 记录都必须已经重放完毕。
- 对于多个存储级别 WAL(创建等),我们需要等到这些 WAL 记录重放完毕后再分配后续 WAL 记录,
- 对于多个存储级别 WAL(删除等),我们需要等到所有已分配的 WAL 记录重放完毕。
- 所有先前 WAL 记录重放完毕后,可以更新已应用 WAL LSN。
实施说明
工作进程配置
- 因为当前恢复在启动过程中完成,所以并行重放线程作为其子进程实施。
- 现在我们为此制定了以下流程
- 启动流程:从 WAL 片段/walsender 读取 WAL 记录并执行总体控制,
- 调度工作进程:分析和调度 WAL 记录,
- 错误页面注册:从其他工作进程收集错误页面。当前实施基于内存上下文的哈希函数,不容易将其移植到共享内存。如果我们能够使用共享内存管理此类信息,则可以将其替换为更简单的配置。
- 事务工作进程:重放没有任何块信息可用于更新的 WAL 记录。
- 块工作进程:重放块信息(主要是 HEAP/HEAP2)。
我们可以使用多个块工作进程。其他工作进程由单个进程组成。
为传递并共享 WAL 记录和状态之类的信息,使用在 storage/dsm.h
中定义的共享内存。
为保护这些数据,使用在 storage/spin.h
中定义的自旋锁。
工作人员之间的同步
为在工作人员之间进行轻量级同步,使用 Unix 域 UDP 套接字。可以使用 stoage/latch.h
。替换此项。
新 GUC
添加以下 GUC 参数
parallel_replay
(布尔值)parallel_replay_test
(布尔值,仅供内部使用)启用额外代码用于连接工作人员以便进行 gdb 测试。num_preplay_workers
(整数)总的重放工作人员数。num_preplay_queue
(整数)保存未完成 WAL 记录的队列数,num_preplay_max_txn
(整数)恢复中允许的最大未完成事务数。使用 Max_connections 或以上值。如果是复制备用服务器,则必须使用主服务器的 Max_connections 或以上值。preplay_buffers
(整数)
连接至 GDB
与常规后端不同,恢复代码以启动进程的方式运行(以及由此启动进程派生的工作人员)。由于此项在邮政总局开始接受连接时必须终止,除非启用热备用,否则需要连接更多项才能连接 GDB 到并行重放工作人员。对此,请按如下操作
- 执行哑函数以用作 gdb 断点 (
PRDebug_sync()
)。 - 当启动进程开始派生工作人员时,它向调试文件(位于
PGDATA
中的pr_debug
中)写入消息。 - 消息中含有在单独外壳中执行的操作
- 启动 gdb
- 分别附加启动进程和工作人员进程,
- 设置临时断点函数,
- 创建信号文件,
- 然后,工作人员(以及启动进程)等待创建信号文件并调用断点函数。
这样一来,我们可以将启动进程和每个工作人员进程连接到 gdb,而且不必重复操作。
当前代码
可从 Koichi's GITHub 存储库 获取当前代码。分支为 parallel_replay_14_6
。
有关测试的详细信息,可访问 另一个 Koichi's GitHub 存储库。使用 master 分支。请理解,此页面目前尚未完善,需要针对常规使用情况进行更多更新。
当前代码状态
尚未进行测试。此代码仅通过构建,并且在测试前处于审核中。现在需要改进/开发多个函数,并且某些其他函数会受此更改的影响。
完整功能包括
- 从启动进程派生出额外的工作人员进程,
- 将
XLogRecord
分配给工作人员, - 多种同步机制,用于维护 WAL 重放的全局顺序语义,
剩余问题包括
- 需要对共享缓冲区分配/释放进行更多测试。如果实现假设(如果没有空间,则只需等待所有其他工作人员完成并释放可用内存)是正确的。
- 恢复代码中断(大部分中断是由于从启动进程传递的错误解码信息造成的)。
- 显示潜在性能增益的良好基准测试。
热忱欢迎任何合作方的参与讨论/测试/开发。可联系 koichi.dbms_at_gmail.com。
敬请谅解,偶尔的工作可能会导致存储库不完整。
进一步补充说明:
- 要添加一个代码,以文本格式(例如 pg_waldump)在内存的某些部分保留 WAL 记录,以帮助进行测试。
xlog_outrec()
对于此目的似乎非常有用。目前,这通过WAL_DEBUG
标志启用。
在 PGCon 2023 中的讨论
幻灯片可从 File:Parallel Recovery in PostgreSQL.pdf 查看。
以下是 PGCon 2023 演示中的一些见解。
- 如果在对相应的数据页执行重做之前对索引页执行重做,则备用实例上的并行恢复可能会中断。
- 在这种情况下,索引记录将指向未占用的页面索引(不使用)。在索引的当前实现中,这将导致错误。
- 设想是:在禁用热备用实例时使用此方法。在并行情况下,我们可以更改索引代码,以忽略此类悬空引用。
- [Mmeent] 对于大多数索引来说,来自索引的悬空引用是损坏的迹象。仅对副本禁用完整性检查是有可能的,但会导致检测完整性违例的能力降低。
- 工作进程可以作为后端处理器的子进程吗?
- 建议评论说这是可能的。在继承启动器的资源方面,启动器的子进程看起来更简单。
- 可以将 Linux 域套接字替换为闩锁。
- 在即将到来的改进中应考虑这一点。
我还没有收到关于使用 POSIX mqueue 的反馈。但是,POSIX mqueue 在配置方面不那么灵活。每次更改都需要进行内核配置,并且使用共享内存和套接字/闩锁来实现会更好。
PGCon 会后的设想
[Mmeent] 经过更多思考,我们或许可以在热备用节点上使用并行恢复。
- 监视每个事务的 WAL 记录的 relfinenode。
- 如果 relfilenode 发生变化,则工作进程会等待事务的所有其他 WAL 记录重放。这将维护数据和索引的更新顺序。
[节省内存分配]
- 发现大多数 WAL 记录非常短,不到 128 字节。
- 我们可以为每个队列提供一个有效负载,以便将此类较短的 WAL 记录存储在队列中。队列在重放开始时分配,而大多数 WAL 记录不需要在共享内存中进行动态内存分配。
- 这可以显著减少动态内存分配的次数。例如,HOT UPDATE、VACUUM、COOMIT/ABORT、VISIBLE、INSERT/DELETE、、PRUNE 等。
[一致的恢复点]
- 我们可以在读取工作进程中依赖此项功能。当达到一致的恢复点时,我们只需同步所有已分配给工作进程的队列进行重放即可。
[使 WAL 入队并且出队]
- 可能需要从专用实现开始,该实现基于共享内存、自旋锁和本地套接字。