golang RWMutex RLock重入导致死锁

现象

一个组件实现了raft分布式协议,在分布式部署环境中来进行选主,在某客户现场突然发生文件句柄泄露,在打印某些错误日志后,几个小时内没有日志打印,然后某个协程突然报无可用的文件句柄。

分析

经过代码和日志分析,组件正常每分钟会打印所有部署节点的日志信息,没有打印日志说明定时器处理逻辑for...select里面某个函数逻辑卡住了,然后发生文件句柄泄露,经过梳理是在响应心跳的逻辑没有回,导致一直创建协程。心跳响应逻辑和定时器处理逻辑中有用到同一个锁,初步判断为这个锁发生死锁。

在本地环境复现了后,通过debug/pprof分析,确实有四处在等待该锁,两处等待写锁,两处等待读锁,但是代码看起来都很正常;pprof分析也没有提示死锁。然后通过搜索引擎搜索关键词“RWMutex 死锁”,找到一篇文件说RWMutex RLock重入可能导致死锁,如果网络异常,有分布式节点疑似下线时,代码中确实有一处会有该锁的RLock同一协程两次重入调用。

RLock重入死锁复现

 1 func TestDeadLock(t *testing.T) {
 2     var l sync.RWMutex
 3     var wg sync.WaitGroup
 4     wg.Add(2)
 5 
 6     c := make(chan int)
 7     go func() {
 8         defer wg.Done()
 9 
10         l.RLock()
11         defer l.RUnlock()
12         t.Log("acquire RLock first")
13 
14         c <- 1
15         runtime.Gosched()
16 
17         t.Log("wait readLock")
18         l.RLock()
19         defer l.RUnlock()
20         t.Log("acquire RLock second")
21     }()
22 
23     go func() {
24         defer wg.Done()
25 
26         <-c
27 
28         t.Log("wait writeLock")
29         l.Lock()
30         defer l.Unlock()
31         t.Log("acquire Lock")
32     }()
33 
34     wg.Wait()
35     t.Log("test finish")
36 }

通过以上测试代码,很容易复现该死锁现象,而在java中可重入读写锁读锁重入不会导致死锁,所以刚开始看到RLock重入时也没有想到该问题。

源码分析

 

参考文档

golang RWMutex RLock重入导致死锁

 

上一篇:Redis分布式锁—Redisson+RLock可重入锁实现篇


下一篇:分布式锁