聊聊非阻塞I/O編程

寫在前面

隨著互聯網的發(fā)展,面對海量用戶高并發(fā)業(yè)務,傳統的阻塞I/O架構已經無能為力,改善阻塞問題是服務器高性能架構的關鍵優(yōu)化點,本篇文章介紹非阻塞I/O編程的實現。

阻塞I/O與非阻塞I/O

阻塞和非阻塞的區(qū)別點在于,線程在發(fā)起接口調用(發(fā)出請求)后,等待操作完成期間,線程是否被掛起無法執(zhí)行其他操作。

跟阻塞/非阻塞概念常常一起比較的,還有同步和異步的概念:同步和異步關注的是一個執(zhí)行流程中每個方法是否必須依賴前一個方法完成后才可以繼續(xù)執(zhí)行,實現異步的手段一般是將前面方法一直接交給其他線程執(zhí)行,不由主線程執(zhí)行,也就不會阻塞主線程,所以后面的方法二不必等到方法一完成即可開始執(zhí)行。

  • 阻塞I/O

用戶線程發(fā)起 I/O 操作后會被掛起,需要阻塞等待直到操作完成,阻塞期間線程不能處理別的任務,此時想同時處理其他I/O操作需基于另外的新線程。操作系統層面對線程的個數是有限制的,當線程數過多,會引起CPU頻繁進行線程上下文切換造成CPU的消耗。

  • 非阻塞I/O
    I/O 操作都是調用之后立刻返回而不會阻塞當前用戶線程,當操作處理完成之后,再觸發(fā)用戶線程繼續(xù)執(zhí)行后續(xù)操作。

對于單個請求,非阻塞I/O相對阻塞I/O,并不會縮短處理耗時,但從整個系統,非阻塞編程可以讓相同數量的線程在相同時間內處理更多請求,提高整個系統的吞吐量。

非阻塞I/O編程

1 多路I/O復用

使用非阻塞I/O,當I/O操作處理完成,如何使用戶線程知道,并觸發(fā)執(zhí)行后續(xù)操作?可以通過另外的線程主動監(jiān)聽I/O事件是否處理完成,而且不僅只監(jiān)聽一個I/O事件,而是多個,即I/O多路復用。

I/O 多路復用指的就是 select/poll/epoll 這一系列的API:支持單一線程同時阻塞等待監(jiān)聽多個文件描述符(I/O 事件),并在其中某個文件描述符可讀寫時由os喚醒阻塞等待的線程。 I/O 復用其實復用的不是 I/O 連接,而是復用線程,讓線程能夠監(jiān)聽多個連接(I/O 事件)。

I/O復用在不同的操作系統有不同的實現,這里以Linux最常用的epoll為例進行介紹
epoll 的 API 非常簡潔,涉及到3 個系統調用:

  • epoll_create();
    創(chuàng)建并返回一個 內核數據對象epoll實例。

  • epoll_ctl();
    添加/刪除/修改file descriptor(socket連接)等待的 I/O 事件到 epoll 實例上。

  • epoll_wait()
    指定超時時間阻塞監(jiān)聽 epoll 實例上所有的 file descriptor 的 I/O 事件,接收一個用戶空間上的一塊內存地址,kernel 會在 I/O 事件就緒時候把文件描述符列表復制到這塊內存地址上,然后 epoll_wait 解除阻塞并返回,最后用戶空間上的程序就可以對相應的 fd 進行讀寫了

2 Java實現

為了更好理解,先看一段Java服務端的簡化示例代碼

ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(port));
Selector selector = Selector.open();
//Channel注冊到Selector中
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true) {
    int n = selector.select();
    if (n == 0) continue;
    Iterator ite = this.selector.selectedKeys().iterator();
    while(ite.hasNext()) {
        SelectionKey key = (SelectionKey)ite.next();
        if(key.isAcceptable()) {
            SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
            //將socket注冊到selector上
            clientChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(bufSize));
        }
        if (key.isReadable()) {
            handleRead(key);
        }
        if (key.isWritable() && key.isValid()) {
            handleWrite(key);
        }
        ite.remove();
    }
}

