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