當前位置:
首頁 > 知識 > 幾個Go系統可能遇到的鎖問題

幾個Go系統可能遇到的鎖問題

之前統一特徵系統在 QA 同學的幫助下進行了一些壓測,發現了一些問題,這些問題是較為通用的問題,發出來給其他同學參考一下,避免踩同樣的坑。

幾個Go系統可能遇到的鎖問題

底層依賴 sync.Pool 的場景

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

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)

內部都會生成一個 fasttemplate.Template 對象,並帶有一個 byteBufferPool 欄位:

type Template struct {
template string
startTag string
endTag string

texts [][]byte
tags []string
byteBufferPool bytebufferpool.Pool ==== 就是這個欄位
}

byteBufferPool 底層就是經過封裝的 sync.Pool:

type Pool struct {
calls [steps]uint64
calibrating uint64

defaultSize uint64
maxSize uint64

pool sync.Pool
}

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

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 對象本身並沒有進行復用,所以這裡的 byteBufferPool 本身的作用其實並沒有發揮出來。

相反的,因為每一個請求都需要新生成一個 sync.Pool,在高並發場景下,執行時會卡在 bb := t.byteBufferPool.Get() 這一句上,通過壓測可以比較快地發現問題,達到一定 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 會阻塞在獲取鎖上,為什麼呢?繼續看看 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 的時候對 pool 進行清理,這也就是為什麼說使用 sync.Pool 做對象池時,其中的對象活不過一個 GC 周期的原因。sync.Pool 本身也是為了解決大量生成臨時對象對 GC 造成的壓力問題。

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

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

壓測盡量早做唄。

metrics 上報和 log 鎖

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

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

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

或者是:

func (c *UDPConn) WriteTo(b []byte, addr Addr) (int, error) {

---------- 刨去無用細節
n, err := c.writeTo(b, a)
---------- 刨去無用細節
return n, err
}

調用的是:

func (c *UDPConn) writeTo(b []byte, addr *UDPAddr) (int, error) {
---------- 刨去無用細節
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
}
}

本質上,就是在高成本的網路操作上套了一把大的寫鎖,同樣在高並發場景下會導致大量的鎖衝突,進而導致大量的 Goroutine 堆積和介面延遲。

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

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 {
----- 略去不相關內容
n, err := syscall.Write(fd.Sysfd, p[nn:max])
----- 略去無用內容
}
}

和 UDP 網路 FD 一樣有 writeLock,在系統打日誌打得很多的情況下,這個 writeLock 會導致和 metrics 上報一樣的問題。

總結

上面說的幾個問題實際上本質都是並發場景下的 lock contention 問題,全局寫鎖是高並發場景下的性能殺手,一旦大量的 Goroutine 阻塞在寫鎖上,會導致系統的延遲飈升,直至介面超時。在開發系統時,涉及到 sync.Pool、單個 FD 的信息上報、以及寫日誌的場景時,應該多加註意。早做壓測保平安。

作者:佚名

原文:http://developer.51cto.com/art/201901/590195.htm

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 程序員小新人學習 的精彩文章:

Spring security + oauth2.0 + redis + mybatis plus 搭建微服務
CSS實現點擊事件及實踐

TAG:程序員小新人學習 |