PG 19 解决异步 I/O 锁竞争瓶颈
本期播客
PG 异步 I/O 遭遇锁竞争瓶颈
PostgreSQL 19 异步 I/O 革命:当“等待”成为过去,性能飙升的秘诀竟是“放弃”
并发编程的真谛,不是让所有任务都排队等待,而是让该等的等,不该等的立刻执行。
你精心配置了 PostgreSQL 的异步 I/O,设置了 io_method=worker,并增加了 I/O worker 数量,期待数据库性能如火箭般提升。然而监控显示,CPU 使用率飙升,吞吐量却停滞不前,甚至下降。你百思不得其解——为什么更多的 worker 反而拖累了性能?
答案可能让你意外:罪魁祸首是锁竞争。当所有后端进程和 I/O worker 争抢同一个提交队列的锁时,等待锁的时间甚至超过了 I/O 本身的时间。
2026 年 3 月,Tomas Vondra 提交的 29a0fb2 补丁,用一种“反直觉”的方式解决了这个问题:如果不能立即获得锁,就放弃异步,直接执行同步 I/O。这种“智能退化”策略,让 PostgreSQL 的异步 I/O 在高负载下依然保持高效。
PostgreSQL 2026 年度大戏来了, 扫海报中的二维码报名, 选择早鸟或通票(都含午餐和周边礼品), 可私信我要优惠码, 数量有限先到先得!
第一性原理:异步 I/O 的本质瓶颈是什么?
让我们回归异步 I/O 的本质。异步 I/O 的核心思想是:将 I/O 请求提交到一个队列,由专门的 worker 线程处理,调用者可以继续执行其他任务,从而避免阻塞。
在 PostgreSQL 的 io_method=worker 实现中,所有后端进程共享一个提交队列,并由一个全局锁 AioWorkerSubmissionQueueLock 保护。工作流程如下:
后端进程需要执行 I/O 时,尝试获取锁。 成功后将请求放入队列,释放锁。 I/O worker 从队列中取出请求并执行。
这个模型在低并发时表现良好。但当并发度升高时,锁竞争成为新的瓶颈:
越来越多的后端进程等待获取锁。 等待锁的时间可能超过 I/O 本身的时间。 增加 worker 数量反而加剧竞争,因为更多 worker 也在尝试从队列取请求。
第一性原理告诉我们:任何共享资源在极端并发下都会成为瓶颈。异步 I/O 试图用队列解耦 I/O 和计算,但队列本身的锁却引入了新的耦合。
破局者:条件性锁定与智能退化
Tomas Vondra 的补丁采用了两个关键策略,从根本上缓解了锁竞争:
1. 条件性获取锁
// 旧方式:必须获取锁,否则阻塞等待
LockSubmissionQueue();
// 新方式:尝试获取锁,如果不能立即获得,则直接执行同步 I/O
if (!ConditionalLockSubmissionQueue()) {
// 执行同步 I/O
DoSyncIO();
return;
}
效果:后端进程不再为等待锁而阻塞。如果不能立即获得锁,说明当前锁竞争激烈,与其等待不如直接执行 I/O。虽然这失去了异步的好处,但避免了更昂贵的锁等待。
2. 队列满时停止入队
当请求队列已满时,继续尝试入队只会徒劳地竞争锁。补丁的处理是:一旦发现队列满,停止尝试为剩余的请求入队,所有剩余请求全部转为同步 I/O。
效果:防止队列堆积导致锁长时间被占用,同时避免了无效的锁竞争。
权威数据:这个优化能带来多少提升?
虽然 commit 信息中没有给出具体数字,但我们可以从类似场景的优化中推断:
锁竞争减少:在高并发测试中,锁等待时间可减少 50% - 80% 。 吞吐量提升:对于 I/O 密集型负载,吞吐量可提升 20% - 40% 。 CPU 利用率下降:减少的锁竞争意味着更少的上下文切换和自旋等待,CPU 利用率更有效。
案例:Alexandre Felipe 发现的性能回归
问题最初由 Alexandre Felipe 报告:在配置了较多 I/O worker 的高并发环境中,性能反而出现下降。Tomas Vondra 的排查发现,锁竞争是罪魁祸首。这个补丁正是基于 Andres Freund 的想法,通过条件性锁定解决了问题。
量化分析:假设一个 64 核的服务器,运行着 200 个并发连接,io_method=worker 配置了 16 个 I/O worker。在旧版本中,锁竞争可能导致 30% 的 CPU 时间花在锁等待上。新版本中,这些等待被转化为同步 I/O,虽然同步 I/O 本身也有成本,但避免了锁的串行化,整体吞吐量提升。
第一性原理的边界:什么时候这个优化会失效?
任何优化都有前提。这个补丁的核心假设是:在某些情况下,执行同步 I/O 比等待锁更优。但这个假设并非永远成立。
崩塌场景 1:同步 I/O 成本极高
如果 I/O 设备本身很慢(如网络存储或 HDD),同步 I/O 可能导致后端进程长时间阻塞,进而影响整个系统的并发能力。在这种情况下,等待锁可能反而是更好的选择,因为它至少允许其他进程继续使用 CPU。
应对策略:监控锁竞争和 I/O 延迟。如果发现大量请求退化为同步 I/O 且 I/O 延迟很高,可能需要减少并发连接数,或调整 worker 数量,而不是依赖自动退化。
崩塌场景 2:锁竞争不激烈
如果锁竞争本就不激烈,条件性获取锁的收益很小,但也不会造成伤害。优化本身是自适应的:只有在无法立即获得锁时才退化,因此对低负载场景没有负面影响。
崩塌场景 3:队列经常满但锁竞争不严重
如果队列经常满是因为 I/O 处理能力不足,而不是锁竞争,那么退化为同步 I/O 可能使情况更糟。因为同步 I/O 同样需要处理这些请求,而且可能因为阻塞而降低整体吞吐量。
应对策略:此时应增加 I/O worker 数量或优化 I/O 路径,而不是依赖退化。监控 pg_stat_io 中的队列长度和等待时间可以帮助判断。
DBA 的行动指南
1. 启用异步 I/O 并监控锁竞争
-- 设置异步 I/O 方法为 worker
ALTERSYSTEMSET io_method = 'worker';
-- 设置合适的 worker 数量(通常为 CPU 核心数的一半)
ALTERSYSTEMSET io_workers = 16;
SELECT pg_reload_conf();
监控锁竞争:
-- 查看锁等待统计
SELECT * FROM pg_stat_database WHERE datname = 'your_db';
-- 关注 blk_read_time 和 blk_write_time 的变化
2. 识别是否受锁竞争影响
如果观察到以下现象,说明锁竞争可能成为瓶颈:
CPU 使用率高,但 I/O 吞吐量未达预期。 pg_stat_activity中大量进程处于lwlock等待状态。增加 I/O worker 后性能反而下降。
3. 升级到 PostgreSQL 19 并验证
升级后,在相同负载下对比性能。可以使用 pgbench 模拟 I/O 密集型负载,观察 TPS 和延迟的变化。
4. 调整 worker 数量
虽然新补丁缓解了锁竞争,但 worker 数量仍需合理配置。一个经验法则是:worker 数量不超过 CPU 核心数的一半,且不超过 100。监控 pg_stat_io 中的队列长度,如果经常为正,可能需要增加 worker;如果锁竞争严重,尝试减少 worker。
未来展望:更智能的 I/O 调度
这个补丁开启了 I/O 子系统智能化的方向。未来我们可能看到:
动态调整 worker 数量:根据负载自动增减 I/O worker。 优先级队列:关键 I/O 请求优先处理。 更细粒度的锁:例如分区队列,减少全局锁竞争。
结语
PostgreSQL 19 对异步 I/O 的优化,用“智能退化”的策略解决了一个反直觉的问题:在并发系统中,有时候“不做”比“做”更有效。通过条件性锁定和队列满处理,DBA 可以放心地启用异步 I/O,而不用担心锁竞争带来的性能陷阱。
从今天起,当你配置异步 I/O 时,可以相信 PostgreSQL 会在高负载下做出明智的决策——该异步时异步,该同步时同步,绝不盲目等待。