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

Go?Singleflight導致死鎖問題解決分析

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

Dump 堆棧很重要

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

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

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

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

curl xxx/debug/pprof/goroutine

關鍵思路

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

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

劃重點

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

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

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

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

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

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

api.(*Client).getBytesNolc

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

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

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

終于找到你

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

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

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

思路整理

偽代碼顯示如下

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

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

現(xiàn)狀小結:

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

當前的疑問就是第一個 key 的任務為啥永遠完不成,堆棧也找不到了,去哪里了?

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

我們再仔細審一下 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()
        // 等待者走到這個分支:等待第一個人執(zhí)行完成,最后直接返回它的結果就行了;
        c.wg.Wait()
        return c.val, c.err
    }
    // 如果同名 key 不存在(第一個人走到這個分支)
    c := new(call)
    c.wg.Add(1)
    // map 里放置 key
    g.m[key] = c
    g.mu.Unlock()
    // 執(zhí)行任務
    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)有個線索,我們的 S3 服務程序一個 http 請求對應一個協(xié)程處理,為了提高服務端進程的可用性,在框架里會捕捉 panic,這樣確保單個協(xié)程處理不會影響到其他的請求?;谶@個前提,我們假設:如果 fn() 執(zhí)行異常,panic 掉了,那么就不會走 delete(g.m, key) 的代碼,那么 key 就永遠都殘留在 map 里面,而進程卻又還活著?;腥淮笪?。

完整的推理流程

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

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

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

    但是由于我們服務程序為了高可用是 recover 了 panic 的,單個請求的失敗不會導致整個進程掛掉,所以進程還是好好的;

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

    并且等待的是一個永遠得不到的鎖,因為 g1 早就沒了;

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

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

思考總結  

一般來講 c 語言寫程序容易出現(xiàn)死鎖問題,因為各種異常邏輯可能會導致忘記放鎖,從而導致?lián)屢粋€永遠都不可能得到的鎖。golang 為了解決這個問題,一般是用 defer 機制來實現(xiàn),使用姿勢如下:

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

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

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

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

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

相關文章

  • go語言實現(xiàn)sqrt的方法

    go語言實現(xiàn)sqrt的方法

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

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

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

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

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

    淺談goland導入自定義包時出錯(一招解決問題)

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

    Golang分布式應用之Redis示例詳解

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

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

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

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

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

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

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

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

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

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

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

最新評論