答案是:查 goroutine 堆栈可确认是否卡在 channel send/recv,高吞吐下消息积压表现为大量 goroutine 停在 chan send 或 chan recv 状态,需结合 pprof 堆栈、len(ch) 水位趋势及 select 使用排查瓶颈。

查 goroutine 堆栈确认是否卡在 channel send / recv
高吞吐下消息积压最直接的表现是 goroutine 数量持续上涨,且大量协程停在 chan send 或 chan recv 状态。这不是猜测,而是可验证的事实。
用 pprof 抓当前堆栈:curl "http://localhost:6060/debug/pprof/goroutine?debug=2"
重点看输出里重复出现的行,比如:
goroutine 1234 [chan send]:
main.processJob(...)
/app/main.go:45
如果同一行反复出现几十甚至上百次,基本锁定是该通道收发逻辑成了瓶颈。
- 若堆栈显示
chan send卡住 → 检查接收端是否处理慢、是否未启动、是否已关闭但没清理等待队列 - 若堆栈显示
chan recv卡住 → 检查发送端是否停止写入、是否因 panic 退出、是否被 context 取消后未 close 通道 - 注意:
runtime.gopark出现在堆栈顶部是正常休眠,但若所有卡住的 goroutine 都停在同一个chan操作,就是积压信号
看缓冲区实际水位而非容量配置
光看 make(chan T, 100) 不代表它“够用”。真正决定是否积压的是运行时的 qcount(当前元素数),不是 dataqsiz(容量)。
Go 运行时不暴露 qcount 给用户代码,但可通过反射或调试器读取——更实用的是用 len(ch) 做采样观察:
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
fmt.Printf("channel len: %d\n", len(myChan))
}
}()
这个值持续接近或等于缓冲容量,说明消费跟不上生产;若长期为 0 但上游仍在发,说明发送端被阻塞在别处(比如 select 里没 fallback)。
-
len(ch)是快照,多线程下不准,但趋势比绝对值更有意义 - 不要用
cap(ch)判断是否满——它只返回初始化时的dataqsiz,和当前状态无关 - 若
len(ch)稳定在高位且缓慢爬升,大概率是下游 goroutine 处理逻辑有 I/O 或锁竞争
用 select + default 拆解阻塞链
无缓冲通道或满缓冲通道上的直接发送,会把整个 goroutine 拖死。尤其在 handler 或 callback 里写 ch ,等于把业务逻辑绑死在通道上。
正确做法是用 select 控制发送行为:
select {
case ch <p>这能防止单条慢消息拖垮整条流水线。但要注意:</p>
- 不能只加
default就完事——得明确丢弃后果是否可接受(比如监控指标可丢,订单通知不可丢) - 若需保序或不丢,改用带超时的
select:case ch - 避免在
default里启动新 goroutine 发送,否则可能引发 goroutine 泄漏
区分死锁、阻塞、锁竞争三类底层成因
现象相似,根因完全不同:死锁是所有 goroutine 全卡住;阻塞是部分 goroutine 卡在 channel 上但程序还在跑;锁竞争是 hchan.lock 被高频争抢,CPU 飙高但吞吐低。
怎么分?看指标:
- 死锁:程序 panic 报
fatal error: all goroutines are asleep - deadlock,pprof 里看不到活跃 goroutine - 阻塞:goroutine 数稳定上升,
len(ch)持续增长,CPU 正常或偏低 - 锁竞争:goroutine 数不涨,但 CPU 占用 >80%,pprof 的
sync.Mutex调用占比高,runtime.chansend和runtime.chanrecv耗时长
锁竞争只能靠减少单个通道的并发写入频率来缓解——要么拆成多个通道(如按 key 分片),要么加中间层做批处理,而不是调大缓冲区。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











