数据库运维9 min read次阅读

数据库连接池与连接风暴治理实战

连接为什么昂贵

每建立一个数据库 TCP 连接,后端都要付出:

  • 三次握手 + TLS 协商(若开 SSL)+ 认证(用户名/密码或证书)
  • 分配一个独立后端进程/线程(PostgreSQL 每连接一进程,MySQL 每连接一线程)
  • 分配连接级内存(work_memjoin_buffersort_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; 实时看池状态。

连接泄漏排查流程

  1. SHOW PROCESSLIST(MySQL)/ SELECT * FROM pg_stat_activity(PG),按 host/user 聚合,找到"哪个应用 IP 占了多少连接"。
  2. Command=SleepTime 很大的连接——典型的"拿了不释放"。
  3. 到对应应用实例查连接池配置与代码:是否有未 Close 的分支(异常路径遗漏 finally)。
  4. 临时止血:SET GLOBAL max_connections 调大 + KILL 异常空闲连接;长期靠修复代码 + 加中间件池收敛。

10 条避坑底线

  1. 短连接是性能杀手:任何高频访问必须走连接池,禁止"即用即建"。
  2. 两层池化:应用侧池(复用)+ 中间件池(收敛到库侧少量长连接)。
  3. PostgreSQL 首选 PgBouncer transaction 模式,把百级连接收敛到十级。
  4. transaction 模式禁止跨事务用预备语句/临时表/会话变量/advisory lock
  5. MySQL 用 ProxySQL 同时获得池化、读写分离、故障转移。
  6. 应用侧 maximumPoolSize 别拍脑袋设 200,按 CPU/ QPS×耗时 估算,通常 10~20。
  7. 连接必须显式关闭defer Close/try-with-resources),否则泄漏打满 max_connections
  8. 不得在持有 DB 连接时做耗时外部调用(HTTP/RPC),会饿死连接池。
  9. 监控 Threads_connected/pg_stat_activity 与池等待队列,超 80% 即告警。
  10. Too many connections定位来源 IP 与泄漏代码,别只调大参数掩盖问题。

连接池是数据库稳定性的"节流阀":配对了,百倍并发也能四两拨千斤;配错了,再强的数据库也会被自己人握手握到雪崩。

分享:

相关文章

评论区