Goroutine 數(shù)量控制在多少合適,會影響 GC 和調(diào)度?
本文轉(zhuǎn)載自微信公眾號「腦子進煎魚了」,作者陳煎魚。轉(zhuǎn)載本文請聯(lián)系腦子進煎魚了公眾號。
大家好,我是煎魚。
前幾天在讀者交流群里看到一位小伙伴,發(fā)出了一個致命提問,那就是:“單機的 goroutine 數(shù)量控制在多少比較合適?”。
也許你和群內(nèi)小伙伴第一反應(yīng)一樣,會答復 “控制多少,我覺得沒有定論”。
緊接著延伸出了更進一步的疑惑:“goroutine 太多了會影響 gc 和調(diào)度吧,主要是怎么預算這個數(shù)是合理的呢?”
這是本文要進行探討的主體,因此本文的結(jié)構(gòu)會是先探索基礎(chǔ)知識,再一步步揭開,深入理解這個問題。
Goroutine 是什么
Go 語言作為一個新生編程語言,其令人喜愛的特性之一就是 goroutine。Goroutine 是一個由 Go 運行時管理的輕量級線程,一般稱其為 “協(xié)程”。
- go f(x, y, z)
操作系統(tǒng)本身是無法明確感知到 Goroutine 的存在的,Goroutine 的操作和切換歸屬于 “用戶態(tài)” 中。
Goroutine 由特定的調(diào)度模式來控制,以 “多路復用” 的形式運行在操作系統(tǒng)為 Go 程序分配的幾個系統(tǒng)線程上。
同時創(chuàng)建 Goroutine 的開銷很小,初始只需要 2-4k 的??臻g。Goroutine 本身會根據(jù)實際使用情況進行自伸縮,非常輕量。
- func say(s string) {
- for i := 0; i < 9999999; i++ {
- time.Sleep(100 * time.Millisecond)
- fmt.Println(s)
- }
- }
- func main() {
- go say("煎魚")
- say("你好")
- }
人稱可以開幾百幾千萬個的協(xié)程小霸王,是 Go 語言的得意之作之一。
調(diào)度是什么
既然有了用戶態(tài)的代表 Goroutine,操作系統(tǒng)又看不到他。必然需要有某個東西去管理他,才能更好的運作起來。
這指的就是 Go 語言中的調(diào)度,最常見、面試最愛問的 GMP 模型。因此接下來將會給大家介紹一下 Go 調(diào)度的基礎(chǔ)知識和流程。
下述內(nèi)容摘自煎魚和 p 神寫的《Go 語言編程之旅》中的章節(jié)內(nèi)容。
調(diào)度基礎(chǔ)知識
Go scheduler 的主要功能是針對在處理器上運行的 OS 線程分發(fā)可運行的 Goroutine,而我們一提到調(diào)度器,就離不開三個經(jīng)常被提到的縮寫,分別是:
- G:Goroutine,實際上我們每次調(diào)用 go func 就是生成了一個 G。
- P:Processor,處理器,一般 P 的數(shù)量就是處理器的核數(shù),可以通過 GOMAXPROCS 進行修改。
- M:Machine,系統(tǒng)線程。
這三者交互實際來源于 Go 的 M: N 調(diào)度模型。也就是 M 必須與 P 進行綁定,然后不斷地在 M 上循環(huán)尋找可運行的 G 來執(zhí)行相應(yīng)的任務(wù)。
調(diào)度流程
我們以 GMP 模型的工作流程圖進行簡單分析,官方圖如下:
- 當我們執(zhí)行 go func() 時,實際上就是創(chuàng)建一個全新的 Goroutine,我們稱它為 G。
- 新創(chuàng)建的 G 會被放入 P 的本地隊列(Local Queue)或全局隊列(Global Queue)中,準備下一步的動作。需要注意的一點,這里的 P 指的是創(chuàng)建 G 的 P。
- 喚醒或創(chuàng)建 M 以便執(zhí)行 G。
- 不斷地進行事件循環(huán)
- 尋找在可用狀態(tài)下的 G 進行執(zhí)行任務(wù)
- 清除后,重新進入事件循環(huán)
在描述中有提到全局和本地這兩類隊列,其實在功能上來講都是用于存放正在等待運行的 G,但是不同點在于,本地隊列有數(shù)量限制,不允許超過 256 個。
并且在新建 G 時,會優(yōu)先選擇 P 的本地隊列,如果本地隊列滿了,則將 P 的本地隊列的一半的 G 移動到全局隊列。
這可以理解為調(diào)度資源的共享和再平衡。
竊取行為
我們可以看到圖上有 steal 行為,這是用來做什么的呢,我們都知道當你創(chuàng)建新的 G 或者 G 變成可運行狀態(tài)時,它會被推送加入到當前 P 的本地隊列中。
其實當 P 執(zhí)行 G 完畢后,它也會 “干活”,它會將其從本地隊列中彈出 G,同時會檢查當前本地隊列是否為空,如果為空會隨機的從其他 P 的本地隊列中嘗試竊取一半可運行的 G 到自己的名下。
官方圖如下:
在這個例子中,P2 在本地隊列中找不到可以運行的 G,它會執(zhí)行 work-stealing 調(diào)度算法,隨機選擇其它的處理器 P1,并從 P1 的本地隊列中竊取了三個 G 到它自己的本地隊列中去。
至此,P1、P2 都擁有了可運行的 G,P1 多余的 G 也不會被浪費,調(diào)度資源將會更加平均的在多個處理器中流轉(zhuǎn)。
有沒有什么限制
在前面的內(nèi)容中,我們針對 Go 的調(diào)度模型和 Goroutine 做了一個基本介紹和分享。
接下來我們回到主題,思考 “goroutine 太多了,會不會有什么影響”。
在了解 GMP 的基礎(chǔ)知識后,我們要知道在協(xié)程的運行過程中,真正干活的 GPM 又分別被什么約束?
煎魚帶大家分別從 GMP 來逐步分析。
M 的限制
第一,要知道在協(xié)程的執(zhí)行中,真正干活的是 GPM 中的哪一個?
那勢必是 M(系統(tǒng)線程) 了,因為 G 是用戶態(tài)上的東西,最終執(zhí)行都是得映射,對應(yīng)到 M 這一個系統(tǒng)線程上去運行。
那么 M 有沒有限制呢?
答案是:有的。在 Go 語言中,M 的默認數(shù)量限制是 10000,如果超出則會報錯:
- GO: runtime: program exceeds 10000-thread limit
通常只有在 Goroutine 出現(xiàn)阻塞操作的情況下,才會遇到這種情況。這可能也預示著你的程序有問題。
若確切是需要那么多,還可以通過 debug.SetMaxThreads 方法進行設(shè)置。
G 的限制
第二,那 G 呢,Goroutine 的創(chuàng)建數(shù)量是否有限制?
答案是:沒有。但理論上會受內(nèi)存的影響,假設(shè)一個 Goroutine 創(chuàng)建需要 4k(via @GoWKH):
- 4k * 80,000 = 320,000k ≈ 0.3G內(nèi)存
- 4k * 1,000,000 = 4,000,000k ≈ 4G內(nèi)存
以此就可以相對計算出來一臺單機在通俗情況下,所能夠創(chuàng)建 Goroutine 的大概數(shù)量級別。
注:Goroutine 創(chuàng)建所需申請的 2-4k 是需要連續(xù)的內(nèi)存塊。
P 的限制
第三,那 P 呢,P 的數(shù)量是否有限制,受什么影響?
答案是:有限制。P 的數(shù)量受環(huán)境變量 GOMAXPROCS 的直接影響。
環(huán)境變量 GOMAXPROCS 又是什么?在 Go 語言中,通過設(shè)置 GOMAXPROCS,用戶可以調(diào)整調(diào)度中 P(Processor)的數(shù)量。
另一個重點在于,與 P 相關(guān)聯(lián)的的 M(系統(tǒng)線程),是需要綁定 P 才能進行具體的任務(wù)執(zhí)行的,因此 P 的多少會影響到 Go 程序的運行表現(xiàn)。
P 的數(shù)量基本是受本機的核數(shù)影響,沒必要太過度糾結(jié)他。
那 P 的數(shù)量是否會影響 Goroutine 的數(shù)量創(chuàng)建呢?
答案是:不影響。且 Goroutine 多了少了,P 也該干嘛干嘛,不會帶來災難性問題。
何為之合理
在介紹完 GMP 各自的限制后,我們回到一個重點,就是 “Goroutine 數(shù)量怎么預算,才叫合理?”。
“合理” 這個詞,是需要看具體場景來定義的,可結(jié)合上述對 GPM 的學習和了解。得出:
- M:有限制,默認數(shù)量限制是 10000,可調(diào)整。
- G:沒限制,但受內(nèi)存影響。
- P:受本機的核數(shù)影響,可大可小,不影響 G 的數(shù)量創(chuàng)建。
Goroutine 數(shù)量在 MG 的可控限額以下,多個把個、幾十個,少幾個其實沒有什么影響,就可以稱其為 “合理”。
真實情況
在真實的應(yīng)用場景中,沒法如此簡單的定義。如果你 Goroutine:
- 在頻繁請求 HTTP,MySQL,打開文件等,那假設(shè)短時間內(nèi)有幾十萬個協(xié)程在跑,那肯定就不大合理了(可能會導致 too many files open)。
- 常見的 Goroutine 泄露所導致的 CPU、Memory 上漲等,還是得看你的 Goroutine 里具體在跑什么東西。
還是得看 Goroutine 里面跑的是什么東西。
總結(jié)
在這篇文章中,分別介紹了 Goroutine、GMP、調(diào)度模型的基本知識,針對如下問題進行了展開:
- 單機的 goroutine 數(shù)量控制在多少比較合適?
- goroutine 太多了會影響 gc 和調(diào)度吧,主要是怎么預算這個數(shù)是合理的呢?
單機的 goroutine 數(shù)量只要控制在限額以下的,都可以認為是 “合理”。
真實場景得看具體里面跑的是什么,跑的如果是 “資源怪獸”,只運行幾個 Goroutine 都可以跑死。
因此想定義 “預算”,就得看跑的什么了。