非阻塞分区背后的秘密pg_pathman的PostgreSQL后台Worker完整实现剖析【免费下载链接】pg_pathmanPartitioning tool for PostgreSQL项目地址: https://gitcode.com/gh_mirrors/pg/pg_pathmanpg_pathman 是一款为 PostgreSQL 提供优化分区管理的开源扩展其中最精妙的设计是它的后台 WorkerBackground Worker子系统通过两个专门的后台进程它实现了插入时自动建分区和边写入边整理数据两种非阻塞分区操作。这篇文章带你从零看懂这两个 Worker 的完整实现原理与使用方法。为什么 PostgreSQL 分区需要非阻塞传统做法中分区表的维护操作新建分区、把父表数据搬运到子表通常需要较长时间的表锁。在业务高峰期这意味着⛔ 用户查询被阻塞等待锁释放⏱️ 大表整理数据可能持续数小时 锁等待堆积甚至引发级联故障pg_pathman 的解决方案很直接把这些重活交给独立的后台进程去做用户连接几乎不感知。整个子系统的实现集中在 src/pathman_workers.c文件头部的注释就点明了它的两大使命在独立事务中为 INSERT 创建新分区CreatePartitionsWorker并发地处理分区整理操作ConcurrentPartWorker两种 Worker 共用 PostgreSQL 的动态后台 Worker APIRegisterDynamicBackgroundWorker封装在统一的启动函数start_bgworker()中。两大 Worker 角色总览Worker 名称触发场景是否等待完成典型用途SpawnPartitionsWorkerINSERT 遇到超出范围的分区键值✅ 同步等待自动扩展 RANGE 分区ConcurrentPartWorker手动调用partition_table_concurrently()❌ 异步立即返回分批整理已有数据Worker 一SpawnPartitionsWorker——插入时的自动扩分区当向一张自动分区表 INSERT 一行数据而该值落不到任何现有分区时pg_pathman 会决定是自己建还是派后台进程建。这个决策逻辑在 src/partition_creation.c 的create_partitions_for_value()中表的配置参数spawn_using_bgw为true分区结构在之前的事务中就已提交对本事务可见当前事务没有持有与 Worker 冲突的锁。三个条件满足时就启动 SpawnPartitionsWorker。它的关键实现细节有三点参数通过 DSM 传递。后台 Worker 进程与用户连接是两个独立进程无法直接传变量。pg_pathman 创建了一块动态共享内存DSM段把分区表 OID、分区键值打包成字节数组、数据库 OID、执行用户等写入其中只把内存句柄传给 Worker。打包/解包工具函数PackDatumToByteArray()和UnpackDatumFromByteArray()定义在 src/include/pathman_workers.h支持定长和变长如NUMERIC、DATE两种类型。加入锁组Lock Group。主连接先调用BecomeLockGroupLeader()成为锁组领袖Worker 通过BecomeLockGroupMember()加入同一锁组。这样 Worker 申请的锁会归组即使 Worker 因异常退出锁也能随主连接一起释放避免死锁残留。独立事务提交。Worker 连接数据库、开启自己的事务、创建分区、提交后把新分区 OID 写回 DSM。主连接通过WaitForBackgroundWorkerShutdown()同步等待并取回结果——对用户来说体验仍是插入时自动有了分区但建分区的 DDL 锁只存在于后台事务中与其他用户会话的冲突面大幅缩小。另外还有一个精巧的防递归保护标志位am_spawn_bgw确保 Worker 内部比如触发init_callback中的 INSERT不会再次派生 Worker否则会直接报错。Worker 二ConcurrentPartWorker——边写边整理的搬运工这是 pg_pathman 非阻塞能力的旗舰功能把堆放在父表里的存量数据分批搬进对应子分区全程几乎不影响业务。共享内存任务槽。所有 Worker 的任务状态存放在启动时初始化的共享内存数组concurrent_part_slots中见init_concurrent_part_task_slots()每个槽ConcurrentPartSlot记录表 OID、批次大小、失败休眠时间、已处理总行数、Worker PID 和状态free/working/stopping。每个槽由自旋锁保护防止竞态。槽数量上限由max_worker_processes决定。分批处理循环。Worker 的主循环bgw_main_concurrent_part()每轮做四件事以RowExclusiveLock短暂锁住目标表非阻塞方式取不到锁就失败重试通过 SPI 调用 SQL 函数_partition_data_concurrent()定义在 init.sql每次只锁定并搬运batch_size行默认 1000提交事务累加已处理行数没有剩余数据、也没有失败时退出循环。容错与重试。这是实现中非常工程化的部分每批操作包在PG_TRY/PG_CATCH中最常见的异常是与用户查询的死锁函数内部用了FOR UPDATE NOWAIT抢行锁。失败后 Worker 会回滚、在日志中记录第几次重试、休眠sleep_time秒默认 1 秒后再来连续失败达到上限60 次后放弃任务并释放槽位。此外Worker 通过on_proc_exit注册了清理回调进程无论正常还是异常退出槽位都会归还为free不会泄漏。三条命令上手使用功能对用户的暴露面非常简洁函数定义见 init.sql启动SELECT partition_table_concurrently(表名, batch_size, sleep_time);参数校验很严格batch_size必须在 1~10000 之间sleep_time不小于 0.5 秒。启动函数会先检查该表是否已有 Worker 在跑同一张表只允许一个搬运任务找到空闲槽位后立即返回并通过 NOTICE 提示你如何停止它。监控查询系统视图pathman_concurrent_part_tasks返回每个任务的用户、PID、数据库、目标表、已处理行数和状态方便实时观察进度。停止SELECT stop_concurrent_part_task(表名);注意它是礼貌性停止只是把槽位状态改为stoppingWorker 会在当前批次完成后优雅退出保证数据一致性。完整的用法示例和自动化测试可以阅读 sql/pathman_bgw.sql其中还演示了 Worker 与FOR SHARE锁冲突时的重试行为。调优建议与常见坑max_worker_processes不足启动失败时错误提示会明确建议增大该参数——它同时决定了 Worker 数量上限和任务槽数量。批次过大batch_size越大吞吐越高但单批持锁时间越长业务查询更容易死锁重试建议从默认值 1000 起步按监控观察调整。sleep_time太小高并发死锁场景下1 秒以下的重试间隔容易造成锁风暴可适当调大让出时间。⚠️版本适配pg_pathman 支持 PostgreSQL 11~15其中 11/14/15 需要核心补丁项目目前已停止新功能开发官方建议新项目优先评估原生分区能力详见 README.md 和 tests/update/README.md。验证环境仓库自带 cmocka 单元测试tests/cmocka/和 Python 分区回归测试tests/python/partitioning_test.py可配合 docker-compose.yml 快速搭建多版本验证环境。总结pg_pathman 用不到一千行 C 代码把 PostgreSQL 的后台 Worker API 用得很满DSM 动态共享内存解决跨进程序列化参数传递锁组机制保证 Worker 异常退出时锁不残留共享内存任务槽 自旋锁实现多任务并发管理与进度可视PG_TRY 重试 批次化 可配置休眠把整理数据变成对业务无感的后台作业。这套独立事务 分批 优雅停止的设计模式对任何需要在生产 PostgreSQL 上执行长耗时维护操作的同学都是很好的参考范本。【免费下载链接】pg_pathmanPartitioning tool for PostgreSQL项目地址: https://gitcode.com/gh_mirrors/pg/pg_pathman创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考