朕喜技术(ZhenxiJS)
2026.09.07
1 分钟阅读
Go HTTP 服务 goroutine 泄漏:请求超时后后台 goroutine 永久阻塞(channel 发送阻塞)
问题背景
生产环境 Go HTTP API 服务内存持续增长直至 OOM。用户请求 /api/search 接口,每次请求都设置 1 秒 context 超时,后台 goroutine 执行耗时的数据库查询(实际 2 秒),查询完成后将结果发送到无缓冲 channel。handler 因超时 1 秒先返回并退出,后台 goroutine 在 2 秒后尝试发送结果时没有接收方,resultCh <- result 永久阻塞,导致每次请求泄漏恰好 1 个 goroutine。
技术栈:Go 1.22 / net/http / context / goroutine / channel / net/http/pprof / Docker
关键报错/现象:
- goroutine 数随请求量线性增长且永不回落(从 ~3 涨到 ~33、~103...)
- 容器内存持续上升直至 OOM,被 docker 重启
/debug/pprof/goroutine?debug=1显示大量 goroutine 阻塞在main.main.func1.1/app/main.go:47- 该行代码为
resultCh <- result
复现步骤
# 1. 启动环境
cd scenario/2026-09-07-1607
docker compose up -d --build
# 2. 基线观测
curl http://localhost:18080/api/stats
# goroutines: 3
# 3. 发 30 次并发请求
for i in $(seq 1 30); do curl -s http://localhost:18080/api/search & done; wait
# 4. 观测泄漏
curl http://localhost:18080/api/stats
# goroutines: 33 ← 泄漏了 30 个
# 5. pprof 定位
curl "http://localhost:18080/debug/pprof/goroutine?debug=1"
根本原因
三处设计缺陷叠加:
- 后台 goroutine 内部函数不感知 context 取消:
simulateSlowQuery固定time.Sleep(2s)不检查ctx.Done(),导致 ctx 取消后它仍跑满 2 秒 - 无缓冲 channel 的 send 在无接收方时永久阻塞:handler 超时退出后没人接收
- goroutine 内部没有用
select监听 ctx.Done():盲目尝试 send,无逃生通道
解决办法
修复分三层(防御式编程):
1. 让慢查询感知 context 取消
func simulateSlowQuery(ctx context.Context) string {
select {
case <-time.After(2 * time.Second):
return "db-query-result"
case <-ctx.Done():
return "ctx-cancelled"
}
}
2. resultCh 改为带缓冲 channel
resultCh := make(chan string, 1)
3. goroutine 用 select 同时监听 ctx.Done
go func() {
result := simulateSlowQuery(ctx)
select {
case resultCh <- result:
// 有接收方或缓冲可用,发送成功
case <-ctx.Done():
// ctx 已取消,跳过发送直接退出
}
}()
验证
bash check.sh
验证结果:
基线 goroutines: 3
60 次请求后 goroutines: 3
goroutine 增长量: 0
✅ PASS: goroutine 泄漏已修复
经验与避坑
- 凡启动 goroutine 必须想好退出路径——不是所有 goroutine 都会自然结束
- channel send 是阻塞操作——无缓冲 channel 在没有接收方时永久阻塞,Go 不会自动超时
- context 是贯穿全链路的——handler 设置了超时,后台 goroutine 必须也监听 ctx.Done()
- pprof 定位泄漏:
curl /debug/pprof/goroutine?debug=1会给出所有 goroutine 的阻塞位置堆栈 - 推荐组合策略:ctx-aware 查询 +
select监听取消 + 带缓冲 channel,三者叠加才能彻底防泄漏 - 调试技巧:用
runtime.NumGoroutine()和/api/stats端点实时观测 goroutine 数
