连接为什么昂贵
每建立一个数据库 TCP 连接,后端都要付出:
- 三次握手 + TLS 协商(若开 SSL)+ 认证(用户名/密码或证书)
- 分配一个独立后端进程/线程(PostgreSQL 每连接一进程,MySQL 每连接一线程)
- 分配连接级内存(
work_mem、join_buffer、sort_buffer等,乘以连接数) - 维持连接生命周期内的状态与锁
一个连接从建立到销毁,开销约几毫秒到几十毫秒,并常驻数十 MB 内存。当并发上来、又用"即用即建、用完即关"的短连接模式:
1000 QPS × 每请求新建连接 → 数据库每秒握手 1000 次
→ 后端进程频繁 fork/销毁 → CPU 全花在上下文切换
→ 业务 SQL 反而抢不到资源 → 雪崩
这就是连接风暴(connection storm)。表现往往是:应用报错 Too many connections,但数据库 CPU 并不高、QPS 反而掉底。
三层连接池模型
应用进程 ──(应用侧池 HikariCP/Druid)──┐
应用进程 ──(应用侧池)──────────────────┤
应用进程 ──(应用侧池)──────────────────┼──► 中间件池 (PgBouncer/ProxySQL) ──► 数据库(少量真连接)
│
洞见:应用侧池解决"应用内复用",中间件池解决"对数据库收敛"。
关键认知:应用侧连接池 ≠ 数据库侧连接池。前者在应用进程内复用连接(减少握手),但 N 个应用实例 × 每实例 20 连接,对数据库仍是 N×20 条真实连接。要在数据库面前把连接收敛成少量长连接,必须在中间加一层池化代理。
PgBouncer(PostgreSQL 标配)
PgBouncer 是 PostgreSQL 轻量连接池,三种模式:
| 模式 | 行为 | 适用 | 风险 |
|---|---|---|---|
| session(会话级) | 连接占用到客户端断开 | 通用,兼容所有特性 | 不收敛,等于直连 |
| transaction(事务级) | 事务结束即归还池中 | 短事务 Web 服务 | 不能跨事务持有预备语句/临时表/会话变量 |
| statement(语句级) | 每条语句后归还 | 极少用(不支持事务) | 几乎不可用 |
生产首选
transaction模式:把"应用侧 200 连接"收敛成"数据库侧 20 长连接",吞吐反而更高。
配置实战
/etc/pgbouncer/pgbouncer.ini:
[databases]
# 把对 pgbouncer 的请求池化后转发到真实 PG
appdb = host=127.0.0.1 port=5432 dbname=appdb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
# 关键参数
max_client_conn = 2000 # 允许连到 pgbouncer 的客户端总数
default_pool_size = 20 # 每个 (用户,数据库) 对到后端 PG 的连接数
min_pool_size = 5
reserve_pool_size = 5 # 池满时的应急连接
reserve_pool_timeout = 3
server_idle_timeout = 60 # 后端空闲连接回收
server_lifetime = 3600 # 后端连接最长寿命(防长连接老化)
log_connections = 1
userlist.txt 存认证(明文或 md5):
"appuser" "md5xxxx..."
应用改成连 6432 端口即可,代码无需改 SQL(transaction 模式下绝大多数 CRUD 透明)。
⚠️ transaction 模式禁区:跨事务使用 PREPARE、会话级临时表、SET 的会话变量、LISTEN/NOTIFY、 advisory lock 跨事务持有。需要这些特性的连接,单独开一个 session 模式的库入口。
ProxySQL(MySQL 生态)
ProxySQL 是 MySQL 的高性能代理,自带连接池 + 读写分离 + 故障转移:
# 后端真实节点
INSERT INTO mysql_servers(hostgroup_id, hostname, port)
VALUES (10, '10.0.0.11', 3306), (10, '10.0.0.12', 3306);
# 用户(ProxySQL 用自己的密码池化后,用后端账号连真实库)
INSERT INTO mysql_users(username, password, default_hostgroup)
VALUES ('appuser', 'apppw', 10);
# 连接池相关变量
UPDATE global_variables SET variable_value='200'
WHERE variable_name='mysql-server-max_connections'; # 到后端总连接上限
UPDATE global_variables SET variable_variable='32'
WHERE variable_name='mysql-conn-pool-max'; # 每后端连接池大小
LOAD MYSQL SERVERS TO RUNTIME; SAVE MYSQL SERVERS TO DISK;
LOAD MYSQL USERS TO RUNTIME; SAVE MYSQL USERS TO DISK;
ProxySQL 把 N 个应用连接复用到后端少量连接,并支持按规则把读流量导到从库,一举两得。
应用侧连接池(HikariCP / Druid)误区
应用侧池(Java 的 HikariCP、Druid,Go 的 database/sql 内置池)是必须的,但常被配错:
- 池太大:
maximumPoolSize=200,10 个实例 = 2000 连接砸向数据库。经验公式:连接数 ≈ (核心数 × 2) + 磁盘数,或QPS × 平均耗时(s)。多数 Web 服务 10~20 足够。 - 池太小 + 获取超时:
connectionTimeout太短,高峰拿不到连接直接抛异常。应配合后端池化一起调。 - 忘了关连接:
conn.Close()没走defer/try-with-resources,连接泄漏直到打满max_connections。 - 用连接池但当短连接:拿了连接做耗时外部 HTTP 调用,连接被长期占用不出池 → 池饿死。
正确姿势:应用侧池(复用,减握手)+ 中间件池(收敛,对库少量长连接) 两层配合,缺一不可。
监控与告警指标
| 指标 | 来源 | 告警阈值建议 |
|---|---|---|
| 数据库当前连接数 | SHOW PROCESSLIST / pg_stat_activity |
> max_connections×80% |
| 连接等待队列 | ProxySQL mysql_connpool、PgBouncer cl_waiting |
> 0 持续 |
| 连接获取超时次数 | 应用 metrics(HikariCP pendingThreads) |
> 0 |
| 空闲连接占比 | SHOW STATUS LIKE 'Threads_connected' vs 活跃 |
长期 100% 空闲也异常 |
| 单连接时长 | 慢连接/慢事务 | 超阈值 |
PgBouncer 管理库(psql -p 6432 -d pgbouncer)可 SHOW POOLS; SHOW STATS; SHOW CLIENTS; 实时看池状态。
连接泄漏排查流程
SHOW PROCESSLIST(MySQL)/SELECT * FROM pg_stat_activity(PG),按host/user聚合,找到"哪个应用 IP 占了多少连接"。- 看
Command=Sleep且Time很大的连接——典型的"拿了不释放"。 - 到对应应用实例查连接池配置与代码:是否有未
Close的分支(异常路径遗漏 finally)。 - 临时止血:
SET GLOBAL max_connections调大 +KILL异常空闲连接;长期靠修复代码 + 加中间件池收敛。
10 条避坑底线
- 短连接是性能杀手:任何高频访问必须走连接池,禁止"即用即建"。
- 两层池化:应用侧池(复用)+ 中间件池(收敛到库侧少量长连接)。
- PostgreSQL 首选 PgBouncer transaction 模式,把百级连接收敛到十级。
- transaction 模式禁止跨事务用预备语句/临时表/会话变量/
advisory lock。 - MySQL 用 ProxySQL 同时获得池化、读写分离、故障转移。
- 应用侧
maximumPoolSize别拍脑袋设 200,按 CPU/ QPS×耗时 估算,通常 10~20。 - 连接必须显式关闭(
defer Close/try-with-resources),否则泄漏打满max_connections。 - 不得在持有 DB 连接时做耗时外部调用(HTTP/RPC),会饿死连接池。
- 监控
Threads_connected/pg_stat_activity与池等待队列,超 80% 即告警。 - 遇
Too many connections先定位来源 IP 与泄漏代码,别只调大参数掩盖问题。
连接池是数据库稳定性的"节流阀":配对了,百倍并发也能四两拨千斤;配错了,再强的数据库也会被自己人握手握到雪崩。

