FastAPI服务半夜又挂了?先别急着重启,查查你的数据库连接池“池子”是不是漏了
案例模拟那是某个凌晨三点手机跟抽风一样狂震。线上服务接口响应时间从 50ms 飙升到 30s然后直接 timeout。你睡眼惺忪地连上服务器一看进程还在但就是死活连不上数据库。日志里躺着一堆 QueuePool limit of size ... overflow ... reached 当时就懵了心想“我这大半夜的又没流量连接池怎么就满了呢”重启大法好啊服务瞬间恢复。但这就像止疼药治标不治本。第二天你扒了一层皮才发现原来是 MySQL 那边的 wait_timeout 把空闲连接给杀了而 SQLAlchemy 的连接池还傻乎乎地以为连接活着拿起来就用结果一用一个不吱声。所以官方文档还是要仔细看啊以前总觉得默认参数就是最佳实践结果被现实狠狠打脸。数据库连接池这玩意儿没有银弹默认配置只保证你能跑不保证你跑得稳。 先听个故事为啥非得要个“池子”好咱们先来点轻松的。把数据库想象成一家火爆的餐厅后厨每个请求就是一个来吃饭的客人。-没有连接池的时候来一个客人服务员现场跑去后厨招一个厨师建立TCP连接握手认证做完菜立马把厨师开了关闭连接。客人一多光雇厨师和开厨师的时间就占了90%后厨门都要被挤爆了。-有连接池的时候提前雇好一群厨师在后厨待命pool_size来客人了直接分配一个厨师去炒菜炒完了厨师不辞退而是回到休息室等着回到池子。这效率天差地别。所以连接池的核心就是仨字复用、省心、有序。减少了 TCP 握手的开销还能限制你最多能开多少连接防止把数据库给冲垮。 四个参数保你平安重点FastAPI 底下通常用 SQLAlchemy而 SQLAlchemy 的队列池QueuePool有四个参数你必须在配置里混个脸熟。别怕我用大白话给你翻译。- pool_size5 常驻厨师数量。后厨里常年保持待命的“正式工”。设太小了吞吐量上不去设太大了数据库那边该骂娘了数据库总连接数有限制。- max_overflow10 临时工上限。高峰期客人太多正式工忙不过来可以招点临时工。但最多只能再招这么多。高峰期一过临时工会自动辞退。- pool_recycle3600 健康体检时间。每个连接厨师上岗满1小时不管有没有活强制让他退休换新人。这就是为了防止数据库那边因为长时间没动静偷偷把连接给掐了比如MySQL默认8小时wait_timeout咱们自己先主动点。- pool_pre_pingTrue 上岗前喊一嗓子。从池子里拿连接之前先发个简单的 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 依