Golang和Erlang的IO調度淺析

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

之前關於調度器的對比分析的文章,在結束時遺留了一些問題:當系統出現高並發的IO訪問時,如一個網路伺服器通常要並發處理成百上千的連結,每個連結可能都是由一個使用者任務執行的,那麼將會出現大量阻塞的IO操作,如果為每個阻塞操作都單獨分配一個OS線程,那麼系統很容易就會退化成多OS線程的系統,輕量任務的優勢將無從談起。本文試圖回答這個問題,通過分析Go和Erlang對於IO、特別是網路IO的最佳化機制,瞭解其對調度器乃至整個系統效能的影響。

Go的IO最佳化機制 —— netpoller

由於Go是一門主要面向互連網環境的分布式語言,相對於一般的IO,如檔案讀寫等,網路IO的並發效能更加重要。對於一般IO,Go的處理方式就是按上篇所說的,將執行Syscall的OS線程剝離。通常應用情境下,不會出現大量並發Goroutine去同時讀寫檔案的情況,因而上面的方式並不會真正造成調度器的退化。因此主要的IO最佳化都是針對io/net庫的。

無專屬偶,Erlang在實現上同樣對網路IO提供了不同於一般IO的高效處理方式,後面再作介紹。

Go實現中利用了OS提供的非阻塞IO訪問模式,並配合epll/kqueue等IO事件監控機制;但是為了彌合OS的非同步機制與Go介面的差異,Go在其庫中做了一些封裝,並在runtime層提供了一種叫做netpoller,“網路輪詢者”的機制,來實現網路IO最佳化。具體來說:

  • 首先,無論何時,當在Go中開啟或接收到一個連結時,其檔案控制代碼都會被設為NONBLOCKING模式。(Go語言庫)
  • 當調用相應的Read/Write等操作時,無論是否成功,都會直接返回而不會阻塞。當傳回值是EAGAIN時,表示IO事件還沒有到達,需要等待。這時,Go庫函數調用PollServerAddFd()將對應檔案控制代碼加入netpoller的監控池,並將當前Goroutine阻塞。(Go語言庫、netpoll.goc中)
  • 當系統中存在空閑 P & M (參見這裡) 時,runtime 會首先尋找本地就緒隊列,若其空,則調用netpoller; netpoller通過OS提供的epoll或kqueue機制,檢查已到達的IO事件,並喚醒對應的Goroutine返回給runtime,將其再度執行。(runtime/proc.c:findrunnable())
  • 最後,Goroutine再次回到Go語言庫上下文時,再調用Read/Write等IO操作時,就可以順利返回了。(Go語言庫)

Erlang的IO最佳化機制之一 —— “Async Threads Pool”

在Erlang中,所有IO操作都需要以Port驅動的形式提供,所謂Port驅動包含一組C回呼函數,用來響應使用者進程的訪問;使用者進程則通過通用的訊息傳遞機制與Port互動。Erlang虛擬機器會把Port當做一種特殊的任務加以調度。

真正的系統調用,如read/write/flush等阻塞式操作都被封裝在Port的回呼函數之中,當調度器調度執行響應的Port時,就會導致當前的調度器執行線程被OS阻塞,從而影響系統的並行性。

Erlang解決該問題的辦法是提供了一組OS線程作為非同步線程池,阻塞的IO操作(以函數指標形式)會被Port註冊到非同步線程池的操作隊列中。非同步線程則執行迴圈操作,取出當前任務隊列的IO任務並執行阻塞操作。

這種方式類似Go對非net類IO及執行阻塞式Syscall的調度方式:用一個單獨的OS線程去執行阻塞操作。

Erlang的file IO基本上就是以上述方式實現的。

因為 Erlang 將調度器映射到一個OS線程而說其調度是1:1的其實是不準確的。基於對阻塞IO的非同步處理及上篇講到的Server Load Balancer機制,使得Erlang實際上也實現了M:N的調度,只不過Erlang的官方文檔並沒有這麼說,只是說單純增加調度器數不會對效能造成影響。

Erlang的IO最佳化機制之二 —— “System Level Activities”

如前所述,無論時Erlang還是Go,都是針對伺服器端設計的語言,因此都提供了不同於一般IO的特殊機制來處理網路IO。

Erlang的做法是提供一種特別的調度單元 —— System Level Activities,來調度非同步IO事件。它的思想和Go的netpoller非常類似:

  • 首先,網路連結對應的控制代碼會被設為NONBLOCKING狀態;
  • 一次IO操作如果在響應事件到來前被調用,則會將其等待的事件註冊到Erlang虛擬機器的IO事件鏈中;
  • 調度器在調度時,會周期性的調用check_io操作來檢查登入的IO事件是否已經到來(利用OS的poll操作),並喚醒響應事件阻塞的使用者任務(進程或Port)。

值得注意的是,Erlang虛擬機器在處理IO事件時,還採用了一種 stealing 的機制。具體來說,當一個driver的函數調用IO操作時,如果對應IO事件沒有到來時,還會主動調用 select_steal()竊取其他登入的IO事件,如果該事件已觸發,則完成相應的讀/寫操作,並通知上層進行後續處理。

Libtask中的非同步IO機制

作為Go語言的前身,Libtask庫同樣實現了非同步IO機制,並且實現方式更加簡潔。

與Go類似,在Libtask中,為使用者級task封裝了IO操作,提供了fdread/fdwrite/fdwait/fdnoblock等介面實現非同步IO。(在libtask提供的例子中,所有IO操作都是針對網路IO的,因此僅就網路IO情況加以分析。)

  • 連結控制代碼首先會通過調用fdblock()被設為NONBLOCKING態;
  • 之後調用fdread/fdwrite時,一旦返回EAGAIN,則調用fdwait,註冊等待IO事件並將自身調出;
  • Libtask會在第一次接收到IO事件註冊後建立一個系統任務fdtask,該任務通過調用poll系統調用檢查新到來的IO事件,並將對應任務重新加到就緒隊列中。

總結及參考

通過上文分析,瞭解到IO最佳化對調度器乃至語言本身效能的影響。這與兩種語言的應用背景——伺服器端編程有很大關係。

通常來說,應用程式必須通過Syscall 訪問操作的特定功能,這就會涉及底層 OS 的調度機制,作為使用者態的任務調度器,Erlang虛擬機器或Go的運行時系統都必須對核心調度引入的不確定性加以控制。特別是 IO 操作這類特殊並且會大量訪問到的Syscall,必須設計有針對性的最佳化方案,才能確保高的並發效能。

Go 和 Erlang 的實現方案隨不盡相同,但核心的思想都是類似的,通過非同步IO 最佳化基於Socket的操作,而對於一般的檔案讀寫,則直接讓執行線程及啟動並執行使用者任務阻塞,調度器再將其他可以執行的任務綁定到其他OS線程繼續執行。

這篇文章除了參考了Erlang/OTP及Go語言的原始碼外,還參考了以下資料:

  • Morsing “The Go netpoller” http://morsmachine.dk/netpoller
  • Ramblings “How Erlang does scheduling” http://jlouisramblings.blogspot.com/2013/01/how-erlang-does-scheduling.html

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.