大流量冲垮CPU主因是协程空转或单netpoller堵塞;应启用SO_REUSEPORT分散连接,并在拉取弹幕循环中加入sleep或context控制。

大流量瞬间冲垮CPU,基本不是并发能力不够,而是协程空转或单netpoller堵塞——这两个问题占线上弹幕服务CPU飙升案例的80%以上。
pprof 显示大量 goroutine 停在 runtime.netpoll 但 CPU 满载
这不是“等待网络IO”,而是连接分发层崩了:默认 http.ListenAndServe 只启一个 listener goroutine,所有 accept、read 都挤在同一个 OS 线程上。压测时 top 只看到一个线程跑满100%,而 ss -tlnp | grep :端口 显示多个连接堆积在 LISTEN 队列,netstat -s | grep -i "listen overflows" 有非零计数,就坐实了。
- 必须改用
net.ListenConfig显式开启SO_REUSEPORT,让内核把新连接分发到多个 Go 进程(或多个 listener goroutine) - Linux 3.9+、Go 1.11+ 才支持;macOS 可降级用
SO_REUSEADDR,Windows 必须 fallback 到单 listener + 队列分发 - 验证是否生效:
ss -tlnp | grep :端口应看到多个 PID 共享同一端口
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硬等,优先走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)
真正难排查的是那种“看起来一切正常”的空转:没有 panic,没有 error 日志,goroutine 数量稳定,但 CPU 就是卡在 95% 不下来——这时候要盯死 runtime.netpoll 和你自己的拉取循环,而不是去加机器或调 GOMAXPROCS。


















