协程泄漏即goroutine持续卡住不退出,铁证是runtime.numgoroutine()持续上涨且pprof debug=2显示大量goroutine阻塞在chan receive/select/semacquire/io wait。

弹幕服务在开播瞬间 CPU 拉满到 300%+,不是代码写得慢,而是调度层或网络层被卡死——先别改业务逻辑,立刻抓 cpu.pprof 看 runtime 占比。
怎么快速确认是不是 goroutine 泄漏导致的 CPU 暴涨
弹幕场景下最常见的是:每条弹幕启一个 goroutine 去做鉴权、过滤、落库,但没设超时或 channel 关闭逻辑,大量 goroutine 卡在 或 <code>http.Do 上不动。此时 CPU 高但实际没干活,全是调度抖动和 GC 扫描压力。
- 立即执行:
curl "http://localhost:6060/debug/pprof/goroutine?debug=2",看输出里有没有成百上千个重复栈(比如全卡在net/http.(*Client).do或database/sql.(*DB).conn) - 重点搜
select { case ——漏写 <code>default就等于无限空转,runtime.mcall会飙升 - 检查所有
for range ch是否配了close(ch);所有http.NewRequestWithContext是否带了context.WithTimeout
为什么开了 16 个 goroutine 处理弹幕,top 却只看到一个线程在跑 CPU
这不是并发没起来,是 Go 的 netpoller 被单点卡死:哪怕你用 http.Server 接弹幕长连接,所有 epoll_wait 都挤在同一个 OS 线程上,其余核完全闲置。
- 验证方式:
ss -tlnp | grep :端口号,如果只显示一个 PID,说明没启用多路复用 - 必须加
SO_REUSEPORT:用net.ListenConfig显式设置syscall.SO_REUSEPORT,再启动多个进程(非 goroutine) - 容器中务必对齐:
GOMAXPROCS=2 ./server启动,不能依赖runtime.NumCPU()返回宿主机核数
火焰图里全是 runtime.futex 和 runtime.semawakeup 怎么办
说明锁竞争严重,典型于弹幕计数器、用户在线状态更新、全局限流器这些共享资源没做分片,所有 goroutine 拼命抢同一把 sync.Mutex。
- 别用全局
map[string]int+mutex做在线人数统计——换sync.Map或按 user_id % 16 分 16 个分片 map - 弹幕频控别用单个
time.Ticker+chan,改用golang.org/x/time/rate.Limiter,每个用户/房间独立实例 - 高频写入结构体(如
struct{ Count int; LastTime time.Time })优先用atomic.AddInt64和atomic.StoreInt64,绕过锁
pprof 显示大量时间花在 runtime.mallocgc,但业务没分配大对象
弹幕消息体小但量极大,json.Unmarshal 每次都 new struct、分配 []byte、触发逃逸,GC 很快就扛不住。
- 禁用默认 JSON:用
go-json(Go 1.20+)或easyjson生成静态方法,避免反射 - 复用解析 buffer:
sync.Pool持有*bytes.Buffer或预分配[]byte,注意别存闭包引用外部变量 - 弹幕协议若可控,直接切到
protobuf:体积更小、解析更快、无反射、GC 压力直降 60%+
真正卡住弹幕服务的,往往不是某一行慢代码,而是调度器被忙循环饿死、netpoller 被单核锁死、或 GC 因高频小对象分配被迫高频 STW——这些点不先揪出来,优化业务逻辑只是给火上浇油。











