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