現象
一個組件實現了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重入導致死鎖