Go gomaxprocs 调高引起调度性能损耗
先前在社區里分享了關于 golang 行情推送[1]的分享,有人針對 ppt 的內容問了我兩個問題,一個是在 docker 下 golang 的 gomaxprocs 初始化混亂問題,另一個是 golang runtime.gomaxprocs 配置多少為合適?
golang runtime
Golang 的 runtime 調度是依賴 pmg 的角色抽象,p 為邏輯處理器,m 為執行體(線程),g 為協程。p 的 runq 隊列中放著可執行的 goroutine 結構。golang 默認 p 的數量為 cpu core 數目,比如物理核心為 8 cpu core,那么 go processor 的數量就為 8。另外,同一時間一個 p 只能綁定一個 m 線程,pm 綁定后自然就找 g 和運行 g。
那么增加 processor 的數量,是否可以用來加大 runtime 對于協程的調度吞吐?
大多 golang 的項目偏重網絡 io,network io 在 netpoll 設計下都是非阻塞的,所涉及到的 syscall 不會阻塞。如果是 cpu 密集的業務,增加多個 processor 也沒用,畢竟 cpu 計算資源就這些,居然還想著來回的切換????? 所以,多數場景下單純增加 processor 是沒什么用的。
當然,話不絕對,如果你的邏輯含有不少的 cgo 及阻塞 syscall 的操作,那么增加 processor 還是有效果的,最少在我實際項目中有效果。原因是這類操作有可能因為過慢引起阻塞,在阻塞期間的 p 被 mg 綁定一起,其他 m 無法獲取 p 的所有權。雖然在 findrunnable steal 機制里,其他 p 的 m 可以偷該 p 的任務,但在解綁 p 之前終究還是少了一條并行通道。另外,runtime 的 sysmon 周期性的檢查長時間阻塞的 pmg, 并搶占并解綁 p。
golang 在 docker 下的問題
在微服務體系下服務的部署通常是放在 docker 里的。一個宿主機里跑著大量的不同服務的容器。為了避免資源沖突,通常會合理地對每個容器做 cpu 資源控制。比如給一個 golang 服務的容器限定了 2 cpu core 的資源,容器內的服務不管怎么折騰,也確實只能用到大約 2 個 cpu core 的資源。
但 golang 初始化 processor 數量是依賴 /proc/cpuinfo 信息的,容器內的 cpuinfo 是跟宿主機一致的,這樣導致容器只能用到 2 個 cpu core,但 golang 初始化了跟物理 cpu core 相同數量的 processor。
//?xiaorui.cc限制2核左右 root@xiaorui.cc:~#?docker?run?-tid?--cpu-period?100000?--cpu-quota?200000?ubuntu容器內 root@a4f33fdd0240:/#?cat?/proc/cpuinfo|?grep?"processor"|?wc?-l 48runtime processor 多了會出現什么問題?
一個是 runtime findrunnable 時產生的損耗,另一個是線程引起的上下文切換。
runtime 的 findrunnable 方法是解決 m 找可用的協程的函數,當從綁定 p 本地 runq 上找不到可執行的 goroutine 后,嘗試從全局鏈表中拿,再拿不到從 netpoll 和事件池里拿,最后會從別的 p 里偷任務。全局 runq 是有鎖操作,其他偷任務使用了 atomic 原子操作來規避 futex 競爭下陷入切換等待問題,但 lock free 在競爭下也會有忙輪詢的狀態,比如不斷的嘗試。
//?xiaorui.cc//?全局?runq if?sched.runqsize?!=?0?{lock(&sched.lock)gp?:=?globrunqget(_p_,?0)unlock(&sched.lock)if?gp?!=?nil?{return?gp,?false} } ...//?嘗試4次從別的p偷任務for?i?:=?0;?i?<?4;?i++?{for?enum?:=?stealOrder.start(fastrand());?!enum.done();?enum.next()?{if?sched.gcwaiting?!=?0?{goto?top}stealRunNextG?:=?i?>?2?//?first?look?for?ready?queues?with?more?than?1?gif?gp?:=?runqsteal(_p_,?allp[enum.position()],?stealRunNextG);?gp?!=?nil?{return?gp,?false}} } ...通過 godebug 可以看到全局隊列及各個 p 的 runq 里等待調度的任務量。有不少 p 是空的,那么勢必會引起 steal 偷任務。另外,runqueue 的大小遠超其他 p 的總和,說明大部分任務在全局里,全局又是把大鎖。
隨著調多 runtime processor 數量,相關的 m 線程自然也就跟著多了起來。linux 內核為了保證可執行的線程在調度上雨露均沾,按照內核調度算法來切換就緒狀態的線程,切換又引起上下文切換。上下文切換也是性能的一大殺手。findrunnable 的某些鎖競爭也會觸發上下文切換。
下面是我這邊一個行情推送服務壓測下的 vmstat 監控數據。首先把容器的的 cpu core 限制為 8,再先后測試 processor 為 8 和 48 的情況。圖的上面是 processor 為 8 的情況,下面為 processor 為 48 的情況??磮D可以直觀地發現當 processor 調大后,上下文切換(cs)明顯多起來,另外等待調度的線程也多了。
另外從 qps 的指標上也可以反映多 processor 帶來的性能損耗。通過下圖可以看到當 runtime.GOMAXPROCS 為固定的 cpu core 數時,性能最理想。后面隨著 processor 數量的增長,qps 指標有所下降。
通過 golang tool trace 可以分析出協程調度等待時間越來越長了。
解決 docker 下的 golang gomaxprocs 校對問題
有兩個方法可以準確校對 golang 在 docker 的 cpu 獲取問題。
要么在 k8s pod 里加入 cpu 限制的環境變量,容器內的 golang 服務在啟動時獲取關于 cpu 的信息。
要么解析 cpu.cfs_period_us 和 cpu.cfs_quota_us 配置來計算 cpu 資源。社區里有不少這類的庫可以使用,uber的automaxprocs[2]可以兼容 docker 的各種 cpu 配置。
總結
建議 gomaxprocs 配置為 cpu core 數量就可以了,Go 默認就是這個配置,無需再介入。如果涉及到阻塞 syscall,可以適當的調整 gomaxprocs 大小,但一定要用指標數據說話!
參考資料
[1]
行情推送: http://xiaorui.cc/?p=6250
[2]automaxprocs: https://github.com/uber-go/automaxprocs
總結
以上是生活随笔為你收集整理的Go gomaxprocs 调高引起调度性能损耗的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一次系统调用开销到底有多大?
- 下一篇: 做好项目,从正确定义问题开始!