Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >死锁问题排查

死锁问题排查

作者头像
数据小冰
发布于 2022-08-15 07:05:31
发布于 2022-08-15 07:05:31
1.2K01
代码可运行
举报
文章被收录于专栏:数据小冰数据小冰
运行总次数:1
代码可运行
问题背景

最近有同事说平台的某个服务出现超时异常,让我帮忙看下原因。我进入平台后触发了该服务,并没有发现超时异常,那可能是在特定操作场景下会出现或者是一个非必现问题。跟同事沟通之后,找到了复现的流程。

接下来开始定位问题可能出现在哪个模块,进入后台之后,触发服务通过top命令检查后台资源使用情况,发现CPU、内存和负载情况都是正常的。嗯,此种方法分析不出什么。

既然已知道异常服务,那可以从这里入手进行分析,又与同事沟通一番,确定了与该服务相关的一些后台模块,接下来重点排查这些模块。首先通过pprof抓取了这些模块的堆栈日志,Go提供了net/http/pprof和runtime/pprof两个包用于性能测评分析,前者通常用于web服务器的性能分析,后者通常用于普通代码的性能分析。下面是出现问题的参考日志,关键点已包含其中,因为原日志不方便展示。

排查方法

日志中出现了sync.(*Mutex).Lock的关键字,怀疑是程序死锁了,结合代码,在mutex.go 138行进入了lockSlow,然后goroutine被挂起了,也就是说在doConcat中获取锁失败了。然后搜索下程序中有哪些地方用到了doConcat中的锁,这些都是可疑的地方。最后分析这些的可疑的地方,基本可以确定是在哪里死锁了。当然这里的日志只是个示例,实际堆栈文件可能很大,有很多地方需要分析,分析起来也比较麻烦,我们可以写个脚本提取出关键的goroutine以及上下文信息,再进行分析。不过github上已有动态检测死锁的工具https://github.com/sasha-s/go-deadlock,使用方法见下面的死锁检测工具小节。

问题本质

上面问题的根因是死锁导致的,死锁也是计算机中常见出现的问题。这里再来回顾下死锁的定义:

❝死锁是指两个或两个以上的进程或线程在执行的过程中,由于竞争资源或者彼此通信而造成的一种阻塞程序不能推进的现象,如果不借助外部作用,它们会无法推进下去。 ❞

产生死锁的原因,通常是系统资源不足、程序的执行顺序不当和资源分配不当等导致的。

产生死锁有四大必要条件,注意是必要条件,就是说如果出现了死锁,下面四个条件必定成立,如果有其中至少一个条件不满足,则不会出现死锁。

  • 互斥条件:一个资源每次只能被一个进程或线程使用
  • 请求与保持条件:一个进程或线程因请求资源而阻塞时,对已获得的资源保持占有不释放
  • 不可剥夺条件:进行或线程已获得的资源,在没有使用完之前,不能强行剥夺
  • 循环等待条件:若干进程或线程之间形成一种头尾相接的循环等待资源状态

如何预防死锁?可以通过破坏死锁产生的4个必要条件来预防死锁,打破其中之一就可以避免死锁产生。那挑选比较容易的进行破坏,由于资源互斥是资源使用时的固有特性无法改变,所以破坏互斥条件这条直接放弃。

  1. 破坏循环等待条件,在申请资源获取锁时保持一致的顺序,例如下面的程序,goroutine 1先获取A锁然后获取B锁,而goroutine 2先获取B锁然后获取A锁,会形成环形依赖,我们可以调整程序的顺序,让它们获取锁的顺序保持一致。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// goroutine 1
A.Lock() 
...
B.Lock() 
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// goroutine 2
B.Lock() 
...
A.Lock() 
  1. 破坏不可剥夺条件,通过外部程序检测,如果出现了死锁,强行释放掉锁,例如在数据库中,检测到死锁之后强制让某个事务回滚。
  2. 破坏请求与保持条件,进程或线程执行前,先一次性申请其在整个运行期间所需的全部资源,或者一段时间获取不到资源尝试主动放弃已经获得的资源。
如何避免

上个小节只是从原理层面说了怎么预防死锁,在我们的实际工作中,怎么做才能够尽早发现死锁问题并进行消除呢?小编想到了下面三种方法,codereview、编写单元测试和通过死锁检测工具检查。

codereview

通过代码评审,有经验的工程师对代码进行review之后,一些比较明显的存在死锁代码逻辑是容易发现的,当然有些逻辑隐藏的比较深,一般很难发现。往往改动代码引发的死锁问题比较容易出现,像本文中出现的问题就是代码改动导致的,添加功能需求的时候关注点集中在了业务逻辑上,容易忽视锁的问题。

