Vadim Yatsenko:共享存储守护进程的零拷贝 I/O——移除 WAL 路径上的最后一次复制
内容提要
PolarDB 的 pfsd 守护进程默认将调用方数据复制到自有内存池再写入设备,该复制位于 WAL 提交关键路径上,增加延迟和带宽开销。新方案采用零拷贝 I/O,用单个 memfd 支撑整个主共享内存段并一次性注册给 pfsd,pfsd 对段内指针直接读写以跳过复制,未对齐或段外缓冲区则回退到复制路径。该优化移除了 WAL 提交路径上最后一次内存复制,也适用于数据页、SLRU 页等共享内存缓冲区,可通过 polar_enable_zero_copy 开关启用。
延伸解读
零拷贝的适用边界:仅限共享内存缓冲区
文章明确指出,零拷贝并非对所有缓冲区生效。pfsd 只能直接读写位于主共享内存段内的缓冲区,因为该段由单个 memfd 支撑并一次性注册给 pfsd。对于 malloc、栈上或数据段中的普通缓冲区,pfsd 无法看到,仍会回退到复制路径。因此,该优化精准针对 WAL 缓冲区、共享缓冲池、SLRU 页等共享内存驻留缓冲区,而非通用 I/O 路径。
对齐是快速路径条件,而非正确性要求
零拷贝实现中,缓冲区无条件按 PG_IO_ALIGN_SIZE 对齐,确保段布局与大小一致。但对齐只是进入零拷贝快速路径的资格条件,不是正确性前提。若缓冲区未对齐,shim 会回退到复制路径,保证功能正确。这种设计让优化可跳过,不会因对齐问题导致错误,也说明零拷贝是可选优化而非强制路径。
开关控制与行为回退
文章提到,在存在共享存储和 pfsd 时,开启 polar_enable_zero_copy 会移除复制;关闭则恢复原有行为。这意味着该优化可通过配置启用,便于部署时权衡。同时,pfsd 仅对它能看到的缓冲区跳过复制,其他情况仍走复制路径,保留了最初引入复制所保障的正确性。
Q&A
PolarDB 的 pfsd 默认写路径为什么要多一次内存拷贝?
因为 pfsd 和 postgres 是不同进程,pfsd 只能读取自己可见的内存。调用方缓冲区(如 malloc、栈上或数据段中的缓冲区)无法让两个进程在同一地址看到,所以 pfsd 必须先把数据复制到自己拥有的内存池,再从该池写入设备。
零拷贝 I/O 具体是怎么实现的?
用一个 memfd 支撑整个主共享内存段,在 postmaster fork 前以 MAP_SHARED 映射,并一次性注册给 pfsd。pfsd 的读写 shim(pfsd_pwrite_zc / pfsd_pread_zc)识别段内指针后直接读写,跳过复制;未对齐或段外缓冲区则回退到复制路径。
零拷贝优化能带来什么好处?
它移除了 WAL 提交关键路径上的最后一次内存复制,降低每次提交的延迟,并减少复制消耗的内存带宽。该优化也适用于数据页、SLRU 页、校验和暂存等共享内存缓冲区,因此收益不限于 WAL。
零拷贝会不会影响数据正确性?
不会。pfsd 只对它能直接看到的共享内存段内缓冲区跳过复制,对未对齐或段外缓冲区自动回退到复制路径。对齐只是快速路径的资格条件,不是正确性要求,因此原有正确性保证得以保留。
如何启用或关闭零拷贝 I/O?
在存在共享存储和 pfsd 的环境中,通过 polar_enable_zero_copy 开关启用零拷贝;关闭该开关则恢复原有的复制行为。
为什么零拷贝只针对共享内存中的缓冲区?
因为只有位于主共享内存段内的缓冲区才能被 pfsd 直接看到,从而避免复制。对于任意普通缓冲区(如 malloc 或栈上分配的),pfsd 无法直接读取,复制仍然不可避免,所以零拷贝只优化可避免复制的子集。