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 的讀寫不可能會阻塞注,導致你釋放不了這個鎖的,所以這些獲取鎖的地方你都可以直接排除,肯定不是在這些地方出的問題。

經過這一輪排查,你會發現,可能有問題的地方已經所剩無幾了,剩下的都是獲取鎖之後,釋放鎖之前之間的過程可能會卡住的地方,歐克,我們繼續分析。

什麼樣的邏輯可能卡住?

  1. 可能是一個 rpc 調用,或者一個 http 調用,你沒有設置超時時間,而和遠端的交互又出了問題,一直拿不到回覆

  2. 可能是在讀寫數據庫,這時也有可能會由於數據量太大或者 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