当前位置: 首页 > 科技观察

几个Go系统可能遇到的锁问题

时间:2023-03-16 16:56:49 科技观察

几个Go系统可能遇到的锁问题避免踩同一个坑。有一些开源库依赖底层的sync.Pool。为了优化性能,使用了官方的sync.Pool。比如我们使用的库https://github.com/valyala/fasttemplate,每当你执行如下When代码: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字段:typeTemplatestruct{templatestringstartTagstringendTagstringtexts[][]bytetags[]stringbyteBufferPoolbytebufferpool.Pool====就是这个字段}byteBufferPool底层是封装的sync.Pool:typePoolstruct{calls[steps]uint64calibratinguint64defaultSizeuint64maxSizeuint64poolsync.Pool}这个设计会带来一个问题,如果用户每次都请求一个新的Template对象。并对它进行评估,比如我们最初的用法,在每次用户请求之后,它都会在模板中填充参数:funcfromTplToStr(tplstring,paramsmap[string]interface{})string{tplVar:=fasttemplate.New(tpl,`{{`,`}}`)res:=tplVar.ExecuteString(params)returnres}评估模板时:func(t*Template)ExecuteFuncString(fTagFunc)string{bb:=t.byteBufferPool.Get()if_,err:=t.ExecuteFunc(bb,f);err!=nil{panic(fmt.Sprintf("unexpectederror:%s",err))}s:=string(bb.Bytes())bb.Reset()t.byteBufferPool.Put(bb)returns}会得到Template对象的byteBufferPool,使用完后将ByteBufferReset放回对象池。但是问题是我们的Template对象本身并没有被复用,所以byteBufferPool本身的作用还没有发挥出来。Onthecontrary,becauseeachrequestneedstogenerateanewsync.Pool,inahigh-concurrencyscenario,itwillbestuckonthesentencebb:=t.byteBufferPool.Get()duringexecution,andtheproblemcanbefoundrelativelyquicklythroughpressuretesting,达到一定QPS压力时,会有大量的Goroutine堆积,比如下面有18910个G堆积在抢锁代码上:goroutineprofile:total1891018903@0x102f20b0x102f2b30x103fa4c0x103f77d0x10714df0x1071d8f0x1071d260x1071a5f0x12feeb80x13005f00x13007c30x130107b0x105c931#0x103f77csync.runtime_SemacquireMutex+0x3c/usr/local/go/src/runtime/sema.go:71#0x10714desync.(*Mutex).Lock+0xfe/usr/local/go/src/sync/mutex.go:134#0x1071d8esync.(*Pool).pinSlow+0x3e/usr/local/go/src/sync/pool.go:198#0x1071d25sync.(*Pool).pin+0x55/usr/local/go/src/sync/pool.go:191#0x1071a5esync.(*Pool).Get+0x2e/usr/local/go/src/sync/pool.go:128#0x12feeb7github.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#0x13005efgithub.com/valyala/fasttemplate.(*Template).ExecuteFuncString+0x3f/Users/xargin/go/src/github.com/valyala/fasttemplate/template.go:278#0x13007c2github.com/valyala/fasttemplate.(*Template).ExecuteString+0x52/Users/xargin/go/src/github.com/valyala/fasttemplate/template.go:299#0x130107amain.loop.func1+0x3a/Users/xargin/test/go/http/httptest.go:22有大量的Goroutine在获取锁时会被阻塞。为什么?继续看sync.Pool的Get过程:func(p*Pool)Get()interface{}{ifrace.Enabled{race.Disable()}l:=p.pin()x:=l.privatel。private=nilruntime_procUnpin()然后pin:func(p*Pool)pin()*poolLocal{pid:=runtime_procPin()s:=atomic.LoadUintptr(&p.localSize)//load-acquirel:=p.local//load-consumeifuintptr(pid)重点在return0,err}deferfd.writeUnlock()for{err:=syscall.Sendto(fd.Sysfd,p,0,sa)iferr==syscall.EAGAIN&&fd.pd.pollable(){iferr=fd.pd.waitWrite(fd.isFile);err==nil{continue}}iferr!=nil{return0,err}returnlen(p),nil}}本质上是对高成本网络操作的大写锁,在高并发场景下也会导致大量的锁冲突,然后同样造成大量的Goroutine堆积和接口延迟。知道了问题,解决方法也很简单。查看相关日志。因为公司目前大部分日志都是直接写入文件系统,本质上是同时操作同一个文件,最终会走到:func(f*File)Write(b[]byte)(nint,errerror){n,e:=f.write(b)returnn,err}func(f*File)write(b[]byte)(nint,errerror){n,err=f.pfd.Write(b)运行时。KeepAlive(f)returnn,err}然后:func(fd*FD)Write(p[]byte)(int,error){iferr:=fd.writeLock();err!=nil{=========>writeLockreturn0,erragain}deferfd.writeUnlock()iferr:=fd.pd.prepareWrite(fd.isFile);err!=nil{return0,err}varnnintfor{-----忽略无关内容n,err:=syscall.Write(fd.Sysfd,p[nn:max])-----省略无用内容}}同UDP网络FD有writeLock,当系统日志很多的时候,这个writeLock会导致与指标报告的问题相同。综上所述,上述问题其实都是并发场景下的锁竞争问题。全局写锁是高并发场景下的性能杀手。一旦大量的Goroutine阻塞在写锁上,系统的延迟就会飙升,直到接口超时。在开发系统时,需要注意涉及sync.Pool、单个FD信息上报、日志写入等场景。尽早进行压力测试以确保安全。