溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Go系統(tǒng)遇到的鎖問題有哪些

發(fā)布時間:2021-11-06 16:37:42 來源:億速云 閱讀:131 作者:iii 欄目:web開發(fā)

本篇內(nèi)容介紹了“Go系統(tǒng)遇到的鎖問題有哪些”的有關(guān)知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領(lǐng)大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠?qū)W有所成!

底層依賴 sync.Pool 的場景

有一些開源庫,為了優(yōu)化性能,使用了官方提供的 sync.Pool,比如我們使用的  https://github.com/valyala/fasttemplate 這個庫,每當你執(zhí)行下面這樣的代碼的時候:

template := "http://{{host}}/?q={{query}}&foo={{bar}}{{bar}}"     t := fasttemplate.New(template, "{{", "}}")     s := t.ExecuteString(map[string]interface{}{         "host":  "google.com",         "query": url.QueryEscape("hello=world"),         "bar":   "foobar",     })     fmt.Printf("%s", s)

內(nèi)部都會生成一個 fasttemplate.Template 對象,并帶有一個 byteBufferPool 字段:

type Template struct {     template string     startTag string     endTag   string      texts          [][]byte     tags           []string     byteBufferPool bytebufferpool.Pool   ==== 就是這個字段 }

byteBufferPool 底層就是經(jīng)過封裝的 sync.Pool:

type Pool struct {     calls       [steps]uint64     calibrating uint64      defaultSize uint64     maxSize     uint64      pool sync.Pool }

這種設(shè)計會帶來一個問題,如果使用方每次請求都 New 一個 Template  對象。并進行求值,比如我們最初的用法,在每次拿到了用戶的請求之后,都會用參數(shù)填入到模板:

func fromTplToStr(tpl string, params map[string]interface{}) string {   tplVar := fasttemplate.New(tpl, `{{`, `}}`)   res := tplVar.ExecuteString(params)   return res }

在模板求值的時候:

func (t *Template) ExecuteFuncString(f TagFunc) string {     bb := t.byteBufferPool.Get()     if _, err := t.ExecuteFunc(bb, f); err != nil {         panic(fmt.Sprintf("unexpected error: %s", err))     }     s := string(bb.Bytes())     bb.Reset()     t.byteBufferPool.Put(bb)     return s }

會對該 Template 對象的 byteBufferPool 進行 Get,在使用完之后,把 ByteBuffer Reset  再放回到對象池中。但問題在于,我們的 Template 對象本身并沒有進行復(fù)用,所以這里的 byteBufferPool 本身的作用其實并沒有發(fā)揮出來。

相反的,因為每一個請求都需要新生成一個 sync.Pool,在高并發(fā)場景下,執(zhí)行時會卡在 bb := t.byteBufferPool.Get()  這一句上,通過壓測可以比較快地發(fā)現(xiàn)問題,達到一定 QPS 壓力時,會有大量的 Goroutine 堆積,比如下面有 18910 個 G  堆積在搶鎖代碼上:

goroutine profile: total 18910 18903 @ 0x102f20b 0x102f2b3 0x103fa4c 0x103f77d 0x10714df 0x1071d8f 0x1071d26 0x1071a5f 0x12feeb8 0x13005f0 0x13007c3 0x130107b 0x105c931 #   0x103f77c   sync.runtime_SemacquireMutex+0x3c                               /usr/local/go/src/runtime/sema.go:71 #   0x10714de   sync.(*Mutex).Lock+0xfe                                     /usr/local/go/src/sync/mutex.go:134 #   0x1071d8e   sync.(*Pool).pinSlow+0x3e                                   /usr/local/go/src/sync/pool.go:198 #   0x1071d25   sync.(*Pool).pin+0x55                                       /usr/local/go/src/sync/pool.go:191 #   0x1071a5e   sync.(*Pool).Get+0x2e                                       /usr/local/go/src/sync/pool.go:128 #   0x12feeb7   github.com/valyala/fasttemplate/vendor/github.com/valyala/bytebufferpool.(*Pool).Get+0x37   /Users/xargin/go/src/github.com/valyala/fasttemplate/vendor/github.com/valyala/bytebufferpool/pool.go:49 #   0x13005ef   github.com/valyala/fasttemplate.(*Template).ExecuteFuncString+0x3f              /Users/xargin/go/src/github.com/valyala/fasttemplate/template.go:278 #   0x13007c2   github.com/valyala/fasttemplate.(*Template).ExecuteString+0x52                  /Users/xargin/go/src/github.com/valyala/fasttemplate/template.go:299 #   0x130107a   main.loop.func1+0x3a                                        /Users/xargin/test/go/http/httptest.go:22

有大量的 Goroutine 會阻塞在獲取鎖上,為什么呢?繼續(xù)看看 sync.Pool 的 Get 流程:

