分享一个线上遇到的死锁问题,什么, golang 也会有死锁?
思路排查
Dump 堆栈很重要
线上某个环境发现 S3 上传请求卡住,请求不返回,卡了30分钟,长时间没有发现有效日志。一般来讲,死锁问题还是好排查的,因为现场一般都在。类似于 c 程序,遇到死锁问题都会用 pstack 看一把。golang 死锁排查思路也类似(golang 不适合使用 pstack,因为 golang 调度的是协程,pstack 只能看到线程栈),我们其实是需要知道 S3 程序里 goroutine 的栈状态。golang 遇到这个问题我们有两个办法:
- 方法一:条件允许的话,gcore 出一个堆栈,这个是最有效的方法,因为是把整个 golang 程序的内存镜像 dump 出来,然后用 dlv 分析;
- 方法二:如果你提前开启 net/pprof 库的引用,开启了 debug 接口,那么就可以调用 curl 接口,通过 http 接口获取进程的状态信息;
需要注意到,golang 程序和 c 程序还是有点区别,goroutine 非常多,成白上千个 goroutine 是常态,甚至上万个也不稀奇。所以我们一般无法在终端上直接看完所有的栈,一般都是把所有的 goroutine 栈 dump 到文件,然用 vi 打开慢慢分析。
- 调试这个 core 文件,意图从堆栈里找到些东西,由于堆栈太多了,所以就使用
gorouties -t -u
这个命令,并且把输出 dump 到文件; curl xxx/debug/pprof/goroutine
关键思路
成千上万个 goroutine ,直接显示到终端是不合适的,我们 dump 到文件 test.txt,然后分析 test.txt 这个文件。去查找发现了一些可疑堆栈,那么什么是可疑堆栈?重点关注加锁等待的堆栈,关键字是 runtime_notifyListWait
、semaphore
、sync.(*Cond).Wait
、Acquire
这些阻塞场景才会用到的,如果业务堆栈上出现这个加锁调用,就非常可疑。
划重点:
- 留意阻塞关键字
runtime_notifyListWait
、semaphore
、sync.(*Cond).Wait
、Acquire
; - 业务堆栈(非 runtime 的一些内部堆栈)
统计分析发现,有 11 个这个堆栈都在这同一个地方,都是在等同一把锁blockingKeyCountLimit.lock
,所以基本确认了阻塞的位置,就是这个地方阻塞到了所有的请求,但是这把锁我们使用 defer 释放的,使用姿势如下:
// do someting
lock.Acquire(key)
defer lock.Release(key)
// 以下为锁内操作;
blockingKeyCountLimit 是我们封装针对 key 操作流控对象。举个例子,如果 limit == 1,key为 “test” 在 g1 上 Acquire 成功,g2 acquire(“test”) 就会等待,这个可以算是我们优化的一个逻辑。如果 limit == 2,那么就允许两个人加锁到,后面的人都等待。
从代码来看,函数退出一定会释放的,但是偏偏现在锁就卡在这个地方,所以就非常奇怪。我们先找哪个 goroutine 占着这把锁不释放,看看能不能搞清楚怎样导致这里抢不到锁的原因。
通过审查业务代码分析,发现可能的源头函数(这个函数是向后端请求的函数):
api.(*Client).getBytesNolc
确认是 getBytesNolc
这个函数执行的操作,那么大概率就是卡在这个地方了。用这个 getBytesNolc
字符串搜索堆栈,找下是哪个堆栈 ?搜索到这个堆栈 goroutine 19458