欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Go?Singleflight導(dǎo)致死鎖問題解決分析

 更新時(shí)間:2023年09月01日 14:55:04   作者:云原生領(lǐng)域  
這篇文章主要為大家介紹了Go?Singleflight導(dǎo)致死鎖問題解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

Dump 堆棧很重要

線上某個(gè)環(huán)境發(fā)現(xiàn) S3 上傳請(qǐng)求卡住,請(qǐng)求不返回,卡了30分鐘,長(zhǎng)時(shí)間沒有發(fā)現(xiàn)有效日志。一般來(lái)講,死鎖問題還是好排查的,因?yàn)楝F(xiàn)場(chǎng)一般都在。類似于 c 程序,遇到死鎖問題都會(huì)用 pstack 看一把。golang 死鎖排查思路也類似(golang 不適合使用 pstack,因?yàn)?golang 調(diào)度的是協(xié)程,pstack 只能看到線程棧),我們其實(shí)是需要知道 S3 程序里 goroutine 的棧狀態(tài)。golang 遇到這個(gè)問題我們有兩個(gè)辦法:

  • 方法一:條件允許的話,gcore 出一個(gè)堆棧,這個(gè)是最有效的方法,因?yàn)槭前颜麄€(gè) golang 程序的內(nèi)存鏡像 dump 出來(lái),然后用 dlv 分析;
  • 方法二:如果你提前開啟 net/pprof 庫(kù)的引用,開啟了 debug 接口,那么就可以調(diào)用 curl 接口,通過 http 接口獲取進(jìn)程的狀態(tài)信息;

需要注意到,golang 程序和 c 程序還是有點(diǎn)區(qū)別,goroutine 非常多,成百上千個(gè) goroutine 是常態(tài),甚至上萬(wàn)個(gè)也不稀奇。所以我們一般無(wú)法在終端上直接看完所有的棧,一般都是把所有的 goroutine 棧 dump 到文件,然用 vi 打開慢慢分析。

調(diào)試這個(gè) core 文件,意圖從堆棧里找到些東西,由于堆棧太多了,所以就使用 gorouties -t -u  這個(gè)命令,并且把輸出 dump 到文件;

curl xxx/debug/pprof/goroutine

關(guān)鍵思路

成千上萬(wàn)個(gè) goroutine ,直接顯示到終端是不合適的,我們 dump 到文件 test.txt,然后分析 test.txt 這個(gè)文件。

去查找發(fā)現(xiàn)了一些可疑堆棧,那么什么是可疑堆棧?重點(diǎn)關(guān)注加鎖等待的堆棧,關(guān)鍵字是 runtime_notifyListWait 、semaphore 、sync.(*Cond).Wait 、Acquire  這些阻塞場(chǎng)景才會(huì)用到的,如果業(yè)務(wù)堆棧上出現(xiàn)這個(gè)加鎖調(diào)用,就非??梢伞?/p>

劃重點(diǎn)

  • 留意阻塞關(guān)鍵字 runtime_notifyListWait 、semaphore 、sync.(*Cond).Wait 、Acquire ;
  • 業(yè)務(wù)堆棧(非 runtime 的一些內(nèi)部堆棧)

統(tǒng)計(jì)分析發(fā)現(xiàn),有 11 個(gè)這個(gè)堆棧都在這同一個(gè)地方,都是在等同一把鎖 blockingKeyCountLimit.lock,所以基本確認(rèn)了阻塞的位置,就是這個(gè)地方阻塞到了所有的請(qǐng)求,但是這把鎖我們使用 defer 釋放的,使用姿勢(shì)如下:

// do someting
lock.Acquire(key)
defer lock.Release(key)
// 以下為鎖內(nèi)操作;

 blockingKeyCountLimit 是我們封裝針對(duì) key 操作流控的組件。舉個(gè)例子,如果 limit == 1,key為 "test" 在 g1 上 Acquire 成功,g2 acquire("test") 就會(huì)等待,這個(gè)可以算是我們優(yōu)化的一個(gè)邏輯。如果 limit == 2,那么就允許兩個(gè)人加鎖到,后面的人都等待。

從代碼來(lái)看,函數(shù)退出一定會(huì)釋放的,但是偏偏現(xiàn)在鎖就卡在這個(gè)地方,所以就非常奇怪。我們先找哪個(gè) goroutine 占著這把鎖不釋放,看看能不能搞清楚怎樣導(dǎo)致這里搶不到鎖的原因。