func (p *Pool) Get() interface{} {     if race.Enabled {         race.Disable()     }     l := p.pin()     x := l.private     l.private = nil     runtime_procUnpin()

然后是 pin:

func (p *Pool) pin() *poolLocal {     pid := runtime_procPin()          s := atomic.LoadUintptr(&p.localSize) // load-acquire     l := p.local                          // load-consume     if uintptr(pid) < s {         return indexLocal(l, pid)     }     return p.pinSlow() }

因為每一個對象的 sync.Pool 都是空的,所以 pin 的流程一定會走到 p.pinSlow:

func (p *Pool) pinSlow() *poolLocal {     runtime_procUnpin()     allPoolsMu.Lock()     defer allPoolsMu.Unlock()     pid := runtime_procPin()

而 pinSlow 中會用 allPoolsMu 來加鎖,這個 allPoolsMu 主要是為了保護 allPools 變量:

var (     allPoolsMu Mutex     allPools   []*Pool )

在加了鎖的情況下,會把用戶新生成的 sync.Pool 對象 append 到 allPools 中:

if p.local == nil {         allPools = append(allPools, p)     }

標準庫的 sync.Pool 之所以要維護這么一個 allPools 意圖也比較容易推測,主要是為了 GC 的時候?qū)?pool  進行清理,這也就是為什么說使用 sync.Pool 做對象池時,其中的對象活不過一個 GC 周期的原因。sync.Pool 本身也是為了解決大量生成臨時對象對  GC 造成的壓力問題。

說完了流程,問題也就比較明顯了,每一個用戶請求最終都需要去搶一把全局鎖,高并發(fā)場景下全局鎖是大忌。但是這個全局鎖是因為開源庫間接帶來的全局鎖問題,通過看自己的代碼并不是那么容易發(fā)現(xiàn)。

知道了問題,改進方案其實也還好實現(xiàn),***是可以修改開源庫,將 template 的 sync.Pool 作為全局對象來引用,這樣大部分 pool.Get  不會走到 pinSlow 流程。第二是對 fasttemplate.Template 對象進行復(fù)用,道理也是一樣的,就不會有那么多的 sync.Pool  對象生成了。但前面也提到了,這個是個間接問題,如果開發(fā)工作繁忙,不太可能所有的依賴庫把代碼全看完之后再使用,這種情況下怎么避免線上的故障呢?

壓測盡量早做唄。

metrics 上報和 log 鎖

這兩個本質(zhì)都是一樣的問題,就放在一起了。

公司之前 metrics 上報 client 都是基于 udp 的,大多數(shù)做的簡單粗暴,就是一個  client,用戶傳什么就寫什么,最終一定會走到:

func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error) {     ---------- 刨去無用細節(jié)     n, err := c.writeTo(b, addr)     ---------- 刨去無用細節(jié)     return n, err }

或者是:

func (c *UDPConn) WriteTo(b []byte, addr Addr) (int, error) {      ---------- 刨去無用細節(jié)     n, err := c.writeTo(b, a)     ---------- 刨去無用細節(jié)     return n, err }

調(diào)用的是:

func (c *UDPConn) writeTo(b []byte, addr *UDPAddr) (int, error) {     ---------- 刨去無用細節(jié)     return c.fd.writeTo(b, sa) }

然后:

func (fd *netFD) writeTo(p []byte, sa syscall.Sockaddr) (n int, err error) {     n, err = fd.pfd.WriteTo(p, sa)     runtime.KeepAlive(fd)     return n, wrapSyscallError("sendto", err) }

然后是:

func (fd *FD) WriteTo(p []byte, sa syscall.Sockaddr) (int, error) {     if err := fd.writeLock(); err != nil {  =========> 重點在這里         return 0, err     }     defer fd.writeUnlock()      for {         err := syscall.Sendto(fd.Sysfd, p, 0, sa)         if err == syscall.EAGAIN && fd.pd.pollable() {             if err = fd.pd.waitWrite(fd.isFile); err == nil {                 continue             }         }         if err != nil {             return 0, err         }         return len(p), nil     } }

本質(zhì)上,就是在高成本的網(wǎng)絡(luò)操作上套了一把大的寫鎖,同樣在高并發(fā)場景下會導(dǎo)致大量的鎖沖突,進而導(dǎo)致大量的 Goroutine 堆積和接口延遲。

同樣的,知道了問題,解決辦法也很簡單。再看看日志相關(guān)的。因為公司目前大部分日志都是直接向文件系統(tǒng)寫,本質(zhì)上同一個時刻操作的是同一個文件,最終都會走到:

func (f *File) Write(b []byte) (n int, err error) {     n, e := f.write(b)     return n, err }  func (f *File) write(b []byte) (n int, err error) {     n, err = f.pfd.Write(b)     runtime.KeepAlive(f)     return n, err }

然后:

func (fd *FD) Write(p []byte) (int, error) {     if err := fd.writeLock(); err != nil { =========> 又是 writeLock         return 0, err     }     defer fd.writeUnlock()     if err := fd.pd.prepareWrite(fd.isFile); err != nil {         return 0, err     }     var nn int     for {         ----- 略去不相關(guān)內(nèi)容         n, err := syscall.Write(fd.Sysfd, p[nn:max])         ----- 略去無用內(nèi)容     } }

和 UDP 網(wǎng)絡(luò) FD 一樣有 writeLock,在系統(tǒng)打日志打得很多的情況下,這個 writeLock 會導(dǎo)致和 metrics  上報一樣的問題。

“Go系統(tǒng)遇到的鎖問題有哪些”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

go
AI