PostgreSQL 的 max_connections:别把连接上限当成吞吐量旋钮

2026-09-11 28 预计阅读时间: 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.

预计阅读时间:10 分钟

max_connections 看起来像一个容量参数,很容易让人产生这样的直觉:把它从 100 调到 500,数据库就能同时处理五倍请求。实际情况恰恰不同。它更接近一份内存预算和一道熔断器,决定数据库最多接纳多少客户端连接,却不能增加 CPU 核心、存储 IOPS 或内存带宽。

真正需要回答的问题不是“能连多少”,而是“多少连接可以同时做有效工作,以及连接积压时应该在哪里排队”。

连接数不等于查询并发能力

每个 PostgreSQL 连接都会带来成本,包括后端进程、进程私有内存、会话状态,以及执行查询时可能分配的排序、哈希和维护内存。即使连接处于空闲状态,它也不是完全免费的;一旦大量连接同时执行复杂查询,内存和调度压力会迅速放大。

与此同时,服务器真正能并行推进多少工作,主要由这些资源决定:

  • CPU 核心数,以及查询是计算密集型还是等待型。
  • 存储延迟、吞吐量与缓存命中率。
  • 单条查询的执行计划和锁竞争情况。
  • 查询使用的 work_mem、并行执行进程及临时文件数量。

因此,500 个已连接会话可能只有 20 个正在执行查询。反过来,20 条昂贵查询也可能已经让一台机器饱和。提高 max_connections 只会允许更多客户端进入数据库,不会让硬件多出计算能力。

可以先用下面的 SQL 观察连接到底处于什么状态:

SELECT
    state,
    count(*) AS connections,
    max(EXTRACT(EPOCH FROM (clock_timestamp() - query_start)))
        FILTER (WHERE state = 'active') AS longest_active_seconds
FROM pg_stat_activity
WHERE backend_type = 'client backend'
GROUP BY state
ORDER BY connections DESC;

运行前不需要修改参数。重点关注 activeidleidle in transaction 的比例。大量 idle 通常说明应用持有的连接超过实际需要;大量 idle in transaction 则更危险,因为它可能长期保留锁或阻碍垃圾回收。

为什么它是一份内存预算

max_connections 不是简单地预留“连接数乘以固定字节数”。PostgreSQL 的内存组成包含共享内存、每个后端的基础开销,以及查询执行期间按操作节点申请的内存。尤其需要避免把 work_mem 直接理解成“每个连接只使用一次”:一条查询可能包含多个排序或哈希节点,并行查询还会进一步增加实际消耗。

做容量评估时,可以采用保守的粗略模型,而不是追求一个看似精确的数字:

可用内存
- shared_buffers
- 操作系统与文件缓存余量
- autovacuum、维护任务和后台进程预算
- 活跃连接数 × 每个活跃连接的峰值查询内存估算
= 安全余量

这里应使用“峰值活跃连接数”,而不是简单使用 max_connections。不过,连接上限仍然必须纳入最坏情况演练,因为连接风暴可能让大量会话在短时间内同时开始工作。

下面的命令可以列出相关配置及其是否需要重启:

psql "$DATABASE_URL" -X -v ON_ERROR_STOP=1 <<'SQL'
SELECT name, setting, unit, context, pending_restart
FROM pg_settings
WHERE name IN (
    'max_connections',
    'shared_buffers',
    'work_mem',
    'maintenance_work_mem'
)
ORDER BY name;
SQL

max_connections 的修改需要重启 PostgreSQL 才能生效。调整前要确认实例或容器确实加载了预期配置,并为管理连接保留恢复空间。

把排队放在更便宜的位置

当数据库已经饱和,继续接受连接通常会让延迟变得更差:后端进程争抢 CPU,查询相互干扰,连接超时又触发客户端重试,最终形成连接风暴。此时,明确拒绝或在连接池排队,往往比让所有请求同时进入数据库更可控。

可以这样实践:在应用与 PostgreSQL 之间使用连接池,并把应用实例的连接池总量作为统一预算管理。例如,4 个应用实例、每个实例 25 个数据库连接,意味着业务池最多占用 100 个连接,而不是单独看某一个实例的配置。

下面是一个可改造的 PgBouncer 最小配置。示例假设 PgBouncer 与 PostgreSQL 位于可信的内部网络;认证方式、TLS 和凭据文件必须按生产环境要求补齐:

[databases]
app = host=postgres port=5432 dbname=app

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
default_pool_size = 25
reserve_pool_size = 5
max_client_conn = 500
query_wait_timeout = 15
server_connect_timeout = 5

这里的 max_client_conn = 500 表示 PgBouncer 可以接纳较多客户端,但 default_pool_size 限制了实际进入某个数据库连接池的服务端连接数量。等待发生在更轻量的池化层,而不是让 PostgreSQL 为每个等待者创建后端进程。

事务池模式并不适合所有应用。依赖会话级临时表、会话锁、跨事务状态或某些预备语句行为的系统,需要先验证兼容性;不兼容时可以使用会话池,或只对合适的工作负载启用事务池。

用压力测试确定数字

连接上限不能靠 CPU 核心数套一个固定公式。更可靠的方法是逐步提高客户端并发,记录吞吐量、P95/P99 延迟、CPU、磁盘延迟、锁等待和内存,然后找到吞吐量不再增长但延迟开始陡升的位置。

可以在测试环境用 pgbench 做一轮基线测试。下面假设目标库允许创建测试表,切勿直接在生产业务库执行初始化:

export PGURI='postgresql://bench:secret@127.0.0.1:5432/bench'

pgbench -i -s 20 "$PGURI"

for clients in 8 16 32 64 128; do
  pgbench "$PGURI" \
    --client="$clients" \
    --jobs=8 \
    --time=60 \
    --progress=10 \
    --protocol=prepared
 done

修改 PGURI、规模因子和客户端梯度后再运行。内置 pgbench 负载不能代表真实业务,因此更进一步的做法是使用脱敏后的典型 SQL、接近生产的数据规模和真实事务比例重复测试。

上线前的决策清单

设置 max_connections 时,可以依次确认:

  • 是否统计了所有应用实例、后台任务、监控、迁移工具和管理员连接。
  • 应用连接池总量是否有全局预算,而不是每个实例各自使用一个过大的默认值。
  • 数据库饱和后,请求是在连接池中有界等待,还是持续创建新连接。
  • 是否监控 activeidleidle in transaction、连接等待时间和拒绝次数。
  • 是否在接近生产的环境验证过内存峰值、尾延迟和重试行为。
  • 是否为故障处理和管理操作预留了连接能力。

合适的 max_connections 应当略高于经过验证的连接预算,同时足够低,能在连接风暴发生时保护数据库。它是边界,不是加速器;性能容量最终仍由硬件、查询效率和并发控制共同决定。


相关推荐