别被名字骗了:PostgreSQL 的 maintenance_io_concurrency 到底影响哪些任务

2026-09-08 18 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

maintenance_io_concurrency 看起来像一个控制“所有维护操作并发 I/O”的总开关,但 PostgreSQL 并不是按命令名称决定是否使用它,而是由具体执行路径是否接入维护型 I/O 机制决定。理解这一点很重要:把参数调大,不会让所有 CREATE INDEXREINDEX 或表重写任务一起加速;另一方面,它的影响也不只限于管理员手工执行的一条 VACUUM

真正受影响的是执行路径,而不是命令标签

在使用该参数的 PostgreSQL 版本中,核心的直接使用者主要是普通 VACUUMANALYZE 的相关扫描路径。由此会扩展出一组容易被忽略的实际任务:

  • 手工执行的 VACUUM
  • VACUUM ANALYZE 中相应的 vacuum 和 analyze 阶段;
  • autovacuum worker 执行的自动清理;
  • autoanalyze 自动收集统计信息;
  • vacuumdb 等客户端工具发起的上述 SQL 操作。

这里最容易混淆的是“工具”和“服务器执行路径”。vacuumdb 本身并不会获得特殊 I/O 能力,它只是连接数据库并发出 VACUUMANALYZE。是否读取 maintenance_io_concurrency,最终仍由服务器端进入的代码路径决定。

反过来,也不能因为某个操作属于日常数据库维护,就假定它一定使用这个参数。例如,下面这些操作应当分别确认,不能仅凭名字归入其中:

  • CREATE INDEX
  • REINDEX
  • CLUSTER
  • VACUUM FULL 所涉及的表重写阶段;
  • 扩展提供的自定义维护命令。

PostgreSQL 的实现会随版本演进,尤其是异步 I/O 和预读机制仍在变化。升级大版本时,应该重新查看对应版本文档和发布说明,而不是永久沿用旧版本的任务清单。

它控制的不是并行 worker 数量

maintenance_io_concurrency 容易与以下几个概念混在一起:

  • 并行查询 worker;
  • autovacuum worker 数量;
  • maintenance_work_mem
  • 存储设备的 IOPS 上限;
  • 单次维护任务的带宽限制。

这个参数并不创建更多 CPU worker,也不会直接规定每秒允许读取多少字节。它控制的是维护扫描能够同时推进多少 I/O,或者在没有真正异步 I/O 的实现中,控制预读可以向前推进到什么程度。具体语义和效果取决于 PostgreSQL 版本、表访问方法以及底层 I/O 实现。

因此,调大它可能让高速 NVMe、云块存储或高延迟远程存储上的扫描更好地隐藏等待时间,但也可能产生副作用:

  • 多个 autovacuum worker 同时扩大 I/O 压力;
  • 挤占前台查询需要的存储带宽;
  • 增加缓存污染;
  • 在数据已经位于操作系统页缓存时几乎看不到收益;
  • 存储队列已经饱和时,延迟反而上升。

它更像一个“允许维护扫描提前准备多少 I/O”的旋钮,而不是通用的性能倍增器。

先查看当前实例,再做一个可重复的小实验

下面的示例可以直接在测试数据库中执行。请先把 DATABASE_URL 改成目标测试库的连接串;脚本会创建并删除两张测试表,不要直接在生产库运行。

export DATABASE_URL='postgresql://postgres:postgres@localhost:5432/postgres'

psql "$DATABASE_URL" -v ON_ERROR_STOP=1 <<'SQL'
\timing on

SELECT name,
       setting,
       unit,
       context,
       source,
       min_val,
       max_val
FROM pg_settings
WHERE name = 'maintenance_io_concurrency';

DROP TABLE IF EXISTS mioc_low;
DROP TABLE IF EXISTS mioc_high;

CREATE TABLE mioc_low AS
SELECT i AS id,
       repeat(md5(i::text), 4) AS payload
FROM generate_series(1, 500000) AS g(i);

CREATE TABLE mioc_high AS
SELECT * FROM mioc_low;

CREATE INDEX ON mioc_low (id);
CREATE INDEX ON mioc_high (id);

UPDATE mioc_low
SET payload = payload || 'x'
WHERE id % 3 = 0;

UPDATE mioc_high
SET payload = payload || 'x'
WHERE id % 3 = 0;

SET maintenance_io_concurrency = 1;
VACUUM (VERBOSE) mioc_low;
ANALYZE (VERBOSE) mioc_low;

SET maintenance_io_concurrency = 32;
VACUUM (VERBOSE) mioc_high;
ANALYZE (VERBOSE) mioc_high;

DROP TABLE mioc_low;
DROP TABLE mioc_high;
SQL

这个脚本的目的,是确认参数可以在当前会话设置,并观察 VACUUMANALYZE 在两种取值下的运行情况。它不是严谨的基准测试:第二次执行可能受缓存预热、检查点、后台写入和存储抖动影响。

要得到可信结果,至少应当做到:

  1. 使用明显大于内存缓存的测试数据集;
  2. 分别运行多轮并交换测试顺序;
  3. 同时记录墙上时间、CPU、设备延迟和吞吐量;
  4. 在有代表性的 autovacuum 并发量下测试;
  5. 检查前台查询的 P95、P99 延迟,而不只看维护任务完成时间。

EXPLAIN 通常不会直接告诉你某条维护路径使用了多少并发 I/O。新版本可以结合 pg_stat_io、操作系统的 iostat,以及云平台提供的磁盘队列和延迟指标进行观察。例如:

iostat -xz 1

如果设备利用率、平均等待时间和队列深度已经很高,继续增加 maintenance_io_concurrency 往往不是正确方向。

会话实验和全局配置要分开

手工测试时,优先使用会话级设置:

SET maintenance_io_concurrency = 16;
VACUUM (VERBOSE, ANALYZE) public.events;

public.events 换成自己的测试表即可。这种方式影响范围小,连接断开后会恢复原值。

如果确认需要让后台自动维护也使用新值,可以考虑实例级配置。自管 PostgreSQL 可以这样修改:

ALTER SYSTEM SET maintenance_io_concurrency = 16;
SELECT pg_reload_conf();

SELECT name, setting, source, pending_restart
FROM pg_settings
WHERE name = 'maintenance_io_concurrency';

托管数据库通常要求通过参数组或控制台修改。还要注意:只在某个普通应用会话中执行 SET,不会改变其他连接,也不能据此认定 autovacuum worker 已经采用同样的值。

调优时用任务清单,而不是凭参数名猜测

上线前可以按下面的顺序判断:

  • 确认 PostgreSQL 大版本以及该版本中参数的消费者;
  • 区分普通 VACUUMANALYZE 与表重写、索引构建等其他路径;
  • 从当前值附近做小幅调整,不要直接跳到存储队列深度的理论上限;
  • 在 autovacuum 高峰期观察总 I/O,而不是只测试单个手工任务;
  • 同时保护前台事务延迟;
  • 大版本升级后重新验证。

maintenance_io_concurrency 的关键不在于找到一个“最快”的数字,而在于给后台维护足够的 I/O 并发,又不让多个维护任务共同压垮存储。先确认谁真正使用它,再根据完整系统负载调节,通常比盲目放大参数有效得多。


相关推荐