在业务持续高并发写入(内存不断变化)的动态环境中,如何精确划定一条“时间分割线”,并确保这条分割线之前的所有数据,100% 写入底层的物理磁盘。

以下是 SELECT pg_start_backup() 在系统底层执行的 5 个绝对因果步骤。逻辑不跳跃,严格按照源码执行顺序推演:

第一步:API 接收与进程间硬中断触发

【前置状态】

PostgreSQL 处于高并发运行中,内存的 shared_buffers 中存在大量未写入磁盘的 BM_DIRTY(脏页)。底层的物理磁盘数据落后于内存数据。

【源码执行】

  1. 指令接收: pg_probackup 通过 TCP 网络协议(libpq)向 PostgreSQL 发送 SELECT pg_start_backup()
  2. 函数调用: 负责处理该连接的后台进程(Backend 进程)接收指令,跳转执行源码中的 do_pg_start_backup() 函数。
  3. 发送中断信号: Backend 进程自身不负责写磁盘。它通过操作系统层面的进程间通信(IPC)机制,向专门负责刷盘的 Checkpointer(检查点进程) 发送一个唤醒信号。该信号携带 CHECKPOINT_IMMEDIATECHECKPOINT_FORCE 两个极其严格的标志位。
  4. 自身挂起: 发送信号后,Backend 进程调用 WaitLatch(),将自身置于休眠状态,死死等待 Checkpointer 进程的完成回执。

第二步:排他锁抢占与绝对坐标提取(划定分割线)

【前置状态】

Checkpointer 进程被唤醒,它必须立刻确立一个不可争议的数学坐标,作为本次物理备份的起点。

【源码执行】

  1. 抢占排他锁: Checkpointer 进程向系统申请并获取 WALInsertLock(WAL 插入排他锁)。
  2. I/O 屏障形成: 在持有该锁的几微秒内,系统内所有其他的业务 Backend 进程,被物理禁止向 WAL 日志缓冲区追加任何新的修改记录。整个数据库的物理时间轴在此刻被强行“截断”。
  3. 读取字节偏移量: Checkpointer 读取当前 WAL 缓冲区的最末端写入指针。这是一个 64 位的无符号整数(例如 0/A000028),代表自数据库建库以来 WAL 日志不断追加写入的绝对字节总数。
  4. 释放排他锁: Checkpointer 将这个值记录在自己的内存变量中(记为 REDO LSN,即后续返回给备份工具的 Anchor LSN),随后立刻释放 WALInsertLock,允许业务进程继续写入。

【因果推导】

这一步确立了绝对的因果边界:因为获取 REDO LSN 时业务被短暂禁止写入,所以,所有在 REDO LSN 之前产生的修改,其对应的 8KB 数据页必然已经存在于 shared_buffers 中,且必定带有 BM_DIRTY 标记。

第三步:内存状态机分离(圈定目标数据页)

【前置状态】

时间轴已经划定,但业务进程恢复了写入,shared_buffers 中会不断产生新的、LSN 大于 REDO LSN 的脏页。系统必须把“旧脏页”和“新脏页”在物理上隔离开来。

【源码执行】

  1. 极速遍历: Checkpointer 进程开始以最高优先级遍历 shared_buffers 中的所有页面描述符(Buffer Descriptors)。
  2. 状态判定与追加标记: 它检查每一个描述符。如果发现某个 8KB 页面当前携带有 BM_DIRTY(脏页)标记,Checkpointer 就会在这个描述符的内存结构中,强行写入一个额外的标志位:BM_CHECKPOINT_NEEDED
  3. 跳过干净页: 如果页面没有 BM_DIRTY 标记,直接跳过。

【因果推导】

因为第二步和第三步是严格串行执行的。所以在第二步划定 REDO LSN 之前就已经存在的脏页,在第三步遍历时,100% 会被系统打上 BM_CHECKPOINT_NEEDED 标记。至于遍历过程中业务新产生的脏页,即使漏打了标记也没有关系,因为它们的修改发生在 REDO LSN 之后,不属于本次备份的保障范围。

第四步:抹平 I/O 鸿沟(系统调用 writefsync

【前置状态】

现在,Checkpointer 进程拥有了一份明确的、带有 BM_CHECKPOINT_NEEDED 标记的目标页面清单。

【源码执行】

  1. 第一段 I/O (write): Checkpointer 再次遍历缓冲区,针对每一个带有 BM_CHECKPOINT_NEEDED 标记的 8KB 页面,调用 Linux 内核的 write() 函数。这一步将 8192 字节从 PostgreSQL 的用户态内存,拷贝进入 Linux 操作系统的内核页缓存(Page Cache)。
  2. 第二段 I/O (fsync): 写入系统缓存后,Checkpointer 紧接着对相应的文件描述符调用 fsync() 函数。这是一个强制阻塞的系统调用,它命令 Linux 内核:立刻指挥底层的磁盘阵列卡或 NVMe 硬盘控制器,将缓存中的电子信号真正刻录到物理磁盘的扇区上。
  3. 标记清除:fsync() 成功返回 0,Checkpointer 会清除该页面上的 BM_CHECKPOINT_NEEDEDBM_DIRTY 标记。

【因果推导】

不管这一步执行耗费了 5 秒还是 50 秒,不管前端业务在这期间又修改了多少次数据,底层物理逻辑都不会错乱。因为执行 fsync() 的对象,是且仅是第三步圈定的那个静态集合。执行完毕后,REDO LSN 之前的所有修改,已经 100% 存在于物理磁盘上。

第五步:控制文件更新与通关凭证签发

【前置状态】

物理磁盘数据已经与 REDO LSN 严格对齐。需要将这个安全状态通知给等待的备份工具。

【源码执行】

  1. 更新全局控制文件: Checkpointer 进程将第二步获取的 REDO LSN 写入底层文件系统的 $PGDATA/global/pg_control 文件中,并执行 fsync。这标志着底层检查点动作的彻底闭环。
  2. 唤醒等待者: Checkpointer 向第一步中处于休眠状态的 Backend 进程发送唤醒信号。
  3. 强行修改内核参数: Backend 进程醒来后,立刻在内存中将 full_page_writes 参数强制置为 on。这是为了防止后续备份工具用 pread() 读取文件时,恰好遇到业务进程修改文件而导致的数据页物理撕裂(Torn Page)。
  4. 返回数据流: Backend 进程将第二步记录的 REDO LSN(此时称为 Anchor LSN)封装为文本数据流(backup_label 的内容),通过 TCP 网络返回给 pg_probackup

【最终后果】

pg_probackup 接收到这个 LSN,确认数据库底层的物理文件已经满足绝对一致性。随后,它开始利用 fork() 创建工作进程,直接向 Linux 操作系统发起 open()pread() 调用,开始生吃底层的物理文件。


Show me the visualization

这就是从 API 触发,到内存锁定,再到磁盘物理扇区写入的严密源码级因果链。没有任何步骤可以倒置,每一行 C 语言代码都在为下一步的物理状态做绝对的保证。


了解 www.876873.xyz 的更多信息

订阅后即可通过电子邮件收到最新文章。

打下时空锚点::等您坐沙发呢!

发表评论