数据库连接池:你好好的应用,怎么就开始抽风了?
想象一个场景:你的服务白天跑得挺欢,晚上突然就开始超时,用户开始骂娘,报警邮件塞满邮箱,你一边揉眼睛一边看日志,发现数据库连接数莫名爆表。然后你"灵机一动"把 max_connections 调大,重启服务,世界安静了——大概安静了两天,然后故态复萌。
这不是个例。这是后端开发群里每隔几天就会出现一次的保留节目。而问题的根源,往往不是数据库太弱,而是你的连接池配置得像在玄学抽奖。
先搞清楚:连接池到底在管什么
每次应用要和数据库说话,得先建立一条物理连接。这条连接要经历 TCP 三次握手、TLS 协商(如果你用了加密)、数据库的身份认证……这一套下来,乐观估计 1-5 毫秒,悲观的能飙到 50 毫秒以上。
如果你每个请求都临时建连接,然后用完就扔——恭喜,你刚为 TPS 挖好了一个性能坟墓。连接池的核心逻辑就是:先把连接建立好,放池子里,谁要用就去池子里拿,用完了还回来,而不是每次都重新建立。
这听起来简单,但魔鬼都在细节里。
几个你可能一直配错的参数
1. maximum pool size:不是越大越好
很多教程会告诉你:"连接数不够?调大就行了!"然后你就开始不断调大,直到数据库报 too many connections。
数据库的 max_connections 是个硬上限,但更关键的是:每个连接都要消耗数据库端的内存和 CPU 资源。PostgreSQL 每个连接大概消耗 5-10MB,MySQL 也差不多。如果你有 1000 个空闲连接,数据库光维护这些"僵尸"就要吃掉 5-10GB 内存。
那到底设多少?有个土办法:
maximum_pool_size = (核心数 * 2) + 硬盘IO等待线程数
更精确的做法是看数据库的当前负载。用 pg_stat_activity 看一下平均活跃连接数,然后留 20-30% 的余量。记住:连接池里真正干活的连接数才是有意义的,排队等着的连接只会增加延迟。
2. minimum idle:别把它设成 0
有些同学为了"省资源",把 minimum idle 设为 0。意思是:没请求时把所有连接关掉,有请求时再建。
这个逻辑听起来很环保,实际上是性能灾难。因为你每次请求都要付出建连接的成本,在流量高峰时会出现大量请求同时在等建连接,延迟直接原地起飞。
建议把 minimum idle 设置为 normal 负载下 30-50% 的连接数。这样既能省资源,又能在正常流量来临时快速响应。
3. connection timeout 和 idle timeout:这两个不是一回事
connection timeout 是"等连接的最大时间",超过就报超时错误。idle timeout 是"连接空闲多久后自动关闭"。
常见错误:把 connection timeout 设得很长(比如 30 秒),然后发现数据库偶尔卡一下,应用就夯住了 30 秒才报错。这个值应该设得比较短,5-10 秒足够,因为正常情况下不应该等这么久。
idle timeout 的坑在于:数据库本身有 wait_timeout(比如 MySQL 默认 8 小时),如果你设的 idle timeout 比数据库还长,连接会被数据库先关掉,应用还以为连接活着,拿出来一用才发现已经废了。所以 idle timeout 要比数据库的 wait_timeout 短。
经典名场面:连接泄漏
连接池最怕的不是配置错,而是连接泄漏——拿了连接不还。
症状:连接池大小正常,数据库负载正常,但就是连接不够用,新请求全部排队。去看 pg_stat_activity,发现一堆连接处于 Idle in transaction 状态。
典型代码:
# 错误示例:出了异常连接没还
def get_user(user_id):
conn = pool.get_connection()
user = conn.execute("SELECT * FROM users WHERE id = ?", user_id)
if not user:
return None
# 忘了 conn.close() 或没有用 with 语句
return user
正确的做法是无论成功失败,连接都得还:
# 正确姿势:用上下文管理器
def get_user(user_id):
with pool.connection() as conn:
return conn.execute("SELECT * FROM users WHERE id = ?", user_id)
或者用显式的 try/finally:
def get_user(user_id):
conn = pool.get_connection()
try:
return conn.execute("SELECT * FROM users WHERE id = ?", user_id)
finally:
conn.close() # 确保一定还回去
如果你用 ORM,大多数 ORM 会自动管理连接,但复杂事务里手动开了连接又忘了提交,也会造成泄漏。
连接池的拓扑:直连 vs 中间件
应用直接连数据库,还是经过 PgBouncer/ProxySQL 之类的连接池中间件?
在单体应用时代,直连没问题。但到了微服务时代,如果每个服务都直连数据库,数据库的连接数会成为所有服务的瓶颈之和。
比如你有 10 个服务,每个服务最大 20 连接,数据库就得支持 200 连接。而用 PgBouncer:每个服务先连 PgBouncer( PgBouncer 本身只要少量连接给数据库),数据库只需要 10-20 个连接就够了。
PgBouncer 的三种模式:
- Session pooling:客户端独占连接,适合长连接场景
- Transaction pooling:按事务租借连接,性能最高,但不兼容PREPARE语句
- Statement pooling:每个语句级别,最激进的连接复用,基本只适合 OLTP 场景
大多数场景推荐 Transaction pooling,但如果你的应用大量使用了 prepared statements,得先确认驱动支持 Session 模式下的 prepared statement 复用(PgBouncer 不支持跨连接复用 prepared statements)。
监控:看不到的就管不住
连接池配置的再漂亮,不监控等于盲飞。几个关键指标:
- 活跃连接数 / 总连接数:接近总连接数说明池子不够用
- 等待连接的请求数:大于 0 说明有请求在排队等连接
- 连接获取耗时:正常应该在毫秒级,突然飙升说明连接不够或者数据库响应慢
- Idle 连接数:持续为 0 说明 minimum idle 设少了
大多数语言的主流连接池库都暴露了 JMX/Prometheus 指标,花 20 分钟配一下,出了问题你就是那个能快速定位的人,而不是在群里问"大佬们救命数据库又炸了"的那个。
最后说一句
连接池参数没有银弹。最优配置取决于你的查询复杂度、数据库机器规格、网络延迟和业务高峰特征。但有一点是确定的:拍脑袋设的参数,迟早会还债。
下次看到 too many connections 错误,别急着调大。先问问自己:这到底是连接数不够,还是有泄漏?还是查询太慢导致连接被长期占用?治标不治本的事情做多了,就没人相信你能解决问题了。
调参一时爽,一直调参一直爽——但你总不想以生产事故的方式获得成长吧。