Go高并发弹幕服务CPU飙升主因是协程空转或单netpoller堵塞;应启用SO_REUSEPORT分散连接,并在拉取弹幕循环中加入sleep或context控制。

Go高并发弹幕服务CPU瞬间飙到100%,大概率不是并发不够,而是某个goroutine在空转,或者网络事件分发被单个netpoller堵死——这两个原因占线上真实案例的80%以上。
pprof显示大量goroutine停在runtime.netpoll但CPU飙升
这不是“等待IO”,是连接分发层崩了:http.Server.Serve绑定唯一netpoller,所有accept、read都挤在同一个OS线程上。压测时top只看到一个线程跑满100%,而ss -tlnp | grep :端口却显示多个连接堆积在LISTEN队列,netstat -s | grep -i "listen overflows"有非零计数,就坐实了。
- 别用
http.ListenAndServe,它只启一个listener goroutine - 改用
net.ListenConfig显式开启SO_REUSEPORT,让内核把新连接分发到多个Go进程(或多个goroutine,需配合runtime.NumCPU()数量的listener) - 验证是否生效:
ss -tlnp | grep :端口应看到多个PID共享同一端口 - Linux 3.9+、Go 1.11+才支持;macOS可用
SO_REUSEADDR降级,Windows必须fallback到单listener + 队列分发
dlv attach后发现goroutine卡在for循环里死等错误返回
弹幕服务常见模式:从Redis/消息队列拉取弹幕,无数据时直接continue,没sleep也没context.Done()检查。结果就是协程100%占用一个CPU核心空转,pprof CPU profile里dispatchIdleTeu或类似函数占95%以上时间。
- 错误示例:
for { if err := fetchDanmaku(); err != nil { continue } process() } - 修复必须加阻塞或退避:
select { case ,或用time.Sleep(10 * time.Millisecond)兜底 - 别用
time.Sleep硬等,优先走context.WithTimeout或context.WithDeadline控制生命周期 - 如果依赖消息队列,确保消费者能响应
ctx.Done()并主动退出,而不是靠信号杀进程
开了1000个goroutine处理弹幕,CPU利用率反而掉到30%
不是并发少,是调度失配:1000个goroutine争抢同一个锁(比如全局sync.Mutex保护的弹幕计数器),或高频分配小对象触发GC抖动。pprof里runtime.mallocgc或runtime.semawakeup占比高,就是信号。
- 计数类操作一律用
atomic.AddInt64,别包mutex - 高频创建的结构体(如
DanmakuEvent)用sync.Pool,但注意Pool不适合>2KB的大对象 -
map并发读写必panic,读多写少用sync.Map,写较频繁考虑分片map+ 局部聚合 - 热循环里禁用
fmt.Sprintf、strconv.Itoa,整数转字符串用strconv.AppendInt(dst, n, 10)
真正难排查的不是CPU跑满,而是“满得不合理”:比如只有一两个goroutine在忙,其余全卡在netpoll或channel recv,却没人意识到监听器本身成了单点瓶颈。SO_REUSEPORT和context控制这两步,漏掉任何一个,都可能让优化白做。