通過審查業(yè)務(wù)代碼分析,發(fā)現(xiàn)可能的源頭函數(shù)(這個(gè)函數(shù)是向后端請(qǐng)求的函數(shù)):

api.(*Client).getBytesNolc

確認(rèn)是 getBytesNolc  這個(gè)函數(shù)執(zhí)行的操作,那么大概率就是卡在這個(gè)地方了。用這個(gè) getBytesNolc 字符串搜索堆棧,找下是哪個(gè)堆棧 ?搜索到這個(gè)堆棧 goroutine 19458

大概率就是第 1 個(gè)堆棧了,也就是其他的 11 個(gè) goroutine 都在等這 goroutine 19458  來(lái)放鎖,仔細(xì)看這個(gè)堆棧。那么為啥這個(gè)堆棧不放鎖呢?這里有個(gè)細(xì)節(jié)要注意下,這里是卡到 gihub.com/golang/groupcache/singleflight/singleflight.go:48 這一行:

singleflight 實(shí)現(xiàn)了緩存防擊穿的功能

終于找到你

這是一個(gè)開源庫(kù),singleflight 實(shí)現(xiàn)了緩存防擊穿的功能。

簡(jiǎn)單介紹下 singleflight 的功能,這是一個(gè)非常有效的工具。在緩存大量失效的場(chǎng)景,如果針對(duì)同一個(gè) key ,其實(shí)只需要有一個(gè)人穿透到后端請(qǐng)求數(shù)據(jù),其他人等待他完成,然后取緩存結(jié)果即可。這個(gè)就是 singleflight 實(shí)現(xiàn)的功能。具體實(shí)現(xiàn)就是:來(lái)了請(qǐng)求之后,把 key 插入到 map 里,后面的請(qǐng)求如果發(fā)現(xiàn)同名 key 在 map 里面,那么就等待它完成就好;

截屏顯示卡到 c.wg.Wait()  這一行,那么說明 map 里面肯定有已經(jīng)存在的 key,說明 goroutine 19458  不是第一個(gè)人?但是外面還有一個(gè) blockingKeyCountLimit 的互斥呢,按道理其他的人也進(jìn)不來(lái)(因?yàn)?limit == 1),這里這么講來(lái)肯定要是源頭才對(duì)?

思路整理

偽代碼顯示如下

func xxx () {
    // 大部分協(xié)程都卡在這里(11個(gè))
    // 這個(gè)鎖的效果主要是流控,limit 值初始化賦值,可以是 1,也可以是其他;
    // locker 為 blockingKeyCountLimit 類型
    limitLocker.Acquire( key )
    defer limitLocker.Release( key )
    // 獲取數(shù)據(jù)
    getBytesNolc( key , ...)
}
func getBytesNolc () {
    // ...
    // 下面就是 singleflight.Group 的用法,防穿透
    // 同一時(shí)間只允許一個(gè)人去后端更新
    ret, err = x.Group.Do(id, func() (interface{}, error) {
        // 去服務(wù)后臺(tái)獲取,更新數(shù)據(jù);
    })
    // ...
}

圖示顯示當(dāng)前的現(xiàn)狀

現(xiàn)狀小結(jié):

  • 大量的協(xié)程都在等 blockingKeyCountLimit 這把鎖釋放;
  • 協(xié)程 goroutine 19458 持有 blockingKeyCountLimit 這把鎖;
  • 協(xié)程 goroutine 19458  卻在等一個(gè)相同 key 名字的任務(wù)的完成( singleflight 一個(gè)防擊穿的庫(kù),同一時(shí)間相同 key 只允許放到一個(gè)后端去執(zhí)行),卻永遠(yuǎn)沒等到,協(xié)程因此呈現(xiàn)死鎖;

當(dāng)前的疑問就是第一個(gè) key 的任務(wù)為啥永遠(yuǎn)完不成,堆棧也找不到了,去哪里了?

發(fā)現(xiàn)蛛絲馬跡

我們?cè)僮屑?xì)審一下 singleflight 的代碼:

func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
    g.mu.Lock()
    if g.m == nil {
        g.m = make(map[string]*call)
    }
    // 如果找到同名 key 已經(jīng)存在;
    if c, ok := g.m[key]; ok {
        g.mu.Unlock()
        // 等待者走到這個(gè)分支:等待第一個(gè)人執(zhí)行完成,最后直接返回它的結(jié)果就行了;
        c.wg.Wait()
        return c.val, c.err
    }
    // 如果同名 key 不存在(第一個(gè)人走到這個(gè)分支)
    c := new(call)
    c.wg.Add(1)
    // map 里放置 key
    g.m[key] = c
    g.mu.Unlock()
    // 執(zhí)行任務(wù)
    c.val, c.err = fn()
    // 喚醒所有的等待者
    c.wg.Done()
    g.mu.Lock()
    // 刪除 map 里的 key
    delete(g.m, key)
    g.mu.Unlock()
    return c.val, c.err
}