编写单元测试

编写单元测试,执行单元测试对于死锁问题是很容易发现的,因为在运行单元测试的过程中,程序会卡死结束不了,可以很快暴露问题。

死锁检测工具

上面两种方法是通过流程制度来约束减少死锁问题的发生,通过死锁检测工具自动帮助我们检测也是一种有效的手段。这里介绍一款在程序运行的时候检测是否可能存在死锁的工具,代码地址https://github.com/sasha-s/go-deadlock。注意这个检测工具不要在生产环境中使用,因为它的实现是有性能开销的。

使用实例如下,像用dead.Mutex替代sync.Mutex,go-deadlock提供了dead.Mutex和dead.RWMutex. 下面的程序存在锁重入,如果用的是sync.Mutex会出现卡死,下面程序会检测了存在死锁,直接退出了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main

import (
 "fmt"
 "github.com/sasha-s/go-deadlock"
 "time"
)

// 锁重入测试
func reentrantLockTest(mu *deadlock.Mutex){
 fmt.Println("called reentrantLockTest")

 mu.Lock()
 defer mu.Unlock()

 do(mu)
}

func do(mu *deadlock.Mutex){
 fmt.Println("called do")

 mu.Lock()
 defer mu.Unlock()

 // do something
 fmt.Println("do something")
}

func main(){
 var mu deadlock.Mutex
 reentrantLockTest(&mu)

 time.Sleep(time.Minute)
}

上面的程序运行结果如下,输出结果指出了代码中存在Recursive locking,并指明了死锁的逻辑存在的两处位置。

再来看另一种情况,两个goroutine加锁顺序不当导致的死锁问题。goroutine 1和goroutine 2都在对lock1和lock2加锁,不过它们获取锁的顺序是不同的,一个先获取lock1在获取lock2,另一个先获取lock2在获取lock1.这会导致它们形成了一个环,都无法推进。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main

import (
 "fmt"
 "github.com/sasha-s/go-deadlock"
 "time"
)

func main(){
 var (
  lock1,lock2 deadlock.Mutex
 )

 go func(){
  lock1.Lock()
  defer lock1.Unlock()

  time.Sleep(time.Second)

  lock2.Lock()
  defer lock2.Unlock()

  fmt.Println("func1 end")
 }()

 go func(){
  lock2.Lock()
  defer lock2.Unlock()

  time.Sleep(time.Second)

  lock1.Lock()
  defer lock1.Unlock()

  fmt.Println("func2 end")
 }()

 time.Sleep(time.Second*10)
}

运行上面的程序,也检测出了存在死锁的问题,并提示是加锁顺序不一致导致的死锁。

还有一种情况,程序上锁之后忘了释放,导致其他获取此锁的goroutine一直卡死,这种情况go-deadlock是通过设置goroutine卡死的时间来提示可能存在死锁,默认超时时间是30秒。之所以说提示可能存在死锁,是因为存在加锁之后之后的处理逻辑执行时间很长,然后才释放,会被误判为死锁。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main

import (
 "fmt"
 "github.com/sasha-s/go-deadlock"
 "time"
)

func main(){
 var lock deadlock.Mutex

 go func(){
  lock.Lock()
  // 不释放锁
  // defer lock.Unlock()

  fmt.Println("func1 end")
 }()

 time.Sleep(time.Second)

 go func(){
  lock.Lock()
  defer lock.Unlock()

  fmt.Println("func2 end")
 }()

 select {

 }
}

上面的程序加锁之后没有释放锁,会导致第二个goroutine一直无法运行,运行输出结果如下:

做一个总结,对上面三种情形,go-deadlock的处理方法如下:

  1. 情形1,存在锁重入,即goroutine 1获取了A互斥锁,在没有释放前,然后又在后序处理中尝试获取A互斥锁。

处理方法:go-deadlock用一个map记录了到当前为止所有还未释放的锁,map的key为*deadlock.Mutex类型,value为堆栈信息和gid信息。然后检查如果map中锁已存在,并且当前尝试获取锁的goroutine id即gid相同,说明存在重入获取锁。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (l *lockOrder) preLock(stack []uintptr, p interface{}) {
 ...
 gid := goid.Get()
 l.mu.Lock()
 for b, bs := range l.cur {
  if b == p {
   if bs.gid == gid {
      // 处理重入情形
   }
   continue
  }
  ...
 }
 l.mu.Unlock()
}

  1. 情形2,获取锁的顺序不当导致的死锁,goroutine 1先获取A锁然后获取B锁,goroutine 2先获取B锁,然后获取A锁。处理方法:go-deadlock中用order map记录了获取锁的先后顺序,key为有序的两个锁,A锁-B锁会存入order,当又存在B锁-A锁时,说明存在顺序不一致。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func (l *lockOrder) preLock(stack []uintptr, p interface{}) {
 ...
 gid := goid.Get()
 l.mu.Lock()
 for b, bs := range l.cur {
  ...
  if s, ok := l.order[beforeAfter{p, b}]; ok {
   ....
            // 情形2,存在顺序不一致
  }
        // 记录下顺序
  l.order[beforeAfter{b, p}] = ss{bs.stack, stack}
  ...
 }
 l.mu.Unlock()
}

