增量检查点的“困“与“难“

📅 2026/7/28 15:43:28
增量检查点的“困“与“难“
本文整理于 HOW 2026 演讲内容演讲者吕海波易景科技首席研究员PG ACED北京大学企业导师。一、增量检查点的引入背景在基于PostgreSQL开发共享存储集群架构类似Oracle RAC的过程中一个现实问题浮出水面完全沿用PG原有的全量检查点机制时各节点间脏页会持续累积导致压力测试下的性能表现始终上不去。为了解决这个问题我们引入了增量检查点。增量检查点的核心思想并不复杂增加一个位于共享内存中的检查点队列ckptq按脏块变脏的顺序排列所有脏块然后以高频次、小批量的方式沿队列定期刷新脏页。相比全量检查点一次性遍历所有脏页这种方式理论上能更平滑地控制I/O负载。但真正落地实现时有两个关键问题比预想中棘手一是ckptq共享内存锁的管理机制二是增量检查点与FPW全页写之间的耦合关系。下文逐一展开。二、ckptq共享内存锁管理自旋锁竞争的隐藏成本ckptq放在共享内存中多进程并发连接脏块必然涉及锁管理。我们最初采用的是PG自带的SpinLock自旋锁但在高竞争场景下暴露出严重的性能问题。2.1 自旋锁的本质自旋锁本质上就是一个内存变量——1字节、2字节、4字节或8字节。进程A持有锁时把值从0改成1进程B发现值不为0就不停地循环检查直到值变回0。这种忙等待的目的是不让出CPU避免上下文切换和Cache污染。问题在于当多个进程同时竞争同一把自旋锁时后果远不止是CPU空转。2.2 CPU核间通信风暴假设16个核Core 0持有锁另外15个核在同时自旋等待。当Core 0要释放锁将1改为0时会发生以下连锁反应Core 0需要向其他15个核广播Invalidate消息通知它们自己L1/L2 Cache中该锁变量的副本已失效等待所有核确认后Core 0才能将变量修改为015个等待的核立即向Core 0发送Write Update消息请求获取最新的变量值CPU内部仲裁后某个核如Core 9获得修改权限再向其他15个核广播Write Invalidate消息待全部确认后Core 9将变量改为1持有锁一轮锁释放与重新获取涉及数十次核间消息广播。在16核规模下尚且如此当代CPU动辄几十乃至上百核这种开销会急剧放大。一轮轮消息同步足以让i9的性能退化到386水平。这就是所谓的锁风暴——热点竞争造成的阻塞被核间通信延迟进一步加剧。这个问题并非增量检查点独有。PG中所有使用自旋锁的地方一旦产生竞争都可能触发同样的核间通信风暴造成性能抖动。2.3 改进思路解决方案的灵感其实来自于CPU自身的缓存一致性协议和RAC的缓存融合机制。核心思路很简单为每个核分配独立的锁变量自旋时每个核只轮询自己的变量互不干扰因此无需广播消息。释放锁时持有者只需要向获得锁的目标核的私有变量发送一次修改消息完成所有权转移。这从O(n²)量级的核间通信降到了O(1)。相关学术研究可参考论文《Non-scalable locks are dangerous》——传统自旋锁在大规模多核系统中的可扩展性问题早有定论只是实践中容易被忽视。三、增量检查点与FPW页分裂问题的实测对比在PG中完全检查点和FPW是强绑定的。引入增量检查点后完全检查点的频率被大幅拉长这会对FPW保护页分裂的能力产生什么影响要回答这个问题得先搞清楚FPW到底在解决什么问题以及其他数据库是如何处理的。3.1 什么是页分裂Partial Write数据库的页如PG的8KB在操作系统层面通常由多个OS页如4KB组成。当数据库发起一个8KB的写操作时在存储层实际上是两次4KB写入。如果写入中途断电或系统崩溃可能出现前4KB写入成功、后4KB未写入的情况——这个数据库页就变成了一半新一半旧的损坏状态这就是页分裂。3.2 如何模拟页分裂长期以来页分裂问题难以验证因为真实场景下除了拔电源几乎无法复现。但通过eBPF/systemtap等内核动态跟踪工具可以拦截pwrite系统调用将写入长度参数从8KB篡改为4KB操作系统就会乖乖地只写一半——完美模拟页分裂且完全排除其他干扰因素。我们分别对Oracle、PostgreSQL和MySQL执行了相同的测试。3.3 Oracle软件层面不处理拦截pwrite后Oracle在检查点刷脏时检测到I/O错误直接崩溃。重启后开始实例恢复定位到检查点位置识别出需要恢复的脏块——然后恢复失败。测试结论很明确Oracle在软件层面并不解决页分裂问题。它既不依赖文件系统的原子写也不在代码中做特殊处理。Oracle的策略是检测到损坏后依赖备份进行介质恢复并提供了BlockRecover工具用于单块恢复。将问题转移给运维本身就是一种选择。3.4 PostgreSQL彻底解决同样流程下PG在I/O错误后并没有崩溃仅报告错误。我们用kill -9杀掉所有进程模拟意外宕机重启后PG从控制文件中读取检查点位置应用对应的WAL日志——数据完整恢复无一丢失。PG通过FPW机制在第一次修改脏页时将整个页写入WAL确保即使发生页分裂也能通过日志完整重做。代价是明显的I/O放大但换来了数据一致性的确定性。3.5 MySQLInnoDB双写的局限MySQL InnoDB使用双写机制先将页写入双写缓冲区再写入实际数据文件。测试发现如果只拦截目标表文件的写入双写可以恢复但如果拦截系统表空间如undo表空间的写入数据库启动报错无法恢复结论是双写解决了部分场景下的页分裂问题但在系统表空间受损时无能为力。在真实的断电系统表空间写入被截断场景下双写并不能保证数据库一定能起来。3.6 三库对比小结数据库方案是否真正解决页分裂Oracle依赖备份与块恢复软件层不解决MySQLDouble Write部分解决系统表空间损坏时失效PostgreSQLFull Page Write彻底解决有性能代价三个主流数据库中只有PG以性能为代价真正从软件层面解决了页分裂问题。Oracle将问题推给硬件/运维MySQL的双写在关键路径上存在盲区。回到TC架构的实际情况底层自研共享存储支持原子写因此在TC中FPW可以关闭。但如果用户没有原子写存储FPW真的是可有可无的吗这个问题没有一个标准答案有兴趣的同学不妨按照本次分享中的步骤实际模拟下页分裂深入体会下底层原理再做定论。活动播报适逢 PostgreSQL 三十周年PGConf.Asia 2026 香港站定于 11 月 17–18 日举办大会面向全球征集 PG 实战技术分享并开放商业赞助合作演讲提案征集 8 月 31 日截止。更多资讯请点击https://mp.weixin.qq.com/s/SQncqrtvUO0IZNQg_pFtYA