善用Postgres连接池、别再滥调 max connections !


Postgres 连接数有限非坏事,滥用会导致性能雪崩。善用连接池、最小化占用、PgBouncer 代理,才是高效管理之道。

为什么你的 PostgreSQL 总是报“连接满了”?别再傻傻调 max connections 了!

你兴冲冲地启动了一个新项目,听说 PostgreSQL(简称 Postgres)性能强、生态好,果断选它当数据库。开发阶段顺风顺水,上线初期也一片祥和。结果某天流量一飙升,系统突然炸了——日志里满屏都是那句让人脊背发凉的错误:“FATAL: remaining connection slots are reserved for non-replication superuser connections”。

啥意思?Postgres 连接池满了,普通用户连不上了!这其实是很多新手在数据库运维路上踩的第一个大坑。别慌,今天我们就把这个问题从根上讲清楚,顺便告诉你,为啥别再迷信“把 max_connections 配大点就完事了”——真相远没那么简单。

Postgres 的连接机制:每个连接都是个“亲儿子”进程

Postgres 并不像 MySQL 那样用线程模型,而是采用经典的“多进程架构”:一个叫 Postmaster 的主进程坐在门口接待所有新连接,每来一个客户端,它就 fork 一个全新的子进程(叫 backend)去专门服务这个客户端。听起来很稳?但代价也来了——每个 backend 进程启动时就要吃掉约 5MB 内存,随着查询复杂度上升,它还会膨胀得更大。那是不是只要内存够大,就能无限开连接?答案是否定的。真正的性能瓶颈,藏在“共享内存”里。

真正的瓶颈:全局锁和遍历所有进程的“慢动作”

Postgres 为了让所有 backend 进程协同工作,专门划分了一块共享内存区域,其中有个叫 ProcGlobal 的结构体,里面存着当前所有活跃进程和事务的信息。关键来了:每次有新连接进来,Postgres 必须先拿到一个“独占锁”(LW_EXCLUSIVE),才能把新进程加进这个全局列表——这就意味着,连接越多,加锁竞争越激烈。更致命的是,几乎每个数据库操作都要调用 GetSnapshotData() 函数生成事务快照,而它的工作逻辑居然是:遍历整个系统中所有活跃进程!代码长这样:

Snapshot  
GetSnapshotData(Snapshot snapshot)  
{  
    ProcArrayStruct *arrayP = procArray;  
    ...  
    numProcs = arrayP->numProcs;  
    for (index = 0; index < numProcs; index++)  
    {  
        ...  
    }  
}

想象一下:你有 500 个连接,每次查个表都要循环 500 次?性能不崩才怪!有人专门做了压测:当并发连接从 1 个慢慢加到 1000 个,每个连接执行 10 次插入、10 次查询、10 次删除,结果发现——随着连接数增长,单个事务的耗时一路飙升!这就是为什么像 Google Cloud、Heroku 这些云厂商死活不敢把 max_connections 放开到上千:不是抠门,是真为了你好。Postgres 的性能在连接数暴增时会雪崩式下降,这不是内存问题,是架构级的“并发诅咒”。

聪明人都用连接池:别让每个请求都新建连接

既然连接这么金贵,那怎么办?答案是:连接池(Connection Pool)。

你可以把它想象成数据库连接的“共享单车”——应用启动时先预热一批连接(比如 10 个),扔进一个池子里。当有请求进来,工人(worker)从池子里“借”一个连接干活,干完立刻还回去,而不是用完就扔。这样既能省掉反复创建/销毁连接的开销(TCP 握手、认证、内存分配),又能精准控制最大连接数。主流语言几乎都内置了连接池:Go 的 database/sql、Java 的 JDBC、Ruby 的 ActiveRecord 都有。

但注意!如果你用的是多进程部署(比如 Ruby 的 Puma、Unicorn),每个进程都会维护自己的小池子,总连接数 = 进程数 × 每个池的大小——这时候总连接数很容易失控。

最小化连接占用:只在核心逻辑时“持证上岗”

光有连接池还不够,你还得学会“精打细算”。

想象一个典型的 HTTP 请求:开头要读取请求体、校验参数、做权限检查;中间才是核心业务逻辑,比如查数据库、改记录;最后还要序列化响应、打日志、发监控指标。

