golang自带的死锁检测并非银弹
网上总是能看到有人说go自带了死锁检测,只要有死锁发生runtime就能检测到并及时报错退出,因此go不会被死锁问题困扰。
这说明了口口相传知识的有效性是日常值得怀疑的,同时也再一次证明了没有银弹这句话的含金量。
这个说法的杀伤力在于它虽然不对,但也不是全错,真真假假很容易让人失去判断力。
死锁检测失灵
死锁我就不多解释了,我们先来看个简单例子:
package main
import (
"fmt"
)
func main() {
c := make(chan int, 1)
fmt.Println(<-c)
}
这段代码会触发golang的死锁报错:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/tmp/deadlock.go:9 +0x32
exit status 2
这个例子为啥锁死了,因为没人给chan发数据,所以接收端永久阻塞在接收操作上了。
这说明了go确实有死锁检测。只不过你要是觉得它什么样的死锁都检测到那就大错特错了:
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 1)
for {
go func() {
fmt.Println(<-c)
}()
time.Sleep(10 * time.Millisecond)
}
}
根据示例1我们可以知道如果一个chan没有发送者,那么所有的接收者都会阻塞,在我们的例子里这些协程是永久阻塞的,理论上应该会被检测到然后报错。
遗憾的是这个程序会持续运行下去,直到内存耗尽为止:
死锁检测是有足够的时间执行的,因为10毫秒虽然对人类来说短的可以忽略但对golang运行时来说相当漫长,而且我们在不停创建协程,满足所有触发检测的条件,具体条件后面会细说。
从实验对照的角度来说,这时合理的猜测应该是会不会主协程被特殊处理了,因为上面的例子里子协程全部死锁,但主协程并没有。所以我们再次进行测试:
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 1)
go func() {
for {
fmt.Println("Hello from child.")
time.Sleep(100 * time.Millisecond)
}
}()
<-c
}
这段代码同样不会报错,程序会持续输出Hello直到你手动终止进程或者关机为止。这正说明了runtime不会在死锁检测上特殊对待主协程。
看上去go的死锁检测时常“失灵”,这是一件很恐怖的事情,尤其是在你信了文章开头那个说法在代码里放飞自我认为只要没报错就是没问题之后。
go的死锁检测到底检测了什么
说这是“失灵”其实有失偏颇,上面的现象解释起来其实很简单,三两段话就能说明白。
首先我们可以把go里的协程分为两大类,一类是runtime自己的协程,包括sysmon和gc;另一类是用户创建的协程,包括用户自己创建的,用户使用的第三方库/标准库创建的所有协程。我们暂且管后者叫“用户协程”。这只是很粗糙的分类,实际的代码中有不少出入,不过作为抽象概率帮助理解是没问题的。死锁检测针对的就是“用户协程”。
知道了检测范围,我们还需要知道检测内容——换句话说,什么情况下能判断一组协程死锁了?理想中当然是检测到一组协程循环等待某些条件或者阻塞在一些永远不会有数据的chan上。现实是go只检测这些:
- 是否有协程处于运行状态,包括并未实际运行在等待调度的“可运行”用户协程;
- 没有上述条件的协程就检测是否还有未触发的定时器;
- 都不满足才会触发死锁报错并终止程序。
检测的时机其实也是有些反直觉的,go只在创建/退出操作系统级别的线程、这些线程变为空闲状态时、sysmon检测到程序处于空闲时才会执行死锁检测。也就是说,触发检测其实和操作系统线程相关性更强而不是和goroutine。
所以,只要还有一个协程能继续运行,哪怕其他99999个协程都锁地死死得,go的死锁检测依然不会报错(更正确的说法是只要还有一个能继续运行的系统级线程,那就不算死锁,这样才能解释为什么有还未触发的定时器以及在等待系统调用也不算死锁)。这样解释了为什么示例2和3都能运行,因为2中主协程能正常运行,3中子协程能正常运行,因此其他的协程锁死了也不会报错。
检测还有两个例外:
- cgo管不了,因此go程序调用的c/c++代码的线程里锁死了go这边也没有办法
- 把go代码编译成c库之后死锁检测会主动关闭,因为如果c/c++代码没调用库里的函数的话,那就只有runtime协程存在,这时候检测会发现根本没有用户协程,这种检测没有意义。所以在这种情况下哪怕go代码真的全部死锁了也不会检测到。
有人估计觉得这是bug或者设计失误要急着去提issue了,但这不是bug!这只是看待问题的方式不同。
go采用的做法,比较正式的描述是“在期望时间内程序的运行是否能取得进展”,这里的进展当然是指的是否在运行或者有定时器/io要处理。以此标准只要还有用户协程能动,那说明“整个程序”并没有死锁——go里判断要不要触发死锁报错是以整个程序作为基准的,而我们通常的判断基准是所有用户协程都能在“期望时间内获得进展”才是没有问题。后者的要求更严格。
而且满足后者的检测实现起来很复杂,预计也会花费非常多的计算资源,从维护和运行性能的角度来说想做也不是很现实。所以go选择了前者,前者虽然不能处理所有的问题,但仍然能在早期阶段防止出现一部分死锁问题。
然而把死锁检测当成万金油保险丝的人就要倒霉了:
- 现实的项目中出现一次性锁死整个程序的情况其实是比较少的,更多的时间是像例子中那样一部分协程锁死;
- 锁死的协程除了不能继续运行之外,还会造成协程泄漏,更要命的是协程持有的对象都不会释放,所以还伴随着内存泄漏;
- 在一些程序里系统的一部分锁死了可能在短时间内影响不到其他部分,在web应用中很常见,这会让问题发生难以察觉,往往当你意识到出问题时整个程序已经到万劫不复的状态了。
所以死锁检测只能偶尔帮你一次,并不能当成救命稻草用。
死锁检测的源代码在"src/runtime/proc.go"的checkdead
函数里,感兴趣的可以自行把玩。
怎么检测死锁
既然报错不能料理所有情况,我们还能借助哪些工具定位是否有死锁发生呢?
其实没啥好办法,下面每一种方案都需要经验以及结合实际代码才能判断出结果。
第一种是观察协程数量或者内存占用是否异常。比如你的程序正常需要1000个协程,那么2千个协程也许不是出问题了,但出现2万个协程那肯定是不对劲的。内存占用同理。
这些数据很好获取,不管是go自带的pprof还是trace,或者是第三方的性能监控,都能很轻松的探测到异常。难的是如何定位具体的问题。不过这节说的是如何发现死锁,所以出现上述异常后把可能存在死锁放进排查方向里也就够了。因为协程泄漏虽然不一定都是死锁造成的,但死锁最直接的表现就是协程泄漏。
方案1的缺点也很明显,如果死锁的协程数量固定,或者产生死锁协程的速度很慢,那么监控数据上很难发现问题。我们也不可能简单地用服务没响应了来判断是不是出了死锁,无响应的原因实在是太多了。
此外uber开发的用于检测协程泄漏的库goleak也可以帮上一些忙,不过缺点是一样的。
第二种是用go trace或者调试器dlv看运行时的协程栈。如果栈里出现很多lock类函数或者chan收发函数,那么存在死锁协程的概率是比较大的。最重要的是要看不同协程的调用栈里是否存在循环依赖或者交叉加锁的情况。
方案2的缺点也很明显,第一个是需要在程序运行时获取调用栈信息,这会影响程序的性能,协程越多影响越明显;第二是分析调用栈现在没啥好的自动化工具往往得程序员自己上阵,如果协程数目巨大的话分析会变得极度困难。
而且调用栈里lock函数多不代表一定有死锁,也可能只是锁竞争激烈而已。
最后一种方案是借助go trace工具,trace里有个叫block profile的,这个可以统计哪些函数被阻塞住了。如果看到里面有大量的lock、select相关函数、chan相关操作函数,那么死锁的可能性很大。
但和方案2一样,方案3依然不能100%确定存在问题,还是要结合实际代码做分析。而且trace只能分析某个时间段内的程序运行情况,如果你的程序死锁问题是偶发的,那么很可能抓几百次trace数据都不一定能抓到案发现场。
最后结论就是没有银弹。所以与其期待有个万能检测器不如写代码的时候就提前预防问题发生。
总结
我之所以写这篇文章是因为很多golang的布道师居然会以golang有死锁检测所以能避免死锁错误为卖点宣传go语言,信以为真的go用户也不少。稍加实验就能证伪的说法如今依然大行其道,令人感叹。
软件行业是很难出现银弹的,因此多动手检验才能少踩坑早下班。
golang自带的死锁检测并非银弹的更多相关文章
- MySQL学习(四)死锁及死锁检测
文章图片来自参考资料 MySQL 的锁 根据加锁的范围,MySQL里面的锁大致可以分成全局锁.表级锁和行锁三类.我们需要明白锁的服务是为了数据统一,或者说是事务,数据隔离. 全局锁 全 ...
- linux死锁检测的一种思路【转】
转自:http://www.cnblogs.com/mumuxinfei/p/4365697.html 前言: 上一篇博文讲述了pstack的使用和原理. 和jstack一样, pstack能获取进 ...
- 为什么MySQL死锁检测会严重降低TPS
在大量的客户端,更新数据表的同一行时,会造成数据库的吞吐量大幅降低. 很多数据库的前辈和同行分别通过实验和源码的方法,定位到了罪魁祸首----MySQL死锁检测 实验方式:http://blog.cs ...
- linux死锁检测的一种思路
前言: 上一篇博文讲述了pstack的使用和原理. 和jstack一样, pstack能获取进程的线程堆栈快照, 方便检验和性能评估. 但jstack功能更加的强大, 它能对潜在的死锁予以提示, 而p ...
- 14.3.5.2 Deadlock Detection and Rollback 死锁检测和回滚:
14.3.5.2 Deadlock Detection and Rollback 死锁检测和回滚: InnoDB 自动检查四艘,回滚一个事务或者事务来打破死锁. InnoDB 试图选择小的事务来回滚, ...
- UNIX环境高级编程——记录上锁(fcntl函数)以及死锁检测
一.记录锁 record locking 功能:当一个进程正在读或修改文件的某个部分时,它可以阻止其它进程修改同一文件区. 字节范围锁 byte-range locking 二.历史 flock函数, ...
- Linux死锁检测-Lockdep
关键词:LockDep.spinlock.mutex. lockdep是内核提供协助发现死锁问题的功能. 本文首先介绍何为lockdep,然后如何在内核使能lockdep,并简单分析内核lockdep ...
- 谈谈MySQL死锁之二 死锁检测和处理源码分析
这一篇主要是通过一个实验来进行描述,过程是比较枯燥的. 实验准备 create table test_lock(id int auto_increment primary key ,stock int ...
- 当前数据库普遍使用wait-for graph等待图来进行死锁检测
当前数据库普遍使用wait-for graph等待图来进行死锁检测 较超时机制,这是一种更主动的死锁检测方式,innodb引擎也采用wait-for graph SQL Server也使用wait-f ...
- MySQL死锁检测和回滚
最近碰到“TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH, WE WILL ROLL BACK FOLLOWING TRANSACT ...
随机推荐
- 《使用Gin框架构建分布式应用》阅读笔记:p127-p142
<用Gin框架构建分布式应用>学习第9天,p127-p142总结,总计16页. 一.技术总结 1.Authentication方式汇总 (1)API keys API keys 认证方式示 ...
- Go语言编写单元测试用例
Go单元测试示例 example/ |--division.go |--division_test.go 为什么被测试文件和测试文件通常放到同一个文件夹下以及同一个声明包里 通常情况下,我们把被测试的 ...
- Redis示例配置文件
# 注意单位问题:当需要设置内存大小的时候,可以使用类似1k.5GB.4M这样的常见格式: # # 1k => 1000 bytes # 1m => # 1kb => 1024 by ...
- 在 Github Action 管道内集成 Code Coverage Report
Github Actions 我们的开源项目 Host 在 Github,并且使用它强大的 Actions 功能在做 CICD.单看 Github Actions 可能不知道是啥.其实它就是我们常说的 ...
- 内网 BitTorrent 下载环境搭建——基于 Transmission
背景 前段时间为公司的产品增加了磁力链.种子下载的能力,测试时发现网上搜到的热门种子,有时好用,有时不好用,不好用主要表现在:没速度.速度慢.速度不稳定.下载一部分后没速度等,严重拖累了测试工作.为此 ...
- 2023NOIP A层联测26 T4 abstract
2023NOIP A层联测26 T4 abstract 乱证明求性质的光速幂优化题. 思路 对于每一个节点,到该节点的子树内的叶子节点的路径中(包括路径上的点),出现的值只有 \(k\times(\l ...
- 【一步步开发AI运动小程序】十八、如何识别用户上传图片中的人体、运动、动作、姿态?
[云智AI运动识别小程序插件],可以为您的小程序,赋于人体检测识别.运动检测识别.姿态识别检测AI能力.本地原生识别引擎,内置10余个运动,无需依赖任何后台或第三方服务,有着识别速度快.体验佳.扩展性 ...
- JS之Date时间处理
初始化当前时间: // 1. 使用构造函数方式 var newDate = new Date() // 2. 使用函数方式 var date = Date() // 返回的是一个Date对象 cons ...
- 链路追踪之Jaeger
官方地址:https://www.jaegertracing.io/ [安装] 官方提供了两个安装方式, 1. 基于二进制(https://www.jaegertracing.io/download/ ...
- MySQL命令行客户端工具之mycli
官网: mycli 目前市面上存在各种五花八门的图形界面客户端,如 phpmyadmin,navicat 以及官方的 MySQL Workbench 等等.而在日常工作或者使用中,通过命令连接 MyS ...