Go 適合 IO 密集型?並不準確!
Go 程序適合什麼樣的場景?
你想過這個問題嗎?Go 程序到底是哪種場景最適合。可能你已經聽說過答案了:IO 密集型的場景。
什麼是 CPU 密集型、IO 密集型?
計算機程序場景會分成 cpu 密集型 和 IO 密集型。CPU 密集型說的是一直在運算 CPU 指令,這種常見於算法運行嘍。IO 密集型說的是經常下發 IO ,比如網卡,磁盤,或者其他外設。
Go 適合 IO 密集型,並不準確?
你肯定記住了答案:Go 適合 IO 密集型的場景。但其實這裏並不準確。更準確的是 Go 適合的是網絡 IO 密集型的場景,而非磁盤 IO 密集型。甚至可以說,Go 對於磁盤 IO 密集型並不友好。
Go 對於 網絡 IO 和磁盤 IO 爲什麼會有差別?
根本原因:在於網絡 socket 句柄和文件句柄的不同。網絡 IO 能夠用異步化的事件驅動的方式來管理,磁盤 IO 則不行。這個在我之前 Linux 句柄系列也詳細提過這個。
socket 句柄可讀可寫事件都有意義,socket buffer 裏有數據,說明對端網絡發數據過來了,即滿足可讀事件。有 buffer 可以寫,那麼說明還能發送數據,滿足可寫事件。
所以 socket 的句柄實現了 .poll
方法,可以用 epoll 池來管理。文件句柄可讀可寫事件則沒有意義,因爲文件句柄理論上是永遠都是可讀可寫的,不會阻塞調用。
所以文件的 .poll
一般是不實現的,所以自然也用不了 epoll 池來管理。而能否用 epoll 池來管理 fd 則決定了能否在 Go 裏用 epoll 池 IO 複用的形式來實現 IO 併發。
socket 句柄可以設置爲 noblocking (非阻塞的方式),這樣當網絡 IO 還未就緒的時候就可以在 Go 代碼裏把調度權切走,去執行其他協程,這樣就實現了網絡 IO 的併發。
但是磁盤 IO 則不行,文件 IO 的 read/write 都是同步的 IO ,沒有實現 .poll
所以也用不了 epoll 池來監控讀寫事件。所以磁盤 IO 的完成只能同步等待。
然而磁盤 IO 的等待則會帶來 Go 最不能容忍的事情:卡線程。接下來就來看看磁盤 IO 的 read/write 等系統調用的原理。
爲什麼 Go 不能容忍卡線程?
Go 的代碼執行者是系統線程,也就是 G-M-P 模型的 M ,M 不斷的從隊列 P 中取 G(協程任務)出來執行。當 G 出現等待事件的時候(比如網絡 IO),那麼立馬切走,取下一個執行。這樣讓 M 一直不停的滿載,就能保證 Go 協程任務的高吞吐。
那麼問題來了,如果某個 G 卡線程了,就相當於這個 M 被廢了,吞吐能力就下降。如果 M 全卡住了那相當於整個程序卡死了。這個是 Go 絕對無法容忍的。
然而對於類似系統調用這種卡線程卻是無法人爲控制的。Go runtime 爲了解決這個問題,就只能創建更多的線程來保證一直有可運行的 M 。
所以,你經常會發現,當系統調用很慢的時候,M 的數量會變多,甚至會暴漲。曾經,奇伢就遇到過,磁盤大量隨機讀,並且壓力過載的情況,Go 程序線程數持續上漲,最終超過 1 萬個被 panic 了。下面來看一下 Go 怎麼處理這種系統調用的?
Go 的 read/write 系統調用
當文件句柄 read/write 的時候,走系統調用回上下包裝兩個函數:entersyscall,exitsyscall :
entersyscall
// 系統調用 read/write
exitssyscall
其中,這兩個調用都是爲了和 Go runtime 的調度邏輯做交互,協商解決。
entersyscall 的作用:
把當前 M 的 P 設置爲 _Psyscall
狀態,打上標識 解綁 P -> M 的綁定,但 M 還保留 P 的指針。
existsyscall 的作用:
由於 M 到 P 的指向還在,那麼優先還是用原來的 P 如果原來的 P 被處理掉了,那麼就去用一個新的 P ,如果還沒有,那就把只能掛到全局隊列了。
所以,你會發現,這裏最重要的就是一個狀態的標記。Go 的 sysmon(內部監控線程)發現有這種卡了超過 10 ms 的 M ,那麼就會把 P 剝離出來,給到其他的 M 去處理執行,M 數量不夠就會新創建。
系統調用的邏輯是屬於 Go 程序外部代碼,Go 用 entersyscall 和 exitsyscall 來包裝一下,主要是和調度交互。
思考一下,cgo 好像也是外部代碼,它又是怎麼解決阻塞可能導致的問題呢?
也是用的 entersyscall 和 exitssyscall 來配合哦。
總結
-
Go 準確的說是適合網絡 IO 密集型的場景;
-
磁盤 IO 密集型可能會導致系統線程增多,甚至暴漲,超過 1 萬個被 panic 也是可能的哦;
-
系統調用的邏輯是屬於 Go 程序外部代碼邏輯,Go 用 entersyscall 和 exitsyscall 來包裝一下,主要是和調度交互。
奇伢雲存儲 雲存儲深耕之路,專注於對象存儲,塊存儲,雲計算領域。堅持撰寫有思考的技術文章。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/0nwe-YrMGrl2futS5wkT6A