如何用pprof检测golang代码中的死锁

文章介绍了在Golang后端项目中,由于并发处理可能导致的死锁和锁等待问题。通过启用pprof服务,开发者可以监控goroutine的状态,找出导致锁竞争的代码位置,从而定位和解决性能瓶颈。作者提供了一个简单的示例,展示了如何通过pprof识别和分析死锁,并提出了通过脚本定期检查并报警的策略。
摘要由CSDN通过智能技术生成

用golang做的后端项目,为了实现高性能,通常会在运行过程中开启多个goroutine,并行处理并发请求。

并发处理请求提升效率的同时,也引入了资源并发读写的场景,这通常会带来一些问题,比如同时读写一个map会导致程序panic,为此,我们需要为那些不应被多个goroutine同时访问的资源加锁。

一个复杂的后端项目,通常会包含很多很多的锁,我们很难保证我们写的程序不出现死锁,或者长时间的锁等待。锁的问题不像业务逻辑的问题那么容易被发现,它有时可能只是阻塞了某个goroutine,这种情况下整体的功能可能不会受到很大影响(也许只会有零星几个请求会失败,或者响应时间偏大),程序也会正常运行(不会因为某个goroutine的阻塞而崩溃),但对于成功率、响应时间要求较高的场景,这种影响就是不可忽视的。

那么,该如何快速的发现并定位死锁或者长时间锁等待的问题呢?其实很简单,pprof就可以帮我们实现。

下面,我就给出一段会出现长时间锁等待的场景,并一步一步的教你如何定位问题。

首先,创建一个工程,如下图

其中文件main.go 的代码如下

package main
import (
	"bufio"
	"fmt"
	"net/http"
	"net/http/pprof"
	"os"
	"sync"
	"time"
)
func StartHTTPDebuger() {
	pprofHandler := http.NewServeMux()
	pprofHandler.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index))
	server := &http.Server{Addr: ":7890", Handler: pprofHandler}
	go server.ListenAndServe()
}

// 用加锁来实现的安全map
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')
}

// 协程1,加锁后sleep10秒
func worker1()  {
	for {
		Sm.lock.Lock()
		defer Sm.lock.Unlock()
		Sm.data["test"] = 1
		time.Sleep(10*time.Second)
	}
}
// 协程2,加锁后sleep10秒
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?这是绝对不允许的。。。

OK,代码就这些,下面我们把程序跑起来:

 然后,打开浏览器,输入http://localhost:7890/debug/pprof/,选择goroutine:

 你会看到每一个goroutine的调用栈,我这里只截图了死锁相关的goroutine,如下图

 需要注意的是我用红框圈起来的部分,这就是发生锁等待的地方,其中:

 sync.runtime_SemacquireMutex

这一行就是告诉你,这个goroutine,当前正在等待获取锁,而右侧的调用栈,会告诉你,哪里正在等待锁,从我的截图里你可以看到,获取锁的地方是service.go的47行,也就是这里:

程序阻塞在这里等待锁,这是我们意料之中的。

这个例子到这里就结束了,不要因为这个例子简单,就觉得这个方法不行,实际的项目中,你完全可以使用这种方法,检测代码中的死锁,至少我现在负责的程序是这样做的。

那么如何将这个检测的手段加入你现有的程序中呢?很简单,只需要在你程序启动的时候,把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项目 如何排查死锁,算是我的一点心得吧,希望可以帮助到大家,哈哈

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值