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;
运行前不需要修改参数。重点关注 active、idle 和 idle 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 时,可以依次确认:
- 是否统计了所有应用实例、后台任务、监控、迁移工具和管理员连接。
- 应用连接池总量是否有全局预算,而不是每个实例各自使用一个过大的默认值。
- 数据库饱和后,请求是在连接池中有界等待,还是持续创建新连接。
- 是否监控
active、idle、idle in transaction、连接等待时间和拒绝次数。 - 是否在接近生产的环境验证过内存峰值、尾延迟和重试行为。
- 是否为故障处理和管理操作预留了连接能力。
合适的 max_connections 应当略高于经过验证的连接预算,同时足够低,能在连接风暴发生时保护数据库。它是边界,不是加速器;性能容量最终仍由硬件、查询效率和并发控制共同决定。