發(fā)現(xiàn)有個(gè)線索,我們的 S3 服務(wù)程序一個(gè) http 請(qǐng)求對(duì)應(yīng)一個(gè)協(xié)程處理,為了提高服務(wù)端進(jìn)程的可用性,在框架里會(huì)捕捉 panic,這樣確保單個(gè)協(xié)程處理不會(huì)影響到其他的請(qǐng)求。基于這個(gè)前提,我們假設(shè):如果 fn() 執(zhí)行異常,panic 掉了,那么就不會(huì)走 delete(g.m, key) 的代碼,那么 key 就永遠(yuǎn)都?xì)埩粼?map 里面,而進(jìn)程卻又還活著?;腥淮笪颉?/p>

完整的推理流程

  • 第一個(gè)協(xié)程 g1 來(lái)了,加了 blockingKeyCountLimit  鎖,然后準(zhǔn)備穿透到后端,調(diào)用函數(shù) getBytesNolc  獲取數(shù)據(jù),并走進(jìn)了 singlelight ,添加了一個(gè) key:x, 準(zhǔn)備干活;

    干活發(fā)生了一些不可預(yù)期的異常(后面發(fā)現(xiàn)是配置的異常),nil 指針引用之類的, panic 堆棧了,panic 導(dǎo)致后面 delete key 操作沒有執(zhí)行;

    雖然 g1 現(xiàn)在 panic 了,但是由于在函數(shù) func xxx 里面 blockingKeyCountLimit 是 defer 執(zhí)行的,所以這把鎖還是,但是 singlelight 的 key 還存在,于是殘留在 map 里面;

    但是由于我們服務(wù)程序?yàn)榱烁呖捎檬?recover 了 panic 的,單個(gè)請(qǐng)求的失敗不會(huì)導(dǎo)致整個(gè)進(jìn)程掛掉,所以進(jìn)程還是好好的;

  • 第二個(gè) goroutine 19458  協(xié)程來(lái)了,blockingKeyCountLimit  加鎖,然后走到 singlelight  的時(shí)候,發(fā)現(xiàn)有 key: x 了,于是就等待;

    并且等待的是一個(gè)永遠(yuǎn)得不到的鎖,因?yàn)?g1 早就沒了;

  • 后續(xù)的 11 個(gè) 協(xié)程來(lái)了,于是被 blockingKeyCountLimit  阻塞住,并且永遠(yuǎn)不能釋放;

實(shí)錘:后續(xù)基于這個(gè)猜想,再去搜索一遍日志,發(fā)現(xiàn)確實(shí)是有一條 panic 相關(guān)的日志。這個(gè)時(shí)間點(diǎn)后面的請(qǐng)求全部被卡住。

思考總結(jié)  

一般來(lái)講 c 語(yǔ)言寫程序容易出現(xiàn)死鎖問題,因?yàn)楦鞣N異常邏輯可能會(huì)導(dǎo)致忘記放鎖,從而導(dǎo)致?lián)屢粋€(gè)永遠(yuǎn)都不可能得到的鎖。golang 為了解決這個(gè)問題,一般是用 defer 機(jī)制來(lái)實(shí)現(xiàn),使用姿勢(shì)如下:

func test () {
    mtx.Lock()
    defer mtx.Unlock()
    /* 臨界區(qū) */
}

golang 的 defer 機(jī)制是一個(gè)經(jīng)過經(jīng)驗(yàn)沉淀下來(lái)的有效功能。我們必須要合理使用。defer 實(shí)現(xiàn)原理是和所在函數(shù)綁定,保證函數(shù) return 的時(shí)候一定能調(diào)用到( panic 退出也能),所以 golang 加鎖放鎖的有效實(shí)踐是寫在相鄰的兩行。

其實(shí)思考下,singleflight 作為一個(gè)通用開源庫(kù),其實(shí)可以把 delete map key 放到 defer 里,這樣就能保證 map 里面的 key 一定是可以被清理的。

