先别急着重启,查查你的数据库连接池“池子”是不是漏了
🎯 案例模拟
那是某个凌晨三点,手机跟抽风一样狂震。线上服务接口响应时间从 50ms 飙升到 30s,然后直接 timeout。
你睡眼惺忪地连上服务器,一看进程还在,但就是死活连不上数据库。日志里躺着一堆 QueuePool limit of size ... overflow ... reached ,当时就懵了,心想:“我这大半夜的又没流量,连接池怎么就满了呢?”
重启大法好啊,服务瞬间恢复。但这就像止疼药,治标不治本。第二天你扒了一层皮才发现,原来是 MySQL 那边的 wait_timeout 把空闲连接给杀了,而 SQLAlchemy 的连接池还傻乎乎地以为连接活着,拿起来就用,结果一用一个不吱声。
所以,官方文档还是要仔细看啊,以前总觉得默认参数就是最佳实践,结果被现实狠狠打脸。数据库连接池这玩意儿,没有银弹,默认配置只保证你能跑,不保证你跑得稳。
🍜 先听个故事:为啥非得要个“池子”?
好,咱们先来点轻松的。把数据库想象成一家火爆的餐厅后厨,每个请求就是一个来吃饭的客人。
-没有连接池的时候:
来一个客人,服务员现场跑去后厨招一个厨师(建立TCP连接,握手认证),做完菜立马把厨师开了(关闭连接)。客人一多,光雇厨师和开厨师的时间就占了90%,后厨门都要被挤爆了。
-有连接池的时候:
提前雇好一群厨师在后厨待命(pool_size),来客人了直接分配一个厨师去炒菜,炒完了厨师不辞退,而是回到休息室等着(回到池子)。这效率,天差地别。
所以连接池的核心就是仨字:复用、省心、有序。减少了 TCP 握手的开销,还能限制你最多能开多少连接,防止把数据库给冲垮。
🔧 四个参数,保你平安(重点!)
FastAPI 底下通常用 SQLAlchemy,而 SQLAlchemy 的队列池(QueuePool)有四个参数,你必须在配置里混个脸熟。别怕,我用大白话给你翻译。
- pool_size=5 :常驻厨师数量。后厨里常年保持待命的“正式工”。设太小了吞吐量上不去,设太大了数据库那边该骂娘了(数据库总连接数有限制)。
- max_overflow=10 :临时工上限。高峰期客人太多,正式工忙不过来,可以招点临时工。但最多只能再招这么多。高峰期一过,临时工会自动辞退。
- pool_recycle=3600 :健康体检时间。每个连接(厨师)上岗满1小时,不管有没有活,强制让他退休,换新人。
这就是为了防止数据库那边因为长时间没动静,偷偷把连接给掐了(比如MySQL默认8小时wait_timeout),咱们自己先主动点。
- pool_pre_ping=True :上岗前喊一嗓子。从池子里拿连接之前,先发个简单的 SELECT 1 探探路。如果对面没反应(连接被杀了),直接扔掉换个新的。
这个参数是救命稻草,我强烈建议你设为 True,虽然有一丢丢性能开销,但跟半夜被叫起来相比,这都不是事儿。
再说个容易翻车的点:
pool_size 到底设多少?网上那些抄来抄去的公式 (CPU核心数 * 2) + 有效磁盘数 是个参考,但千万别当圣旨。
根据以往的经验,对于 FastAPI 这种异步框架,因为并发高,连接占用时间短, pool_size 反而可以比同步框架(Django)设得小一点,比如 10-20 之间,然后靠 max_overflow 去扛突发流量。具体是多少,往下看监控部分。
👀 光说不练假把式,监控得支棱起来
是不是以为参数配好就完了?Too young!参数调优没有监控,那就跟盲人开车一样——全靠运气。咱们得把连接池的底裤扒开看看。
好在 SQLAlchemy 留了后门——事件监听。我们可以写几行代码,把连接池的状态暴露给 Prometheus。
# 这段代码可以直接塞进你的 FastAPI 启动事件里 from sqlalchemy import event from prometheus_client import Gauge # 定义几个仪表盘指标 pool_size_gauge = Gauge('db_pool_size', 'Current pool size') checked_out_gauge = Gauge('db_pool_checked_out', 'Connections currently in use') overflow_gauge = Gauge('db_pool_overflow', 'Current overflow connections') def collect_pool_metrics(db_pool): pool_size_gauge.set(db_pool.size()) checked_out_gauge.set(db_pool.checkedout()) overflow_gauge.set(db_pool.overflow()) # 监听从池子里拿连接的事件(checkout) @event.listens_for(engine, 'checkout') def receive_checkout(dbapi_conn, conn_record, conn_proxy): collect_pool_metrics(conn_proxy._pool) # 监听归还连接的事件(checkin) @event.listens_for(engine, 'checkin') def receive_checkin(dbapi_conn, conn_record): collect_pool_metrics(conn_record._pool)把这几个指标挂到 /metrics 端点,用 Grafana 画个图,你会看到一个全新的世界:
-场景一:checked_out 长时间逼近 pool_size + max_overflow,并且出现等待。结论:池子太小了,加人!
-场景二:overflow 常年大于0,高峰期更离谱。结论:把 pool_size 调大点,别老招临时工,不稳定。
-场景三:半夜一看图,checked_out 归零,但 overflow 偶尔跳一下。结论:没事,那是 pool_pre_ping 在体检呢。
🚨 两个经典陷阱,早看早脱身
1. 连接泄漏(Leak):
用了 Depends 注入 Session 但忘了关闭?或者在一个无限循环里开了连接没 commit/close?这就是典型的“厨师借出去没还回来”。时间一长,池子里的连接全被占着,新请求直接排队等死。
解法:死都要用 with 上下文管理器,或者 FastAPI 里用 yield 依赖,保证请求结束把连接还回去。
2. 首次请求卡顿:
服务刚启动,池子是空的。第一个倒霉蛋请求进来时,不仅要处理业务,还得负责把 pool_size 个连接建立起来。这就是“冷启动”的代价。
解法:在 startup 事件里,手动执行一次 await db.execute("SELECT 1") ,强迫连接池提前预热,别让第一个用户当冤大头。
