Go 項目如何排查死鎖
發現了死鎖怎麼辦?自然是要解之。。最近剛好解決了兩個死鎖,分享一下心得,希望能幫助到大家~
我把死鎖分爲兩種,一種爲簡單死鎖,另一種爲併發場景下的死鎖,接下來我們分別來說一說。
簡單死鎖
所謂簡單死鎖,就是寫代碼的時候粗心大意寫出來的,比如說我們寫了一個這樣的函數:
函數 A 會在獲取鎖之後調用函數 B,而 B 也要獲取這個鎖,這時候顯然就會發生死鎖,而且是百分百出現的。
現在你看着我的這個圖可能覺得,我怎麼可能犯如此低級的錯誤,但實際上,如果函數 A 不是你寫的,而且你在 B 裏面又調用了其他函數,和 A 跨了好幾層,這時候你就很有可能忽略 A 這個外層函數已經加了鎖。
那麼這種鎖該如何避免呢?很簡單,寫單元測試,寫單元測試是一個很好的習慣,實際上,如果單元測試寫的好的話,很多邏輯問題不用通過實際測試就可以得到很好的驗證。
回到圖中的這種情況,如果函數 A 本身是有單元測試的,而你修改了 B 的邏輯,加上了一個外層已經加了的鎖,那麼這個時候,A 的單元測試在運行的時候就會卡住,你也就可以及時發現問題。
當然還有一種可能是,你只修改了 B 的邏輯,那麼雖然 A 是有單元測試,但是你並不會去測 A,這個也沒關係,如果你的項目配置了 ci,那麼提交到 github 或者 gitlab 上的時候就會幫你執行一遍所有的單元測試,如果有死鎖,ci 會卡住,直到超時,然後顯示 ci 不通過,這個時候你就也可以知道,你寫了個死鎖~
併發場景下的死鎖
我相信更多人打開這篇文章,想知道的就是高併發情況下,出現了死鎖,要如何排查,這確實是一個令人頭疼的問題,我第一次遇到這種死鎖,是在一次修改了程序,壓測的過程中,這個問題非常難復現,必須連續壓測好幾個小時才能復現,當時我真的是感受到了絕望,雖然我通過 pprof 定位到了是哪個鎖卡住了,但問題是,這個鎖有超多地方會調用,怎麼定位是在哪裏死鎖了,又是爲什麼死鎖的呢?
其實,死鎖問題是有一套清晰的解決思路的,我們首先要做的是,莫慌,冷靜下來,一步一步分析。
下面假設我們通過 pprof 看到的卡住的鎖叫做 Lock
首先,爲什麼會卡住?
因爲獲取不到 Lock。
爲什麼獲取不到 Lock?
因爲有其他 goroutine 獲取到了 Lock,但是一直沒有釋放。
但這裏,你已經可以採取第一步行動了,那就是查看所有獲取這個鎖的地方,看看在釋放之前,是否存在卡住的可能。
這個排查,其實往往是很快的,不要想着說,這個鎖有幾十個地方會獲取,就望而卻步了,事實上,大部分情況,你的鎖只是用來鎖住關鍵的資源,比如一個可能併發讀寫的 map,而你上鎖之後也只是讀寫了一下 map,那麼很顯然,map 的讀寫不可能會阻塞注,導致你釋放不了這個鎖的,所以這些獲取鎖的地方你都可以直接排除,肯定不是在這些地方出的問題。
經過這一輪排查,你會發現,可能有問題的地方已經所剩無幾了,剩下的都是獲取鎖之後,釋放鎖之前之間的過程可能會卡住的地方,歐克,我們繼續分析。
什麼樣的邏輯可能卡住?
-
可能是一個 rpc 調用,或者一個 http 調用,你沒有設置超時時間,而和遠端的交互又出了問題,一直拿不到回覆
-
可能是在讀寫數據庫,這時也有可能會由於數據量太大或者 sql 寫的不夠好或者和數據庫之間的連接出現了問題,導致卡很久
如果是以上這些情況,你就要考慮一下了,這些操作有必要鎖住嗎?
如果沒有,開一個新的 goroutine 讓他慢慢去做就好了;
如果又必要鎖住,那你一定要設置一個超時時間,不要讓程序一直在這裏傻傻的等待。。
另外一種情況,是我要着重介紹的,那就是交叉鎖
何謂交叉鎖,假設有兩個鎖,Lock1 和 Lock2,然後有兩個 goroutine,邏輯分別是這樣的:
我想你應該已經看出問題了,那就是如果在某一時刻,goroutine1 獲取到了 Lock1,而 goroutine 獲取到了 Lock2,那麼接下來,兩個 goroutine 就都無路可走了,後續如果又其他想獲取 Lock1 和獲取 Lock2 的 goroutine,也都會卡死。不要覺得這種情況概率低就不可能出現,高併發的情況下,一切皆有可能!
我們回到排查的過程中,如果你發現 Lock 在某處可能卡住,而卡住的原因是因爲這裏在釋放 Lock 之前,還申請了其他鎖,假設叫 otherLock 吧,那麼這個時候,你就可以開始第二輪排查了,排查對象,就是所有獲取 otherLock 的地方,而排查目標就是,在釋放 otherLock 之前,需要獲取 Lock 的地方。
如果發現了這樣的地方,那麼你的死鎖,十有八九就是這裏導致的,接下來,你又該好好思考一下了,在鎖住 Lock 的情況下,真的有必要再鎖住 otherLock 嗎?
如果沒有必要,那你就把申請 otherLock 放在 Lock 釋放後面去,或者開一個 goroutine,在 goroutine 裏再獲取 otherLock;
如果真的有必要,那麼對不起,你的想法有問題,再好好想想吧。。
總結
最後總結一下,三點,
第一,養成寫單元測試的好習慣,簡單的死鎖往往可以通過單元測試解決~
第二,一定要避免交叉鎖,這是一定會有問題的~
第三,不要慌張,遇到死鎖,按我的思路,冷靜排查,也許需要很久,但你一定可以查到的,加油吧!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/omFQEpT-aNeGPQeuPbGeTQ