jdk中Selector是對操作系統的I/O多路復用調用的一個封裝,在Linux中默認基于epoll的實現。
SelectionKey是對I/O事件的封裝,而SocketChannel 是對客戶端socket連接的封裝。

工作流程如下:

  • 創(chuàng)建服務端Socket對象,并開始監(jiān)聽指定端口。
  • 創(chuàng)建Selector對象,并將服務端Socket對象注冊到它上面。
  • 阻塞監(jiān)聽就緒的I/O事件,當監(jiān)聽到客戶端Socket連接建立事件,將該連接注冊到Selector上,監(jiān)聽該連接上的后續(xù)的I/O事件。
  • 監(jiān)聽到客戶端連接的I/O事件可讀或可寫,觸發(fā)相應的事件處理。

3 Netty實現

Java nio對多路I/O復用做了基礎的封裝,沒有實現I/O事件的多線程處理,Netty在Java nio的基礎上做了進一步的封裝,實現Reactor模型。

Reactor是的中文是反應堆,對應是“事件反應”,可以通俗理解為“來了一個事件觸發(fā)相應的反應”,簡單理解的話,就是I/O多路復用+線程池,Reactor 會根據事件類型來調用相應的代碼進行處理。Reactor 模式也叫 Dispatcher 模式,即 I/O 多路復用統一監(jiān)聽事件,收到事件后分配(Dispatch)給某個線程。

Reactor模型較為常見的主從Reactor模型設計是:系統有2個線程池,主線程池和子線程池:

  • 在主線程池中運行的的MainReacor內置Selector負責監(jiān)聽連接建立事件(accept),當連接建立之后,分發(fā)給子線程池。
  • 在子線程池中運行的SubReactor負責監(jiān)聽連接數據就緒可讀事件,然后進行業(yè)務處理和寫回響應數據。

設計成2個線程池分開的好處在于,主線程池和子線程池的職責非常明確,主線程池只負責接收新連接,子線程池負責完成后續(xù)的業(yè)務處理,避免相互影響。

Netty的異步事件驅動模型本質上是Reacor模型,其中“事件”可以理解成I/O復用中監(jiān)聽的各種I/O事件,包括連接建立,連接上數據就緒可讀,連接上數據已完成寫入、連接關閉等。通過Selector對象,不斷監(jiān)聽I/O事件,驅動觸發(fā)相應的處理邏輯。包含的組件及其工作原理如下:

  • Boss Group是Reactor模型中的主線程池,內置Selector對象和一個NioEventLoop對象。
  • Worker Group是Reactor模型中的子線程池,內置Selector對象和多個NioEventLoop對象。
  • NioEventLoop內部維護了一個處理線程,線程的執(zhí)行邏輯是從當前線程池的Selector進行select,獲取出就緒的I/O事件進行處理(processSelectedKeys)。NioEventLoop同時也維護了一個內部任務隊列,最終執(zhí)行runAllTasks 方法,處理被提交到任務隊列中的任務。
  • Boss Group中的NioEventLoop的processSelectedKeys處理連接就緒事件(acceptable),與客戶端建立連接,并將連接注冊到Worker Group內置的Selector中。
  • Worker Group中的NioEventLoop的processSelectedKeys調用當前客戶端連接(channel)的事件處理器(ChannelHanndler)處理具體業(yè)務邏輯。

4 Node.js實現

Node.js高性能服務端JavaScriptpt運行平臺,底層通過Bindings調用C/C++的libuv庫實現異步事件驅動。

Node.js單線程只是一個js主線程,本質上的異步操作還是由線程池完成的,Node.js將所有的阻塞操作都交給了libuv庫內部線程池去實現,本身只負責不斷地往返調度,并沒有進行真正的I/O操作,從而實現異步非阻塞I/O。

