某个服务时不时报警cpu飙升,通过pprof工具查看是sync.(*Mutex).Unlock/sync.(*Mutex).Lock
占用cpu
那就很明显是锁竞争导致的问题了
其中Go版本为 1.13.4
pprof的数据(top)简略如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ----------------------------------------------------------+------------- flat flat% sum% cum cum% calls calls% + context ----------------------------------------------------------+------------- 21.53s 100% | context.(*cancelCtx).Done \usr\local\go\src\context\context.go:330 21.53s 36.82% 36.82% 21.53s 36.82% | sync.(*Mutex).Unlock \usr\local\go\src\sync\mutex.go:186 ----------------------------------------------------------+------------- 16.51s 100% | context.(*cancelCtx).Done \usr\local\go\src\context\context.go:325 16.51s 28.23% 65.05% 16.51s 28.23% | sync.(*Mutex).Lock \usr\local\go\src\sync\mutex.go:74 ----------------------------------------------------------+------------- 57.34s 100% | xxx.go:916 3.54s 6.05% 71.10% 57.34s 98.05% | xxx.go:567 21.55s 37.58% | context.(*cancelCtx).Done \usr\local\go\src\context\context.go:330 18.38s 32.05% | context.(*cancelCtx).Done \usr\local\go\src\context\context.go:325 7.88s 13.74% | runtime.selectnbrecv \usr\local\go\src\runtime\chan.go:636 2.30s 4.01% | runtime.selectnbrecv \usr\local\go\src\runtime\chan.go:635 0.99s 1.73% | context.(*cancelCtx).Done \usr\local\go\src\context\context.go:331 0.93s 1.62% | context.(*cancelCtx).Done \usr\local\go\src\context\context.go:329 0.92s 1.60% | runtime.selectnbrecv \usr\local\go\src\runtime\chan.go:637 0.84s 1.46% | context.(*cancelCtx).Done \usr\local\go\src\context\context.go:324 0.01s 0.017% | context.(*cancelCtx).Done \usr\local\go\src\context\context.go:326
|
原因
对应版本查看 源码
1 2 3 4 5 6 7 8 9
| func (c *cancelCtx) Done() <-chan struct{} { c.mu.Lock() if c.done == nil { c.done = make(chan struct{}) } d := c.done c.mu.Unlock() return d }
|
对应pprof top所显示的Lock与Unlock代码位置
业务代码简化为:
1 2 3 4 5 6 7 8 9 10 11 12
| for { select { case <-dt.ctx.Done(): default: if dt.isPause() { continue }
} }
|
查看源码后问题就很清晰了,Done的频繁执行导致了锁的竞争,引发CPU飙升
而具体原因则是default分支并非阻塞操作,如果dt.isPause() 判断为true,则直接进入下一个循环,执行Done
而isPause方法实现也是简单的比较操作结果(资源是否足够,不足则暂停处理,所以资源吃紧时才会发生),整个 for select 相当于死循环高频执行,从而占满所有的CPU
解决方法
问题其实很简单,当case的条件不满足时,循环将会走default,然后执行下一个循环,这就造成了死循环,写代码时就应当考虑每个分支的执行情况,以避免此类问题。
- 去掉default,改为其它channel的 case 分支,正确使用 for-select
- isPause 为 true 时,sleep一个较短时间,如10ms,降低循环执行的频率
当然,sleep比较low,但最终选择了它,因为简单,够用
参考链接
基于select的多路复用· Go语言圣经
原文链接