<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          緩存擊穿導(dǎo)致 Go 組件死鎖的問題剖析

          共 5082字,需瀏覽 11分鐘

           ·

          2020-09-10 20:46


          ??大綱??


          • 思路排查

            • Dump 堆棧很重要

            • 關(guān)鍵思路

            • 終于找到你

            • 思路整理

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

            • 完整的推理流程

          • 思考總結(jié)


          分享一個線上遇到的死鎖問題,什么, golang 也會有死鎖?


          ??思路排查??


          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 遇到這個問題我們有兩個辦法:

          1. 方法一:條件允許的話,gcore 出一個堆棧,這個是最有效的方法,因為是把整個 golang 程序的內(nèi)存鏡像 dump 出來,然后用 dlv 分析;
          2. 方法二:如果你提前開啟 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

          關(guān)鍵思路


          成千上萬個 goroutine ,直接顯示到終端是不合適的,我們 dump 到文件 test.txt,然后分析 test.txt 這個文件。去查找發(fā)現(xiàn)了一些可疑堆棧,那么什么是可疑堆棧?重點關(guān)注加鎖等待的堆棧,關(guān)鍵字是 runtime_notifyListWaitsemaphoresync.(*Cond).WaitAcquire ?這些阻塞場景才會用到的,如果業(yè)務(wù)堆棧上出現(xiàn)這個加鎖調(diào)用,就非常可疑。

          劃重點

          1. 留意阻塞關(guān)鍵字 runtime_notifyListWaitsemaphoresync.(*Cond).WaitAcquire
          2. 業(yè)務(wù)堆棧(非 runtime 的一些內(nèi)部堆棧)

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

          // do sometinglock.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 占著這把鎖不釋放,看看能不能搞清楚怎樣導(dǎo)致這里搶不到鎖的原因。

          通過審查業(yè)務(wù)代碼分析,發(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 的功能,這是一個非常有效的工具。在緩存大量失效的場景,如果針對同一個 key ,其實只需要有一個人穿透到后端請求數(shù)據(jù),其他人等待他完成,然后取緩存結(jié)果即可。這個就是 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) { // 去服務(wù)后臺獲取,更新數(shù)據(jù); }) // ...}

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



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

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

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


          發(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í)行完成,最后直接返回它的結(jié)果就行了;        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í)行任務(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)有個線索,我們的 S3 服務(wù)程序一個 http 請求對應(yīng)一個協(xié)程處理,為了提高服務(wù)端進程的可用性,在框架里會捕捉 panic,這樣確保單個協(xié)程處理不會影響到其他的請求。基于這個前提,我們假設(shè):如果 fn() 執(zhí)行異常,panic 掉了,那么就不會走 delete(g.m, key) 的代碼,那么 key 就永遠都殘留在 map 里面,而進程卻又還活著。恍然大悟。


          完整的推理流程


          1. 第一個協(xié)程 g1 來了,加了 blockingKeyCountLimit ?鎖,然后準(zhǔn)備穿透到后端,調(diào)用函數(shù) getBytesNolc ?獲取數(shù)據(jù),并走進了 singlelight ,添加了一個 key:x, 準(zhǔn)備干活;
            1. 干活發(fā)生了一些不可預(yù)期的異常(后面發(fā)現(xiàn)是配置的異常),nil 指針引用之類的, panic 堆棧了,panic 導(dǎo)致后面 delete key 操作沒有執(zhí)行;
            2. 雖然 g1 現(xiàn)在 panic 了,但是由于在函數(shù) func xxx 里面 blockingKeyCountLimit 是 defer 執(zhí)行的,所以這把鎖還是,但是 singlelight 的 key 還存在,于是殘留在 map 里面;
            3. 但是由于我們服務(wù)程序為了高可用是 recover 了 panic 的,單個請求的失敗不會導(dǎo)致整個進程掛掉,所以進程還是好好的;
          2. 第二個 goroutine 19458 ?協(xié)程來了,blockingKeyCountLimit ?加鎖,然后走到 singlelight ?的時候,發(fā)現(xiàn)有 key: x 了,于是就等待;
            1. 并且等待的是一個永遠得不到的鎖,因為 g1 早就沒了;
          3. 后續(xù)的 11 個 協(xié)程來了,于是被 blockingKeyCountLimit ?阻塞住,并且永遠不能釋放;

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


          ??思考總結(jié)??


          一般來講 c 語言寫程序容易出現(xiàn)死鎖問題,因為各種異常邏輯可能會導(dǎo)致忘記放鎖,從而導(dǎo)致?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 一般不讓隨便用,如果真是嚴(yán)重的問題,掛掉就掛掉,這個估計還好一些。當(dāng)然這是要看場景的,還是有一些特殊場景的,畢竟 golang 都已經(jīng)提供了 panic-recover 這樣的一個手段,就說明還是有需求。這個就跟 unsafe 庫一樣,你只有明確知道自己的行為影響,才去使用這個工具,否則別用。





          推薦閱讀



          學(xué)習(xí)交流 Go 語言,掃碼回復(fù)「進群」即可


          站長 polarisxu

          自己的原創(chuàng)文章

          不限于 Go 技術(shù)

          職場和創(chuàng)業(yè)經(jīng)驗


          Go語言中文網(wǎng)

          每天為你

          分享 Go 知識

          Go愛好者值得關(guān)注





          瀏覽 38
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩特级黄色电影 | 国产视频在线一区 | 四虎做爱 | av在线无码高清 Av之家亚洲中文 | 黄色片三级片在线看网站 |