還有一點(diǎn),其實(shí) golang 是不提倡異常-捕捉這樣的方式編程,panic 一般不讓隨便用,如果真是嚴(yán)重的問題,掛掉就掛掉,這個(gè)估計(jì)還好一些。當(dāng)然這是要看場(chǎng)景的,還是有一些特殊場(chǎng)景的,畢竟 golang 都已經(jīng)提供了 panic-recover 這樣的一個(gè)手段,就說明還是有需求。這個(gè)就跟 unsafe 庫(kù)一樣,你只有明確知道自己的行為影響,才去使用這個(gè)工具,否則別用。

以上就是Go Singleflight導(dǎo)致死鎖問題解決分析的詳細(xì)內(nèi)容,更多關(guān)于Go Singleflight死鎖分析的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • go語(yǔ)言實(shí)現(xiàn)sqrt的方法

    go語(yǔ)言實(shí)現(xiàn)sqrt的方法

    這篇文章主要介紹了go語(yǔ)言實(shí)現(xiàn)sqrt的方法,實(shí)例分析了Go語(yǔ)言實(shí)現(xiàn)計(jì)算平方根的技巧,需要的朋友可以參考下
    2015-03-03
  • go?mod文件內(nèi)容版本號(hào)簡(jiǎn)單用法詳解

    go?mod文件內(nèi)容版本號(hào)簡(jiǎn)單用法詳解

    這篇文章主要為大家介紹了go?mod文件內(nèi)容版本號(hào)簡(jiǎn)單用法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-10-10
  • Golang內(nèi)存模型教科書級(jí)講解

    Golang內(nèi)存模型教科書級(jí)講解

    go官方介紹go內(nèi)存模型的時(shí)候說:探究在什么條件下,goroutine?在讀取一個(gè)變量的值的時(shí),能夠看到其它?goroutine?對(duì)這個(gè)變量進(jìn)行的寫的結(jié)果,Go內(nèi)存模型規(guī)定了一些條件,在這些條件下,在一個(gè)goroutine中讀取變量返回的值能夠確保是另一個(gè)goroutine中對(duì)該變量寫入的值
    2023-03-03
  • 淺談goland導(dǎo)入自定義包時(shí)出錯(cuò)(一招解決問題)

    淺談goland導(dǎo)入自定義包時(shí)出錯(cuò)(一招解決問題)

    這篇文章主要介紹了淺談goland導(dǎo)入自定義包時(shí)出錯(cuò)(一招解決問題),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來(lái)看看吧
    2020-12-12
  • Golang分布式應(yīng)用之Redis示例詳解

    Golang分布式應(yīng)用之Redis示例詳解

    這篇文章主要為大家介紹了Golang分布式應(yīng)用之Redis示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-07-07
  • Go無(wú)緩沖通道(同步通道)的實(shí)現(xiàn)

    Go無(wú)緩沖通道(同步通道)的實(shí)現(xiàn)

    本文主要介紹了Go無(wú)緩沖通道(同步通道)的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2025-02-02
  • Go?不支持?[]T轉(zhuǎn)換為[]interface類型詳解

    Go?不支持?[]T轉(zhuǎn)換為[]interface類型詳解

    這篇文章主要為大家介紹了Go不支持[]T轉(zhuǎn)換為[]interface類型詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-01-01
  • Go語(yǔ)言動(dòng)態(tài)并發(fā)控制sync.WaitGroup的靈活運(yùn)用示例詳解

    Go語(yǔ)言動(dòng)態(tài)并發(fā)控制sync.WaitGroup的靈活運(yùn)用示例詳解

    本文將講解 sync.WaitGroup 的使用方法、原理以及在實(shí)際項(xiàng)目中的應(yīng)用場(chǎng)景,用清晰的代碼示例和詳細(xì)的注釋,助力讀者掌握并發(fā)編程中等待組的使用技巧
    2023-11-11
  • Golang Map實(shí)現(xiàn)賦值和擴(kuò)容的示例代碼

    Golang Map實(shí)現(xiàn)賦值和擴(kuò)容的示例代碼

    這篇文章主要介紹了Golang Map實(shí)現(xiàn)賦值和擴(kuò)容的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-04-04
  • Golang中下劃線(_)的不錯(cuò)用法分享

    Golang中下劃線(_)的不錯(cuò)用法分享

    golang中的下劃線表示忽略變量的意思,也沒有產(chǎn)生新的變量,但是后面的表達(dá)式依然會(huì)被執(zhí)行,本文為大家整理了golang中下劃線的一些不錯(cuò)的用法,需要的可以參考下
    2023-05-05

最新評(píng)論