基本執(zhí)行原理如下:

  • Node.js將異步任務放入事件隊列中(Event Queue)。
  • libuv主線程從事件隊列不斷循環(huán)取出事件,驅動所有的異步回調函數的執(zhí)行,Event Loop總共6個階段,每個階段都有一個子事件隊列,當所有階段被順序執(zhí)行一次后,event loop 完成了一個 tick。
  • Event Loop執(zhí)行過程中,如果I/O操作有注冊回調,都是提交到libuv的線程池的工作線程來執(zhí)行,實現異步I/O,例如文件操作,網絡連接讀寫。當操作完成后工作線程更新 file descriptor,libuv主線程通過多路I/O復用(例如epool)監(jiān)聽file descriptor,再層層回調,最終會調用到用戶注冊的回調函數。

5 Golang實現

前面介紹的幾種非阻塞I/O的實現,為了避免I/O操作阻塞線程而采用異步寫入的方式,然后再基于I/O多路復用監(jiān)聽到I/O操作完成,再觸發(fā)后續(xù)操作。這樣做雖然可以提高性能,但需要編寫相關異步回調邏輯,相比同步順序執(zhí)行程序,異步回調邏輯并不友好,帶來一定的代碼復雜度,例如回調地獄問題,程序上下文變量/對象如何傳遞到異步回調程序。

有沒有什么方案,既兼顧性能,實現線程非阻塞I/O,又程序友好,代碼同步順序執(zhí)行而不是異步回調的方案?看似相互矛盾的需求,看看Golang的協程方案如何實現:


大部分編程語言的線程庫(例如C++11的std::thread、Java的java.lang.Thread)都是對操作系統的線程(內核級線程)的一層封裝,因此其管理和調度完全由OS調度器來做,這種方式實現簡單,但在需要使用大量線程的場景下對OS的性能影響會很大。

Go的協程(goroutine)是一種用戶態(tài)的輕量級線程,協程的調度完全由用戶控制。協程的最大優(yōu)勢在于其"輕量級",可以輕松創(chuàng)建上百萬個而不會導致系統資源衰竭,Go通過實現一個調度器,實現協程與內核線程的動態(tài)關聯調度,OS內核的調度器實現內核線程到CPU的調度。

Go的I/O 多路復用netpoller模型,一個goroutine處理一個客戶端連接來處理(goroutine-per-connection)實現非阻塞I/O基本原理如下:

  • (1) goroutine處理客戶端I/O事件,(例如建立連接、讀寫連接數據),Linux系統下對應epoll_ctl在內核空間注冊待監(jiān)聽事件,goroutine調用相關netpoller的I/O事件API之后,進入休眠狀態(tài)(gopark)
  • (2) 當I/O事件就緒,可以通過runtime.netpoll(相當于epoll實例對象),獲取休眠狀態(tài)goroutine的并進行喚醒,runtime.netpoll觸發(fā)場景有以下2個:
    • Go的調度器Go scheduler調用;
    • Go runtime 在程序啟動的時候會創(chuàng)建一個獨立的sysmon監(jiān)控線程定時調用。
  • (3) goroutine被喚醒之后,繼續(xù)執(zhí)行后續(xù)業(yè)務邏輯。

Java中的BIO為每一路連接單獨分配線程來處理的性能并不高,而在Go中可以為每一路連接單獨分配輕量級的goroutine進行高性能處理,在每個goroutine協程中,調用I/O操作API時,代碼同步順序執(zhí)行,不用寫I/O事件完成時的異步操作回調代碼。

參考

《EPOLL_CTL_DISABLE and multithreaded applications》 https://lwn.net/Articles/520012/
《go-netpoll-io-multiplexing-reactor]》https://strikefreedom.top/go-netpoll-io-multiplexing-reactor
《Go netpoller 原生網絡模型之源碼全面揭秘》https://strikefreedom.top/go-netpoll-io-multiplexing-reactor
《libuv源碼閱讀》http://masutangu.com/2016/10/13/libuv-source-code/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容