如何用pprof检测golang代码中的死锁
用golang做的后端项目,为了实现高性能,通常会在运行过程中开启多个goroutine,并行处理并发请求。并发处理请求提升效率的同时,也引入了资源并发读写的场景,这通常会带来一些问题,比如同时读写一个map会导致程序panic,为此,我们需要为那些不应被多个goroutine同时访问的资源加锁。一个复杂的后端项目,通常会包含很多很多的锁,我们很难保证我们写的程序不出现死锁,或者长时间的锁等待。锁
用golang做的后端项目,为了实现高性能,通常会在运行过程中开启多个goroutine,并行处理并发请求。
并发处理请求提升效率的同时,也引入了资源并发读写的场景,这通常会带来一些问题,比如同时读写一个map会导致程序panic,为此,我们需要为那些不应被多个goroutine同时访问的资源加锁。
一个复杂的后端项目,通常会包含很多很多的锁,我们很难保证我们写的程序不出现死锁,或者长时间的锁等待。锁的问题不像业务逻辑的问题那么容易被发现,它有时可能只是阻塞了某个goroutine,这种情况下整体的功能可能不会受到很大影响(也许只会有零星几个请求会失败,或者响应时间偏大),程序也会正常运行(不会因为某个goroutine的阻塞而崩溃),但对于成功率、响应时间要求较高的场景,这种影响就是不可忽视的。
那么,该如何快速的发现并定位死锁或者长时间锁等待的问题呢?其实很简单,pprof就可以帮我们实现。
下面,我就给出一段会出现长时间锁等待的场景,并一步一步的教你如何定位问题。
首先,创建一个工程,添加两个go文件:
其中,debug.go实现pprof的http服务,监听本地的7890端口:
package main
import (
"net/http"
"net/http/pprof"
)
const (
pprofAddr string = ":7890"
)
func StartHTTPDebuger() {
pprofHandler := http.NewServeMux()
pprofHandler.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
server := &http.Server{Addr: pprofAddr, Handler: pprofHandler}
go server.ListenAndServe()
}
然后我们在service.go中模拟一个多协程并发访问一个加锁资源的场景:
package main
import (
"bufio"
"fmt"
"os"
"sync"
"time"
)
type SafeMap struct{
lock sync.Mutex
data map[string]interface{}
}
var Sm = &SafeMap{data: map[string]interface{}{}}
func main() {
//让pprof服务运行起来
go StartHTTPDebuger()
//模拟两个处理请求的goroutine
go worker1()
go worker2()
//下面3行只是为了让主协程不退出
fmt.Println("input anything to quit.")
reader := bufio.NewReader(os.Stdin)
reader.ReadString('\n')
}
func worker1() {
for {
Sm.lock.Lock()
defer Sm.lock.Unlock()
Sm.data["test"] = 1
time.Sleep(10*time.Second)
}
}
func worker2() {
for {
Sm.lock.Lock()
defer Sm.lock.Unlock()
Sm.data["test"] = 2
time.Sleep(10*time.Second)
}
}
这里面,worker1和worker2两个goroutine都会先去获取Sm的锁,然后占用锁10s,不管谁占用到了锁,另一个协程都将阻塞10s,假设这是一个对响应时间要求很高的场景,一个请求处理10s?这是绝对不允许的。。。
欧克,代码就这些,下面我们把程序跑起来:
然后,打开浏览器,输入http://localhost:7890/debug/pprof/,选择goroutine:
你会看到每一个goroutine的调用栈:
你需要注意的是我用红框圈起来的部分,这就是发生锁等待的地方,其中:
sync.runtime_SemacquireMutex
这一行就是告诉你,这个goroutine,当前正在等待获取锁,而右侧的调用栈,会告诉你,哪里正在等待锁,从我的截图里你可以看到,获取锁的地方是service.go的34行,也就是这里:
程序阻塞在这里等待锁,这是我们意料之中的。
这个例子到这里就结束了,不要因为这个例子简单,就觉得这个方法不行,实际的项目中,你完全可以使用这种方法,检测代码中的死锁,至少我现在负责的程序是这样做的。
那么如何将这个检测的手段加入你现有的程序中呢?很简单,只需要在你程序启动的时候,把pprof服务用一个单独的goroutine跑起来,然后,你可以写一个脚本,定时去检查pprof/goroutine页面有没有出现sync.runtime_SemacquireMutex,如果出现了,你可以采取一些报警措施,报警怎么做就因人而异了,我的项目是会在发现锁等待的时候,调用程序的http接口,然后程序把pprof的内容打印到日志,并通过我们公司的接口触发报警,脚本可以发出来供大家参考:
#!/bin/bash
while true; do
pprof=$(curl http://localhost:7890/debug/pprof/goroutine?debug=1)
lock=$(echo $pprof | grep sync.runtime_SemacquireMutex | wc -l)
if [[ $lock -gt 0 ]]; then
curl -H "Content-Type: text/plain" -X POST -d "$pprof" "http://localhost:1234/deadlock"
fi
sleep 10
done
需要注意的是,出现sync.runtime_SemacquireMutex并不一定是死锁或者长时间锁等待,可能只是你的脚本请求页面的那个时刻刚好有锁处于等待状态,但这个锁可能马上就被获取到了,这其实是非常正常的,所以我们可以将报警的条件设置的苛刻一些,比如连续5次都获取到了sync.runtime_SemacquireMutex,这个就有可能真的是某个goroutine连续50s没有获取到锁,这时候你就得好好看看了。
以上所说的方法可能并不是最优的,但绝对是一种可行的方法,如果你在这方面没有什么经验,完全可以直接把它应用到你的项目中,当然如果你有更好的方法,也欢迎在评论中吐槽我,大家一起交流,一起进步~
<<<<<<<<<<<<<<<<<<<<<<<<<<
补充:
相信看我这篇文章的人,一定是实际项目中遇到了死锁,所以你一定不满足于只是发现哪里锁住了,还想知道到底怎么死锁的,不要慌,可以参考我的这一篇golang项目 如何排查死锁,算是我的一点心得吧,希望可以帮助到大家,哈哈
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)