【Vert.x準備篇2】C10K問題與Reactor模式

知乎專欄: 關(guān)于Vert.x你需要知道的一切

C10K問題是1999年一個叫Dan Kegel的美國人提出的概念,其中Cconcurrently, 10K指的是1萬個網(wǎng)絡(luò)連接, 結(jié)合起來意為如何能夠做到并發(fā)處理1萬個連接。

這里首先要澄清一下,并發(fā)(concurrency)和并行(parallel)雖然都是用來描述"同時"干多件事的名詞,但他們是有本質(zhì)區(qū)別的。并發(fā)指的是CPU通過在不同的線程之間快速切換來營造出一種多個線程同時在執(zhí)行的假象, 而并行則是真正意義上的多個線程在同時運行。

對于現(xiàn)代操作系統(tǒng)來說,C10K問題的核心在于線程(或進程)無法隨著連接數(shù)的增加而無休止的創(chuàng)建。對于web應(yīng)用,我們平時最常用也是最熟悉的線程模型就是"一請求一線程"模型,但它難以解決C10K的原因是單個線程會占用較多的內(nèi)存資源,內(nèi)存的有限性就決定了線程的總數(shù)是有限制的。即便現(xiàn)在很多企業(yè)級服務(wù)器都是100G+的內(nèi)存,線程切換所帶來的開銷也會隨著線程數(shù)量的增加而增加,從而進一步限制了有效線程的數(shù)量。這樣看來解決問題的唯一方法,就是想辦法讓單個線程能在不發(fā)生阻塞的前提下處理多個連接了。于是,在操作系統(tǒng)的支持下,reactor模式誕生。

Reactor模式

Reactor是一種基于事件驅(qū)動的設(shè)計模式,它可以做到只用少量的線程就能處理大量的I/O操作。簡單來說,一個Reactor就是一個事件循環(huán),執(zhí)行這個循環(huán)的線程會阻塞在多路復(fù)用器的select()調(diào)用中,當感興趣的I/O事件發(fā)生時,操作系統(tǒng)會讓select()函數(shù)返回,同時告訴你發(fā)生了哪些事件,然后當前線程會將事件分發(fā)給事件對應(yīng)的處理器(Handler)來進行處理, 處理完成后再進入下一次循環(huán),以此類推。在這個過程中主要有以下三個核心組件:

  • Reactor

由一條線程執(zhí)行的無限循環(huán),其任務(wù)就是等待操作系統(tǒng)通知I/O ready事件的發(fā)生然后將這些事件分派給對應(yīng)的處理器來處理。

  • Demultiplex

即多路復(fù)用器,其作用為讓當前線程阻塞在等待事件發(fā)生的過程上,然后在事件發(fā)生時返回。其實多路復(fù)用起初是通訊工程中的術(shù)語,本意是讓多種不同的信號在同一條物理線路上傳輸。在這里多路是指可同時監(jiān)聽多個I/O事件,復(fù)用是指對這些事件的處理復(fù)用同一條線程。前面我們在說Reactor的誕生有一個前提條件,即必須有操作系統(tǒng)的支持。在這里操作系統(tǒng)就扮演著通知應(yīng)用程序的角色,具體的通知方式在不同的OS有著不同的實現(xiàn),如epoll(Linux), k-queue(freeBSD), iocp(windows)等。

  • Handler

事件處理器。事件處理器首先要向多路復(fù)用器告知自己對哪些事件感興趣,然后當這些事件發(fā)生時自身會被調(diào)用。其實Handler就是我們需要實現(xiàn)的業(yè)務(wù)邏輯,但需要注意的一點是,無論如何都不能阻塞Handler, 因為這會直接block整個事件循環(huán)。

