现象
一个组件实现了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重入导致死锁