用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项目 如何排查死锁,算是我的一点心得吧,希望可以帮助到大家,哈哈

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