現(xiàn)在我們來看一下Reactor是如何做到用少量線程來處理大量I/O的。假定現(xiàn)在已經(jīng)建立了10個HTTP連接且我們想讓服務(wù)器同時為這10個client服務(wù)而不是像排隊一樣完成前一個再處理下一個。在一請求一線程的模型下,我們需要啟動10條線程來調(diào)用receive()方法并block在這個方法調(diào)用上, 這樣只要有請求數(shù)據(jù)到來這些線程就會馬上進行處理。不過在Reactor模式里,我們可以只使用一條線程先向Demultiplex注冊一下我們對這10個連接的"讀就緒"事件感興趣并綁定對應(yīng)的Handler,之后block在select()調(diào)用上; 當事件發(fā)生后(如這10個連接的讀就緒事件同時發(fā)生),我們的主線程從select()中返回,主線程遍歷所有的事件并調(diào)用對應(yīng)的事件處理器完成業(yè)務(wù)邏輯。這樣我們僅用一個線程就完成了先前10個線程才能完成的工作。當然,這里是有一些限制條件的,比如Handler中絕對不允許出現(xiàn)阻塞代碼。但是業(yè)務(wù)邏輯難免會有像數(shù)據(jù)庫查詢這樣的阻塞且耗時的調(diào)用, 該怎么辦呢?答案是在Handler中只發(fā)起DB查詢?nèi)缓罅⒓捶祷?,發(fā)起后告知Demultiplex我們對DB查詢完成的事件感興趣, 當DB返回結(jié)果時再喚醒Handler進行后續(xù)處理。在Vert.x中,注冊和等待事件ready的邏輯框架都會為我們代勞,我們只需要專注于編寫Handler即可。

世面上各框架在實現(xiàn)Reactor時都會有很多變種,例如在Vert.x中,Reactor會有多個,其負責(zé)執(zhí)行Reactor事件循環(huán)的NIO線程也會有多條, 而不是上面最簡單的單Reactor模型。

從上面的討論可以看到,使用Reactor模式并不能降低服務(wù)器對于單個請求處理的總耗時,而是能最大限度的減少線程阻塞(提高CPU利用率),從而大大減少了處理10K連接時需要的線程數(shù)量,進而提高了服務(wù)器的并發(fā)處理能力。不過這樣也給程序員帶來了麻煩,即我們需要為各種會block線程的操作注冊各種回調(diào),導(dǎo)致業(yè)務(wù)邏輯從先前線性的"一本道"變成了被迫分散在各個回調(diào)方法中。魚和熊掌不可兼得,如果你真遇到了高并發(fā)問題,那么就只能犧牲一下代碼了【Go語言除外??】。

與Proactor的區(qū)別

談到reactor就不得不提一句proactor。這二者最根本的區(qū)別在于,Reactor中監(jiān)聽的是I/O就緒事件,此時數(shù)據(jù)還在操作系統(tǒng)的內(nèi)核緩沖區(qū),線程被喚醒后需要主動將數(shù)據(jù)從內(nèi)核緩沖區(qū)讀取到用戶進程中; 而Proactor里用戶進程監(jiān)聽的是I/O完成事件,即當線程被喚醒時,數(shù)據(jù)已經(jīng)從內(nèi)核轉(zhuǎn)移到進程中了,這個過程是操作系統(tǒng)幫你完成的,你只需要在發(fā)起I/O時提供一個buffer, 等I/O完成時buffer已經(jīng)被填滿,而不需要你手動從read()中獲取了。

Tomcat為什么"搞不定"高并發(fā)

首先,這不是tomcat的鍋,而是servlet規(guī)范(3.0之前)和業(yè)務(wù)代碼的問題。自Tomcat6開始就已經(jīng)支持了JDK的NIO, 可以使用少量的線程處理大量的I/O事件,問題在于servlet和你的業(yè)務(wù)代碼是同步的。也就是說,即便tomcat使用了某種黑魔法,僅用了一個線程就能搞定N個連接的創(chuàng)建和讀寫操作,但當tomcat調(diào)用servlet處理業(yè)務(wù)邏輯時仍然需要從維護的worker線程池中取一個線程來執(zhí)行,這就又回到一請求一線程的模式了----只有同時啟動10K條線程才能真正完成10K個請求的處理,否則后面的請求盡管已經(jīng)完成了連接的建立和數(shù)據(jù)的接收,也只能是在一味的等待,等待前面的worker線程干完活才能來處理后面的請求。所以,只要servlet和你的業(yè)務(wù)代碼也異步起來,Tomcat完全可以搞定C10K。只可惜,servlet3.0來的太晚了,異步編程的江山已經(jīng)被Netty, Vert.x, Akka和Node.js這樣的框架(工具)瓜分完畢了。

下一篇我們會介紹Vert.x中的核心組件,以及它是如何實現(xiàn)Reactor模式的。

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

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容