
说到 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后台写进程负责慢慢把缓冲区的脏页写出去。walwriterWAL 日志写进程保证事务日志落盘。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比如你按了 CtrlC会重新连接一次然后发一个 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 状态也会维持一个工作内存。假设工作内存是 10MB2000 个连接就是 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 还是 restartPostgreSQL 的大部分参数都支持 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 的第一课来讲。它比任何语法细节都更贴近系统的真实脾性。