关键来了:数据库连接只在“中间那段”才真正需要!聪明的做法是——在进入核心逻辑前才从池里借连接,一出核心逻辑就立刻归还。

这招叫“最小可行连接占用”(Minimum Viable Checkouts)。别小看这几毫秒,积少成多,它能让 100 个连接轻松支撑上千个并发请求。

反例是什么?有人把连接从请求开始一直占到结束,结果 90% 的时间连接在“摸鱼”,纯属浪费。

调用第三方 API 时?赶紧把连接还回去!

还有一种隐藏的连接杀手:在数据库事务里调用外部 API。比如你先从数据库查用户余额,然后调微信支付接口,再根据结果更新数据库。这流程本身没问题,但如果你在整个过程中(包括等待微信支付响应的 2 秒)都占着数据库连接,那这 2 秒里你的连接就废了——它既不能干别的,还阻塞着池里其他请求。正确姿势是:查完余额立刻提交事务、归还连接;再去调微信支付;支付成功后再建新连接、开启新事务去更新余额。记住:数据库连接 ≠ 业务事务,别把它们绑死!

单机不够用?上 PgBouncer 做“全局连接调度员”

即使你把单机连接池优化到极致,当服务横向扩展到几十上百个节点时,新问题又来了:每个节点都有自己的连接池,彼此不通信。假设你的 Postgres 最大支持 500 连接,你部署了 10 个节点,每个节点配 60 连接池——理论上 600 > 500,但平时可能只用 400,一切正常。

可一旦流量高峰到来,所有节点同时飙高,瞬间就超了 500,然后集体报错。这时候,你需要一个“全局连接管家”——PgBouncer。它坐在应用和 Postgres 中间,所有连接都先打给它。

PgBouncer 有三种模式:会话池(Session Pooling)、事务池(Transaction Pooling)和语句池(Statement Pooling)。对绝大多数应用,事务池是最佳选择——它只在你执行事务时分配真实连接,事务一结束就回收。这意味着,即使你有 1000 个客户端连着 PgBouncer,只要它们不是同时执行事务,Postgres 真实连接数可能只有几十个!当然,事务池有代价:你不能用 SET 设置会话变量、不能用 LISTEN/NOTIFY 机制、不能用预编译语句。但对 90% 的 CRUD 应用来说,这完全可接受。

云厂商的“小气”其实是大智慧:别挑战 Postgres 的并发极限

说到这里,你应该理解为什么 Heroku、GCP 这些大厂对连接数抠抠搜搜了。比如 Heroku 的 Hobby 级数据库只给 20 个连接,标准版也就 100 多。很多人第一反应是:“给我升级到 500 啊!” 但真正懂行的工程师会反问:“你确定要用 500 个真实连接?而不是用 PgBouncer + 连接池把真实连接压到 50 个?”

记住:Postgres 的性能不是线性增长的,而是随着连接数增加逐渐衰减的。与其硬堆连接数,不如优化连接使用效率——这才是云厂商想让你学会的生存技能。

连接是稀缺资源!把它当成“内存”一样精打细算

过去很多框架(比如早期的 Rails)为了简化开发,把连接管理藏得严严实实,让你感觉“数据库连接是无限的”。短期看是省事了,长期看是埋雷。一旦你的应用上量,早晚会被连接问题打脸。所以,从项目第一天起,你就得建立“连接资源意识”:  
- 知道单个应用实例最多用多少连接(看连接池配置)  
- 知道集群总连接数 = 实例数 × 单实例连接数  
- 留意部署时的“连接峰值”(比如蓝绿部署时新旧实例并存)  
- 监控 Postgres 的活跃连接数(用 pg_stat_statements 或直接查 pg_stat_activity)  

把这些数字和 Postgres 的 max_connections 对标,留出 20% 缓冲余量。否则,半夜三点的告警电话,可能就是它打来的。

不只是 Postgres:所有数据库都有“连接天花板”

虽然本文以 Postgres 为例,但连接资源瓶颈是所有数据库的共性。MySQL 的线程池、MongoDB 的连接数限制、甚至 Redis 的客户端连接上限,背后逻辑都一样:资源有限,滥用必崩。所以,这套“连接池 + 最小占用 + 全局代理”的组合拳,放到任何数据库场景都管用。技术债迟早要还,不如早点把连接管理当核心能力来修炼。