先把当时的情况摆出来。FastAPI 用 SQLAlchemy 2.0 加 asyncpg 0.29 连 pgbouncer 1.21 的 transaction 模式,压测十分钟后开始报 DuplicatePreparedStatementError: prepared statement __asyncpg_stmt_3__ already exists,重启连接池只能好十分钟。
排查方向其实有限,先把可能性排一排。asyncpg 默认缓存 100 条预处理语句。另一种情况是transaction 模式下连接会被复用给别的会话。
不容易想到的是,多个进程共用同一份 pool 配置。还有一种情况,pgbouncer 没有开启 max_prepared_statements。
上来先在 DSN 上关掉语句缓存
python -c "u='postgresql+asyncpg://u:p@pgb:6432/db?prepared_statement_cache_size=0';print(u)"
然后让 pgbouncer 支持预处理语句
python -c "open('pgbouncer.ini','a').write('max_prepared_statements = 100\n');print('written')"
压测复现并盯 pgbouncer 日志
python load_test.py --concurrency 32 --duration 600
剩下的就是换 session 模式做一次对照
python -c "print('pool_mode = session')"
我习惯把每一步的执行时间和结果记在一边,后面复盘省事。
改之前我把相关日志按时间截了一段存着,事后对时间线用得上。
中途换过一次思路,把范围缩到最小的那一层,反而快了。
恢复的判定其实很直接。压测 30 分钟不再出现 DuplicatePreparedStatement,P99 没有明显上升,连接数保持稳定。
提醒一点。关掉缓存会让每条 SQL 多一次解析开销,读多写少的服务可以只对长事务连接单独关闭。拖得越久,排查成本越高,因为现场状态会被后续操作覆盖。
