应用日志里跳出FATAL: sorry, too many clients already,紧接着健康检查全挂,这时候psql也连不进去。先别急着把max_connections从100调到500,PostgreSQL一个连接就是一个独立进程,连接数上去之后内存压力和上下文切换开销同步上升,当时确实不报了,过一两周整体变慢的锅还是这个。
真正要分清的是三件事:配置真的不够、应用侧连接泄漏,还是被锁卡住导致的假性耗尽。
先想办法连进去。
连不进去时怎么钻进去
superuser_reserved_connections默认3,给超级用户留了3个槽,普通账号报的是remaining connection slots are reserved for non-replication superuser connections,超级用户还能进:
psql -U postgres -d postgres
连上去之后先看这个预留值是不是还是默认:
SHOW superuser_reserved_connections;
现场见过最尴尬的情况是这个值被改成了0,所有人包括DBA都被挡在门外,只能重启实例。生产环境留5到10比较稳,改完SELECT pg_reload_conf();就生效,不用重启。
预留槽也被长事务占满时,只能到操作系统层点掉具体的后端进程:
ps aux | grep "postgres:" kill -SIGTERM <pid>
kill -SIGTERM相当于让这个后端自己退出,比-9干净。整实例重启用pg_ctl restart -m fast,代价是所有连接断开,留到最后一步。
连接到底被谁占了
SHOW max_connections; SELECT count(*) FROM pg_stat_activity; SELECT state, count(*) FROM pg_stat_activity GROUP BY state; SELECT usename, application_name, client_addr, count(*) FROM pg_stat_activity GROUP BY 1,2,3 ORDER BY count(*) DESC;
max_connections默认100,包含所有客户端后端、复制连接和超级用户预留槽,改这个值必须重启实例。count(*)贴着上限跑的时候看第三条的状态分布,最后一条能直接看出连接是哪个应用、从哪台机器来的。
state有四种要分开看:active在跑查询,idle空闲,idle in transaction事务开着但没在干活,idle in transaction (aborted)是语句出错后没回滚的那类。前两种只是占个槽,后两种才麻烦,idle in transaction除了占槽还持有事务快照,会挡住VACUUM,表膨胀和事务ID回卷的压力都是从这儿来的。
之前接过一个库,idle in transaction的会话挂了六个小时,源头是一个批量任务在事务里调外部接口,接口超时了,事务却一直不回滚。
看着像连接不够,其实是锁
连接数飙到上限,active一堆,但CPU和IO都不高,这是典型的锁竞争。一条SQL找出谁挡着谁:
SELECT blocked.pid AS blocked_pid, blocked.query AS blocked_query, blocking.pid AS blocking_pid, blocking.query AS blocking_query, now() - blocked.query_start AS wait_duration FROM pg_stat_activity blocked JOIN pg_locks bl ON bl.pid = blocked.pid AND NOT bl.granted JOIN pg_locks kl ON kl.locktype = bl.locktype AND kl.database IS NOT DISTINCT FROM bl.database AND kl.relation IS NOT DISTINCT FROM bl.relation AND kl.pid <> bl.pid JOIN pg_stat_activity blocking ON kl.pid = blocking.pid WHERE kl.granted;
嫌长就用内置函数,pg_blocking_pids自PostgreSQL 9.6、13.0、14.0,这条线往上都有:
SELECT pid, pg_blocking_pids(pid) AS blocking_pids, state, left(query, 80) FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid)) > 0;
拿到阻塞源的pid之后先pg_cancel_backend,它只中止当前查询、不断开会话,对连接池友好;pg_terminate_backend直接断开连接,客户端会收到连接中断,连接池要重建,代价更大。实在杀不掉的多半是系统进程或者正在提交,别硬来。
该配的几个超时
statement_timeout默认0,就是不限时。lock_timeout管的是等锁的时间,自9.3起;idle_in_transaction_session_timeout专门治idle in transaction,自9.6起,默认同样是0。这几个可以按角色或按库下发:
ALTER ROLE appuser SET lock_timeout = '3s'; ALTER ROLE appuser SET idle_in_transaction_session_timeout = '60s'; ALTER DATABASE appdb SET statement_timeout = '30s';
idle_session_timeout管的是纯idle的会话,PostgreSQL 14.0才加的,14.0之前的版本只能靠连接池侧或定时任务回收。改完同样用SELECT pg_reload_conf();生效。
死锁走日志,别跟锁等待混为一谈
死锁由数据库自己检测并回滚其中一个事务,deadlock_timeout默认1s。日志里会有DEADLOCK DETECTED,跟着两段事务各自执行到了哪条语句、在等哪把锁,读一遍就知道是哪两段业务逻辑的加锁顺序反了。修法是让所有事务访问多张表时保持一致的加锁顺序,按表名排序或者按主键大小排都行,冲突概率会明显往下掉。应用侧要捕获死锁异常并重试,而不是直接panic。
锁等待是排队,死锁是数据库主动掐掉一个,两者的处理方式完全不一样。
连接池才是根治
PostgreSQL没有内置连接池,一个应用连接对应一个后端进程,四十个服务各十个Pod、每Pod池子开十,理论连接数就是4000。PgBouncer放在前面,把大量廉价客户端连接映射到几十个真实后端。
三种池模式:session最安全,但idle的客户端照样占着后端;transaction在事务结束时把连接还给池;statement每条语句结束就还,不支持多语句事务。省得最多的是transaction,代价是会话级的东西会失效或者串味:SET与RESET会漏给下一个客户端,会话级咨询锁会被别的客户端解掉,LISTEN收不到通知。要跨事务用临时表或会话级设置的业务,只能用session模式。
PostgreSQL 17.0、18.0这条线上跑PgBouncer,建议直接用较新的版本:PgBouncer 1.24.1、1.25.0、1.26.1,其中1.26.1是2026年10月发的。1.24.0起max_prepared_statements默认开到200,transaction模式下预处理语句的麻烦少了一大半。
装好之后看池的占用,端口默认6432:
psql -p 6432 -U pgbouncer pgbouncer -c "SHOW POOLS;"
cl_active是客户端侧活跃连接,sv_active是真实占用的后端连接,两个数字差得越大说明复用越有效。
连接风暴这一层也别漏
K8s滚动更新的时候十个Pod一起重启,每个启动时建50个连接,瞬间几百个请求打过来,max_connections开到1000也扛不住。三层缓解:把滚动更新的maxSurge压到1让Pod错峰起来,应用侧连接池预热(HikariCP的minimum-idle先建好),最外层再放PgBouncer挡一道。实测下来只做前两层,发布窗口里的连接峰值能压下去一半以上,加上池化基本就看不到报警了。
判断到底是配置不够还是泄漏,看连接数的曲线形状就够了:跟着发布事件尖一把就回落的是风暴,平稳上涨不回头的是泄漏,台阶式跳一下就不动的多半是连接池参数被改过。数据截至2026年10月。