?golang?行情推送[1]的分享,有人針對(duì) ppt 的內(nèi)容問(wèn)了我兩個(gè)問(wèn)題,一個(gè)是在 docker 下 golang 的 gomaxprocs 初始化混亂問(wèn)題,另一個(gè)是 golang runtime.gomaxprocs 配置多少為合適?
golang runtime
Golang 的 runtime 調(diào)度是依賴 pmg 的角色抽象,p 為邏輯處理器愁溜,m 為執(zhí)行體(線程),g 為協(xié)程媒役。p 的 runq 隊(duì)列中放著可執(zhí)行的 goroutine 結(jié)構(gòu)祝谚。golang 默認(rèn) p 的數(shù)量為 cpu core 數(shù)目,比如物理核心為 8 cpu core酣衷,那么 go processor 的數(shù)量就為 8交惯。另外,同一時(shí)間一個(gè) p 只能綁定一個(gè) m 線程穿仪,pm 綁定后自然就找 g 和運(yùn)行 g席爽。
那么增加 processor 的數(shù)量,是否可以用來(lái)加大 runtime 對(duì)于協(xié)程的調(diào)度吞吐啊片?
大多 golang 的項(xiàng)目偏重網(wǎng)絡(luò) io只锻,network io 在 netpoll 設(shè)計(jì)下都是非阻塞的,所涉及到的 syscall 不會(huì)阻塞紫谷。如果是 cpu 密集的業(yè)務(wù)齐饮,增加多個(gè) processor 也沒(méi)用,畢竟 cpu 計(jì)算資源就這些笤昨,居然還想著來(lái)回的切換祖驱??? 所以,多數(shù)場(chǎng)景下單純?cè)黾?processor 是沒(méi)什么用的瞒窒。
當(dāng)然捺僻,話不絕對(duì),如果你的邏輯含有不少的 cgo 及阻塞 syscall 的操作,那么增加 processor 還是有效果的匕坯,最少在我實(shí)際項(xiàng)目中有效果束昵。原因是這類操作有可能因?yàn)檫^(guò)慢引起阻塞,在阻塞期間的 p 被 mg 綁定一起葛峻,其他 m 無(wú)法獲取 p 的所有權(quán)锹雏。雖然在 findrunnable steal 機(jī)制里,其他 p 的 m 可以偷該 p 的任務(wù)术奖,但在解綁 p 之前終究還是少了一條并行通道逼侦。另外,runtime 的 sysmon 周期性的檢查長(zhǎng)時(shí)間阻塞的 pmg腰耙, 并搶占并解綁 p。
golang 在 docker 下的問(wèn)題
在微服務(wù)體系下服務(wù)的部署通常是放在 docker 里的铲球。一個(gè)宿主機(jī)里跑著大量的不同服務(wù)的容器挺庞。為了避免資源沖突,通常會(huì)合理地對(duì)每個(gè)容器做 cpu 資源控制稼病。比如給一個(gè) golang 服務(wù)的容器限定了 2 cpu core 的資源选侨,容器內(nèi)的服務(wù)不管怎么折騰,也確實(shí)只能用到大約 2 個(gè) cpu core 的資源然走。
但 golang 初始化 processor 數(shù)量是依賴 /proc/cpuinfo 信息的援制,容器內(nèi)的 cpuinfo 是跟宿主機(jī)一致的,這樣導(dǎo)致容器只能用到 2 個(gè) cpu core芍瑞,但 golang 初始化了跟物理 cpu core 相同數(shù)量的 processor晨仑。
//?xiaorui.cc
限制2核左右
root@xiaorui.cc:~#?docker?run?-tid?--cpu-period?100000?--cpu-quota?200000?ubuntu
容器內(nèi)
root@a4f33fdd0240:/#?cat?/proc/cpuinfo|?grep?"processor"|?wc?-l
48
runtime processor 多了會(huì)出現(xiàn)什么問(wèn)題?
一個(gè)是 runtime findrunnable 時(shí)產(chǎn)生的損耗拆檬,另一個(gè)是線程引起的上下文切換洪己。
runtime 的 findrunnable 方法是解決 m 找可用的協(xié)程的函數(shù),當(dāng)從綁定 p 本地 runq 上找不到可執(zhí)行的 goroutine 后竟贯,嘗試從全局鏈表中拿答捕,再拿不到從 netpoll 和事件池里拿,最后會(huì)從別的 p 里偷任務(wù)屑那。全局 runq 是有鎖操作拱镐,其他偷任務(wù)使用了 atomic 原子操作來(lái)規(guī)避 futex 競(jìng)爭(zhēng)下陷入切換等待問(wèn)題,但 lock free 在競(jìng)爭(zhēng)下也會(huì)有忙輪詢的狀態(tài)持际,比如不斷的嘗試沃琅。
//?xiaorui.cc
//?全局?runq
ifsched.runqsize?!=0{
lock(&sched.lock)
gp?:=?globrunqget(_p_,0)
unlock(&sched.lock)
ifgp?!=nil{
returngp,false
}
}
...
//?嘗試4次從別的p偷任務(wù)
fori?:=0;?i?<4;?i++?{
forenum?:=?stealOrder.start(fastrand());?!enum.done();?enum.next()?{
ifsched.gcwaiting?!=0{
gototop
}
stealRunNextG?:=?i?>2//?first?look?for?ready?queues?with?more?than?1?g
ifgp?:=?runqsteal(_p_,?allp[enum.position()],?stealRunNextG);?gp?!=nil{
returngp,false
}
}
}
...
通過(guò) godebug 可以看到全局隊(duì)列及各個(gè) p 的 runq 里等待調(diào)度的任務(wù)量。有不少 p 是空的选酗,那么勢(shì)必會(huì)引起 steal 偷任務(wù)阵难。另外,runqueue 的大小遠(yuǎn)超其他 p 的總和芒填,說(shuō)明大部分任務(wù)在全局里呜叫,全局又是把大鎖空繁。
隨著調(diào)多 runtime processor 數(shù)量,相關(guān)的 m 線程自然也就跟著多了起來(lái)朱庆。linux 內(nèi)核為了保證可執(zhí)行的線程在調(diào)度上雨露均沾盛泡,按照內(nèi)核調(diào)度算法來(lái)切換就緒狀態(tài)的線程,切換又引起上下文切換娱颊。上下文切換也是性能的一大殺手傲诵。findrunnable 的某些鎖競(jìng)爭(zhēng)也會(huì)觸發(fā)上下文切換。
下面是我這邊一個(gè)行情推送服務(wù)壓測(cè)下的 vmstat 監(jiān)控?cái)?shù)據(jù)箱硕。首先把容器的的 cpu core 限制為 8拴竹,再先后測(cè)試 processor 為 8 和 48 的情況。圖的上面是 processor 為 8 的情況剧罩,下面為 processor 為 48 的情況栓拜。看圖可以直觀地發(fā)現(xiàn)當(dāng) processor 調(diào)大后惠昔,上下文切換(cs)明顯多起來(lái)幕与,另外等待調(diào)度的線程也多了。
另外從 qps 的指標(biāo)上也可以反映多 processor 帶來(lái)的性能損耗镇防。通過(guò)下圖可以看到當(dāng) runtime.GOMAXPROCS 為固定的 cpu core 數(shù)時(shí)啦鸣,性能最理想。后面隨著 processor 數(shù)量的增長(zhǎng)来氧,qps 指標(biāo)有所下降诫给。
通過(guò) golang tool trace 可以分析出協(xié)程調(diào)度等待時(shí)間越來(lái)越長(zhǎng)了。
解決 docker 下的 golang gomaxprocs 校對(duì)問(wèn)題
有兩個(gè)方法可以準(zhǔn)確校對(duì) golang 在 docker 的 cpu 獲取問(wèn)題啦扬。
要么在 k8s pod 里加入 cpu 限制的環(huán)境變量蝙搔,容器內(nèi)的 golang 服務(wù)在啟動(dòng)時(shí)獲取關(guān)于 cpu 的信息。
要么解析 cpu.cfs_period_us 和 cpu.cfs_quota_us 配置來(lái)計(jì)算 cpu 資源考传。社區(qū)里有不少這類的庫(kù)可以使用吃型,uber的automaxprocs[2]可以兼容 docker 的各種 cpu 配置。
總結(jié)
建議 gomaxprocs 配置為 cpu core 數(shù)量就可以了僚楞,Go 默認(rèn)就是這個(gè)配置勤晚,無(wú)需再介入。如果涉及到阻塞 syscall泉褐,可以適當(dāng)?shù)恼{(diào)整 gomaxprocs 大小赐写,但一定要用指標(biāo)數(shù)據(jù)說(shuō)話!