说到 PostgreSQL,很多人第一反应是“功能强、扩展好、开源里最像商业数据库的那个”。但真正上手做运维、调性能,或者被线上连接数打爆搞得焦头烂额的时候,很多人会发现自己对它的了解还停留在“能连上就行”的层面。
PostgreSQL 的连接机制,恰恰是理解这个数据库一切行为的基础。它决定了你能开多少连接、什么时候会报 too many clients、为什么连不上、甚至影响你该不该引入连接池。从进程模型到通信协议,这一整套机制不算复杂,但网上讲得要么太浅,要么直接贴源码注释,对实际排查帮助有限。
这篇东西我会从实用视角拆一遍,结合我自己运维和排障过程中踩过的坑,把 PostgreSQL 的连接机制讲透。适合刚接触 PostgreSQL 想搞清楚底层逻辑的人,也适合已经被连接问题折磨过、想系统补课的 DBA 或后端开发。
1. 先画张全景图:一条连接从建立到关闭的完整旅程
1.1 一条连接一生的四个阶段
PostgreSQL 是典型的“一连接一进程”模型,这与 MySQL 默认的线程模型有本质区别。理解这点之前,先把一条连接的生命周期过一遍,它大致分四步:发起、握手、执行、断开。
- 发起:客户端(psql、JDBC Driver、Python psycopg2 等)向服务器端口(默认 5432)发起 TCP 连接。
- 握手:服务器端主进程 postmaster 接受连接,fork 出一个 backend 进程,然后双方开始按协议交互版本信息、认证信息。
- 执行:backend 进程接收 SQL 查询请求,执行并返回结果。
- 断开:客户端关闭连接,backend 进程退出并释放资源。
这个流程的关键在于第二阶段:握手阶段并不是简单的 TCP 三次握手,它会启动一套完整的应用层协议,也就是 PostgreSQL 原生通信协议,通常称为“前端/后端协议”(Frontend/Backend Protocol)。
1.2 连接是“会话”,不是“网络连接”
在 PostgreSQL 语境里,“连接”这个词往往对应两层含义:一是底层 TCP 连接,二是一个由 backend 进程承载的数据库会话。
这两者从生命周期、管理方法上都完全不同。TCP 连接断开了,会话也就随之结束;但反过来,会话空闲不代表 TCP 连接要断——比如 psql 连着不操作,TCP 连接还在,backend 进程也在,只是处于 idle 状态。
所以排查连接问题时,第一件事就是分清楚你是在看网络层,还是在看会话层。平时用pg_stat_activity看到的其实都是会话层信息,而ss -tnp看到的是 TCP 层的连接状态。这两者有时候会给你完全不同的结论。
2. 进程模型:PostgreSQL 凭什么敢一个连接分一个进程
2.1 postmaster 和 backend 的父与子
PostgreSQL 的核心进程角色有两个:
- postmaster:主进程,启动、关停、接受连接请求、崩溃恢复、fork 子进程。
- backend:处理具体一条连接的查询,干活的都是它。
每当一个新连接到来,postmaster 会调用 fork() 生成一个完全独立的 backend 进程。这个进程拥有独立的进程空间,里面的共享内存区(shared memory)则是所有 backend 都能访问的同一个内存区域。
这带来一个很直接的优势:一个 backend 崩了,不会拖垮整个数据库。这一点在稳定性上是实打实的收益。我在生产环境见过连接被 kill、backend 因为 OOM 被杀、甚至因为某个 SQL 把 CPU 打满的极端场景——负担最重的那个 backend 挂掉,其他正常查询往往还能继续跑,这种隔离性是线程模型很难做到的。
2.2 辅助进程们:你看到的那一堆 PID 到底是什么
连接数一多,你去看进程列表往往能看到一大堆 postgres 进程,名字长得差不多,但角色完全不同。除了连接对应的 backend,还有很多“辅助进程”,比如:
- checkpointer:负责执行检查点,把脏页刷盘。
- background writer:后台写进程,负责慢慢把缓冲区的脏页写出去。
- walwriter:WAL 日志写进程,保证事务日志落盘。
- autovacuum launcher:自动清理启动器。
- stats collector:统计信息收集器。
这些进程大多是全局性的,不直接对应任何一条客户端连接。很多人查连接数时会用ps aux | grep postgres | wc -l,结果发现数量比pg_stat_activity里的记录多出一截,还以为是泄漏了——其实就是这些辅助进程挤在里面。最好是直接查询视图,别用 ps 计数。
2.3 为什么要用 fork,而不是开线程
业界对这个问题有过很多讨论,总结下来,核心原因是稳定性与隔离性。PostgreSQL 的代码设计从一开始就假设进程不会共享可变状态,数据访问主要通过共享内存完成,但各自执行计划、排序区、哈希表等私有数据结构是进程级的。这样一来,内存控制、崩溃恢复、安全隔离都简单粗暴但有效。
代价当然也有:进程切换开销比线程大,内存占用更高,连接数一多性能会明显下降。这也是后续引入连接池、异步连接方案的原动力之一。
3. 通信协议:客户端和服务器之间到底在聊什么
3.1 启动包的字节级结构
PostgreSQL 的前端/后端协议本身不复杂,但几个关键细节没搞明白,排查问题时会很痛苦。
客户端连上端口后,首先会发送一个启动包(StartupMessage),里面包含协议版本号、数据库名称、用户名、客户端编码等信息。注意:这个包的长度字段和协议版本都是网络字节序(大端),如果客户端和服务器的字节序假设不一致,早期版本会直接导致协议解析失败。现在主流驱动都处理了这个,但如果你在写自定义工具,这是个坑。
启动包发送之后,服务端会按顺序返回认证请求——常见的几种情况:
| 认证方式 | 含义 | 安全性 |
|---|---|---|
| trust | 无密码直接放行 | 极低 |
| password | 明文密码传输 | 极低 |
| md5 | 密码哈希传输 | 中低,不建议新环境使用 |
| SCRAM-SHA-256 | 服务端挑战,客户端证明 | 高,现代默认 |
3.2 md5 和 SCRAM-SHA-256 的区别,不只是字母写法
很多人在 pg_hba.conf 里看到scram-sha-256,却不知道它和 md5 本质上的差别在哪。
md5 认证有个经典漏洞:客户端和服务器之间传递的 MD5 哈希值实际上是可重放的,而且服务端保存的也是密码衍生的 MD5 值,拿到数据库文件里的哈希后可以直接用离线字典破解。
SCRAM-SHA-256 是基于挑战-响应机制的,服务端不传输密码,也不传输一个固定可重复的哈希值,而是每次生成随机盐和挑战值,客户端基于这个挑战值证明自己知道密码。安全性比 md5 高一个量级。
所以新环境一律建议 SCRAM,老环境做版本升级时也要计划把认证方式从 md5 迁到 SCRAM。这个坑我见多了,很多团队数据库从老版本升上来,pg_hba.conf 还写着 lmd5,线上密码早就是弱密码了,只是没人去查。
3.3 查询是如何从字符串变成执行结果的
认证完了之后就是业务查询,简单查询和扩展查询是两种不同路径。
简单查询是一条查询走一步:客户端发一条 SQL 字符串,服务端解析(parse)、执行(execute)、返回结果集。psql 用的就是这种方式,简单直观。但这种方式有个天然问题:SQL 字符串在网络上传输的是明文,虽然到了一定阶段可以用 SSL 加密传输层,但整体流程无法避免重复解析、反复规划执行计划。
扩展查询(Extended Query Protocol)则拆成了四个独立阶段:Parse、Bind(绑定参数)、Execute(执行)、Close。每个阶段可以单独缓存复用,尤其对参数化的 SQL,可以避免重复解析规划过程。
现代驱动比如 JDBC 和 psycopg2,默认都走扩展查询,特别是利用 PreparedStatement 的时候,能显著提升重复查询的性能。但从协议层面来看,这四步比简单查询复杂很多,出错信息也更难定位。
3.4 取消查询的隐藏密码:CancellationKey
连接时还有一个很少被注意、但异常重要的字段——CancellationKey。
这个 key 是服务端在认证成功后返回给客户端的两个 32 位随机整数。当客户端想取消某条正在执行的 SQL(比如你按了 Ctrl+C),会重新连接一次,然后发一个 CancelRequest,里面带上这条连接的 PID 和这把 key。
这里的设计非常巧妙:它保证了没有权限的用户无法取消别人的查询,因为拿不到 key 就无法伪造取消请求。但反过来说,如果有人截获了这条数据,理论上就能控制取消。所以生产环境必须用 SSL,这在 PostgreSQL 上已经不是可选项,而是基本安全基线。
4. 踩坑实录:连接问题到底是怎么发生的
4.1 连接数打满的经典场景
在我接触的大量案例里,“too many clients already” 大概是最常见的连接报错之一。它出现的本质就是活跃 backend 进程数到了 max_connections 上限,但形成这个过程的原因千差万别:
- 应用连接池配置过大,业务量一上来连接数瞬间冲顶。
- 某个微服务的连接池没释放连接(连接泄漏),数据库连接长期被占用。
- 查询出现锁等待,导致持锁的 backend 停在那里不动,其他连接只能排队。
- autovacuum 在跑大表时占了一个连接,而你的连接基数又很低。
排查这类问题,第一步永远是:
select state, count(*) from pg_stat_activity group by state;先看连接里都是什么状态:活跃、空闲、还是 idle in transaction。如果一大堆 idle in transaction,说明是应用事务没提交就放那挂着,这才是真正的连接杀手。
第二步确认谁在连、连的是什么库:
select pid, usename, application_name, client_addr, state from pg_stat_activity order by backend_start;注意 application_name 和 client_addr,能帮你快速锁定是哪个服务出的问题。
4.2 max_connections 不是越大越好
很多人的第一反应是把 max_connections 调大,比如调到 1000、2000、5000。这里必须泼一盆冷水。
每个 backend 进程都会占用内存,即使只是 idle 状态,也会维持一个工作内存。假设工作内存是 10MB,2000 个连接就是 20GB 的内存开销,还没跑业务就先把内存耗光了,而且 PostgreSQL 的共享缓冲区、统计信息也可能被大量进程争抢。
更重要的:PostgreSQL 的锁管理、共享内存里固定大小的结构也会随连接数量增长而增长。调大 max_connections 并没有让性能变好,反而让性能下降更严重。正确做法是“高连接数 + 连接池”,而不是“裸连接数直接拉满”。
4.3 pg_hba.conf 的优先级陷阱
认证失败是仅次于连接数爆炸的第二大坑。
pg_hba.conf 是“先匹配先生效”的制度,一旦某条规则匹配了,后面的规则不会再看。很多生产环境出问题时,都是因为新加了一条宽松规则,结果全库认证逻辑直接变了;或者反过来,先写了一条拒绝规则,把本应放行的来源全挡掉了。
常见症状是日志里出现:
pg_hba.conf rejects connection for host "...", user "...", database "...", no encryption我见过一个最典型的案例:有人为了调试临时加了一条host all all 0.0.0.0/0 scram-sha-256,放在最前面,结果其他网段的高权限用户全被强制要求 SCRAM,但同事手头的旧驱动不支持,导致整个应用连不上。事后排查改了配置,还得记得 reload 而不是 restart——pg_ctl reload只重读配置,不断开已有连接,线上能极大减少抖动。
4.4 怎么区分 idle、active、idle in transaction
用pg_stat_activity看状态,这三个值是最关键的:
- active:正在执行查询。
- idle:连接空闲,没在跑 SQL。
- idle in transaction:事务已经开启,但还没有提交或回滚,也没有正在执行语句。
idle 本身不可怕,但 idle in transaction 极端可恨。因为它会占住锁,还会让 vacuum 无法清理你事务之前可能产生的死元组。
有一次线上出现大量锁等待,查出来是一个应用在自己的 ORM 里把事务开启了,但没捕获到异常,导致事务一直挂着。最终表现是:所有人都在 etc 等锁,而那个连接就在那“不急不慢”地占用着资源。
处理方法也很直接:
select pg_terminate_backend(pid) from pg_stat_activity where state = 'idle in transaction' and state_change < now() - interval '10 minutes';但要小心:直接 kill 掉应用的事务,客户端下次拿到的可能就是操作失败,如果没有好的重试机制,会造成业务数据不一致。所以这只能是应急手段,根治得靠应用端的超时控制。
4.5 修改配置后到底该 reload 还是 restart
PostgreSQL 的大部分参数都支持 reload,但有个别的不行,比如max_connections。这个参数从字面意义上就在 PG 启动时决定了共享内存里相关结构的大小,所以必须重启才能生效。
对应到实际运维,就是:如果只是想调work_mem、shared_buffers这类规模性参数,reload 大多够用;如果你想调max_connections或者改了端口监听,那只能重启,而且还得考虑重启对已有连接的影响。很多线上故障就是“调了个参数顺手 restart 了一下”,结果一堆处于事务中间状态的连接全被切断。
5. 连接机制带来的深远影响:不只是“能连上”
5.1 为什么说 PostgreSQL 的连接机制天生消耗资源
一连接一进程,意味着每条连接都要经过 fork,需要复制父进程的内存页表,初始化进程上下文,启动成本并不是“只建一条 TCP 连接”那么简单。
尤其是在短连接场景——比如无状态服务频繁开库、关库,每秒几百次连接时,fork 带来的开销会被无限放大。测试过用服务端 PostgreSQL 直接裸连,高频率建连的情况下 TPS 会明显下滑,进程数一多还会加剧调度开销。
所以,任何承担高并发读写的应用,都应该走连接池。常见方案要么是服务端的 PgBouncer,要么是应用内部的 HikariCP、psycopg2 pool。pgbouncer 的 mode 要分清楚:transaction mode 只在事务期间占用后端连接,session mode 一个客户端会话始终占用一条,两者资源配置完全不同。
5.2 取消连接和 kill 进程,一个容易犯的错
日常排障里,最常执行的语句大概就是pg_terminate_backend或者直接 kill 掉 backend 的进程。
但这里有个极大的坑:pg_terminate_backend实际上发的是一条信号,类似 SIGTERM。它会告诉后端进程“优雅退出”,但如果后端正卡在某个不能中断的步骤(比如等待某种锁、在 kernel 调用里阻塞、或者在 io 阻塞),这个信号也不一定立刻生效。必要时得用pg_cancel_backend(只取消当前查询),最后实在不行才 kill -9。
我更倾向于看日志。等 backend 真的崩了之后,PostgreSQL 会重新拉起它所在的连接,并自动回滚未提交事务。这个机制本身没问题,但如果你在 kill 之前没确认它的事务状态,可能已经在数据一致性的边缘试探了。
5.3 连接与并发:connection 不是 concurrency
最后强调一个特别容易混淆的点:系统进程里开了一百条连接,不代表它能同时跑一百个查询。并发度取决于 PostgreSQL 能同时调度执行多少 backend 的查询,CPU 核数、锁与 I/O 瓶颈都在那里摆着。连接数很多时候是“排队来看病”的号,而不是“正在看病”的人。
理解了这一点,再回头看为什么很多 PostgreSQL 调优文章都在讲“连接池 + 调低 max_connections 反而更快”——就是因为连接过多反而放大了锁等待和上下文切换成本。我见过一台 16 核服务器,max_connections 从 500 降到 100,配合 PgBouncer 事务级池后,性能反而提升了一倍,内存压力也大幅减少。这个场景不是孤例,而是很多团队第一次意识到连接机制重要性时都会遇到的转折点。
6. 从协议细节到日常运维的几点实用心得
6.1 不要随便把 auth 方式从 SCRAM 改回 md5
很多历史项目还在用 md5,原因无非是“老驱动不支持 SCRAM”或者“之前配好了懒得动”。但在现在的合规与安全要求下,这条线是不该碰的。
如果驱动太老,优先升级驱动;如果驱动升级成本高,至少要限制来源网段,别把0.0.0.0/0放开。我见过有团队为了图方便,把 pg_hba.conf 里的方法改成 trust,然后基本等于打开了裸奔的大门。这是绝对要避免的。
6.2 连接数异常前,先看应用层,再查数据库
我接手的排障里,80% 的连接数问题都不是 PostgreSQL 自身的问题,而是应用连接池配置不合理或者犯了连接泄漏。数据库这边连接数的合理规模一定是“应用实例数 × 每个实例的池大小”,不会有太大的浪涌。如果数据库节点连接数猛涨,第一反应应该去查应用监控,而不是去 kill 进程。
6.3 工具链里,什么值得用
- pg_stat_activity:必查
- pg_stat_statements:能帮你看到哪些 SQL 耗时长、吃连接
- PgBouncer:生产必备
- 日志里的 pg_hba.conf 拒绝记录:排查认证问题最快路径
日常把log_connections和log_disconnections打开,连接异常时能少走很多弯路。
7. 最后想分享的一个观点
说句实在话,PostgreSQL 的连接机制拆到最后,真正影响你日常工作的其实不是那几行源码,而是由此带来的架构取舍。它能撑住复杂查询和数据完整性,但对高并发连接的态度从来都是“记账但不好客”——允许你连很多,但连太多一定出事。
所以在设计应用架构时,我从一开始就会想清楚这样几件事:连接池选哪一层、超时怎么设、事务生命周期应该多短、哪些服务必须共享一个数据库节点还是直接拆库。想明白了,线上很多“莫名其妙”的连接问题,其实都可以在设计阶段就避免掉。
这也是我为什么坚持要把连接机制当作 PostgreSQL 的第一课来讲。它比任何语法细节都更贴近系统的真实脾性。