开放问答
Go 网关 P50 正常但 P99 偶发 300ms:连接池耗尽 vs 慢 SQL vs GC 毛刺,先查哪个?
该问题的正文、回答与评论。
提问者 程知远-317 · 状态 open
Go性能调优连接池P99线上排查MySQL
【背景】 我在做一个 3 年经验的性能优化,最近帮一个内部团队复盘一个 Go 1.21 网关服务:4C8G 单实例,QPS 约 4k,链路是「网关 → MySQL(读写分离)+ Redis」。P50 稳定在 25ms,P95 在 60ms 左右,但 P99 每隔 10~30 分钟会冒一次 200~400ms 的尖刺,持续 2~5 秒后回落。
【现象】
- 监控显示尖刺期间 MySQL 的 Threads_running 短暂到 30+(连接池上限 50),但慢查询日志里并没有新增的慢 SQL(阈值 100ms)。
- Redis 侧 P99 基本没动,可以排除。
- GC trace 上尖刺时刻确实有一次 STW,但时长只有 8~12ms,撑不起 300ms 的延迟。
- 业务侧看到的是「部分请求超时重试 → 重试又打到 MySQL → 雪崩一小波」,所以体感比 300ms 更糟。
【已尝试的排查】
- 调大连接池到 100:尖刺频率没变,只是雪崩窗口从 3s 拉长到 5s,疑似掩盖问题。
- 给所有下游调用加了超时(200ms)+ 1 次重试:P99 反而更难看,重试放大了抖动。
- 抓了一次 pprof(goroutine + block),尖刺时刻 goroutine 数量正常,block profile 里 database/sql 的 connection 等待占比 40%——但无法判断是「池子真的不够」还是「池子里的连接被慢查询占着」。
- 让 DBA 看了 InnoDB 的 row_lock_waits,尖刺时刻确实有零星等待,但总等待时长 < 50ms,对不上 300ms 的延迟。
【困惑点】 三种假设我都能自圆其说: A. 连接池耗尽 → 请求在 pool 里排队 → 排队时间被计入 P99; B. 某条"没进慢日志"的查询(比如锁等待 + 实际执行 80ms,刚好卡在 100ms 阈值下)拖住了连接; C. GC 毛刺 + 网络抖动叠加(虽然 GC trace 看着不像)。
【期望得到的答案形态】 希望有同样踩过坑的同学能给一个可复现的排查清单:
- 你们线上遇到「P50 正常 / P99 偶发尖刺 / 慢日志干净」这种情况,第一刀切在哪?(比如先加 pool 排队时长指标?还是先抓 MySQL 的 processlist 快照?还是直接看 pprof 的 mutex/block?)
- 有没有一个「20 分钟能跑完」的最小排查方案,能明确区分 A/B/C 三种假设?
- 连接池的「排队时间」在 Go 的 database/sql 里到底怎么观测?我目前只能间接从 goroutine block 里猜,有没有更直接的办法?
我倾向答案是 A,但证据不够硬,想听大家真实案例里的判断依据。
评论
暂无评论。