在如下的环路中,lockA --> lock B --> lock C, l.order会记录两两lock之前的先后关系,例如会存储下面的顺序,现在goroutine 1重新调度时,会检查l.order中是否存 lock A-lock B或C, 如果有说明顺序不一致。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
l.order[lock A-lock B]
l.order[lock A-lock C]
l.order[lock B-lock C]

3.情形3:获取了锁但没有释放,会导致其他goroutine一直拿不到,这种情况没法严格检查,go-deadlock通过检查goroutine卡主时间来判断的,默认是30秒。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func lock(lockFn func(), ptr interface{}) {
 ...
 if Opts.DeadlockTimeout <= 0 {
  lockFn()
 } else {
  ch := make(chan struct{})
  currentID := goid.Get()
        // 开启一个goroutine 启动定时器检查
  go func() {
   for {
    t := time.NewTimer(Opts.DeadlockTimeout)
    defer t.Stop() 
    select {
    case <-t.C:
    ...
    case <-ch:
     return
    }
   }
  }()
  lockFn()
  postLock(stack, ptr)
  close(ch)
  return
 }
 postLock(stack, ptr)
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-01-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Golang sync.Cond 简介与用法
Cond 实现了一个条件变量,在 Locker 的基础上增加的一个消息通知的功能,保存了一个通知列表,用来唤醒一个或所有因等待条件变量而阻塞的 Go 程,以此来实现多个 Go 程间的同步。
恋喵大鲤鱼
2019/08/01
5.9K0
Golang 基础:底层并发原语 Mutex RWMutex Cond WaitGroup Once等使用和基本实现
上一篇 《原生并发 goroutine channel 和 select 常见使用场景》 介绍了基于 CSP 模型的并发方式。
张拭心 shixinzhang
2022/05/10
4410
Golang 基础:底层并发原语 Mutex RWMutex Cond WaitGroup Once等使用和基本实现
《Go语言程序设计》读书笔记(七)基于共享变量的并发
上面的例子里Unlock会在return语句读取完balance的值之后执行,所以Balance函数是并发安全的。
KevinYan
2020/01/13
3940
这可能是最容易理解的 Go Mutex 源码剖析
上一篇文章《一文完全掌握 Go math/rand》,我们知道 math/rand 的 global rand 有一个全局锁,我的文章里面有一句话:“修复方案: 就是把 rrRand 换成了 globalRand, 在线上高并发场景下, 发现全局锁影响并不大.”, 有同学私聊我“他们遇到线上服务的锁竞争特别激烈”。确实我这句话说的并不严谨。但是也让我有了一个思考:到底多高的 QPS 才能让 Mutex 产生强烈的锁竞争?
haohongfan
2021/04/26
7460
这可能是最容易理解的 Go Mutex 源码剖析
Go 语言并发编程之互斥锁 sync.Mutex
Go 标准库 sync 提供互斥锁 Mutex。它的零值是未锁定的 Mutex,即未被任何 goroutine 所持有,它在被首次使用后,不可以复制。
frank.
2024/11/19
1320
Go 语言并发编程之互斥锁 sync.Mutex
Go 语言互斥锁
在并发编程中,互斥锁(Mutex,全称 Mutual Exclusion)是一个重要的同步原语,用于确保多个线程或进程在访问共享资源时不会发生竞态条件。竞态条件是指在多个线程同时访问或修改共享数据时,由于操作顺序的不确定性,导致数据不一致或者程序行为不可预测的问题。
FunTester
2025/02/19
1600
Go 语言互斥锁
GO系列(4)-goroutine基本用法
runtime 调度器是个非常有用的东西,关于 runtime 包几个方法:
爽朗地狮子
2022/10/20
3170
Go开发疑难杂症终结者通关指南|果fx
在 Go 语言中,并发编程是其核心特性之一,但也可能导致一些棘手的问题。以下是一些常见的并发问题及其解决方案:
sou、百课优
2024/12/03
1660
Go语言核心36讲(Go语言实战与应用四)--学习笔记
从本篇文章开始,我们将一起探讨 Go 语言自带标准库中一些比较核心的代码包。这会涉及这些代码包的标准用法、使用禁忌、背后原理以及周边的知识。
郑子铭
2021/11/14
3350
Go语言核心36讲(Go语言实战与应用四)--学习笔记
Go开发疑难杂症终结者通关指南|果fx原创
在 Go 语言中,并发编程是其核心特性之一,但也可能导致一些棘手的问题。以下是一些常见的并发问题及其解决方案:
瘦瘦itazs和fun
2024/12/20
1460
Golang并发编程控制
重学编程之Golang的plan中的上一篇文章我向大家介绍了,并发编程基础,goroutine的创建,channel,正由于go语言的简洁性,我们可以简易快速的创建任意个协程。同时也留下了许多隐患,如果没有更加深入的学习,其实很难直接将其运用到实际项目中,实际生活中。为什么呢?并发的场景许许多多,但一味的只知道其创建,是很难有效的解决问题。例如以下场景-资源竞争
PayneWu
2020/12/18
6010
Golang并发编程控制
并发模型和同步机制
在计算机科学中,多线程是指一个进程中的多个线程共享该进程的资源。一般来说,多线程可以提高程序的执行效率,从而加快了应用程序的响应时间。Go语言作为一种现代化的编程语言,特别适合于开发高并发的网络服务。本文将介绍Golang的并发模型和同步机制。
用户1413827
2023/11/28
2960
Go 精妙的互斥锁设计
在并发编程中,互斥锁(Mutex)是控制并发访问共享资源的重要工具。Go 语言的互斥锁设计以其简洁、高效和易用性著称。本文将详细介绍 Go 语言中的互斥锁设计,探讨其内部实现原理,并展示如何在实际项目中正确使用互斥锁。
Michel_Rolle
2024/06/30
3.1K0
Go 高性能编程技法
作者:dablelv,腾讯 IEGggG 后台开发工程师 代码的稳健、可读和高效是我们每一个 coder 的共同追求。本文将结合 Go 语言特性,为书写效率更高的代码,从常用数据结构、内存管理和并发,三个方面给出相关建议。话不多说,让我们一起学习 Go 高性能编程的技法吧。 常用数据结构 1.反射虽好,切莫贪杯 标准库 reflect 为 Go 语言提供了运行时动态获取对象的类型和值以及动态创建对象的能力。反射可以帮助抽象和简化代码,提高开发效率。 Go 语言标准库以及很多开源软件中都使用了 Go 语言的反
腾讯技术工程官方号
2022/03/18
2.1K0
go 互斥锁的案例
贵哥的编程之路
2024/03/22
1040
go 互斥锁的案例
java 死锁的问题怎么解决的
在 Java 中解决死锁问题通常需要结合代码设计、工具检测和预防策略。以下是详细的解决方案和最佳实践:
用户6556402
2025/05/29
1410
【go】一次读锁重入导致的死锁故障
在两天前第一次遇到自己的程序出现死锁, 我一直非常的小心使用锁,了解死锁导致的各种可能性, 这次的经历让我未来会更加小心,下面来回顾一下死锁发生的过程与代码演进的过程吧。
thinkeridea
2019/11/04
1.3K0
【go】一次读锁重入导致的死锁故障
Go并发编程基础(译)
原文:Fundamentals of concurrent programming 译者:youngsterxyf 本文是一篇并发编程方面的入门文章,以Go语言编写示例代码,内容涵盖: 运行期并发线程(goroutines) 基本的同步技术(管道和锁) Go语言中基本的并发模式 死锁和数据竞争 并行计算 在开始阅读本文之前,你应该知道如何编写简单的Go程序。如果你熟悉的是C/C++、Java或Python之类的语言,那么 Go语言之旅 能提供所有必要的背景知识。也许你还有兴趣读一读 为C++程序员准备的Go
李海彬
2018/03/26
1.5K0
Go并发编程基础(译)
17.Go语言-线程同步
在 Go 语言中,经常会遇到并发的问题,当然我们会优先考虑使用通道,同时 Go 语言也给出了传统的解决方式 Mutex(互斥锁) 和 RWMutex(读写锁) 来处理竞争条件。
面向加薪学习
2022/09/04
3030
golang的锁
在Go语言中,锁用于同步访问共享资源。Go语言提供了两种类型的锁:互斥锁(mutex)和读写锁(RWMutex)。
运维开发王义杰
2023/08/21
2410
golang的锁
相关推荐
Golang sync.Cond 简介与用法
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验