知乎專欄: 關(guān)于Vert.x你需要知道的一切
C10K問題是1999年一個(gè)叫Dan Kegel的美國(guó)人提出的概念,其中C
為concurrently
, 10K
指的是1萬個(gè)網(wǎng)絡(luò)連接, 結(jié)合起來意為如何能夠做到并發(fā)處理1萬個(gè)連接。
這里首先要澄清一下,并發(fā)(concurrency)和并行(parallel)雖然都是用來描述"同時(shí)"干多件事的名詞,但他們是有本質(zhì)區(qū)別的。并發(fā)指的是CPU通過在不同的線程之間快速切換來營(yíng)造出一種多個(gè)線程同時(shí)在執(zhí)行的假象, 而并行則是真正意義上的多個(gè)線程在同時(shí)運(yùn)行。
對(duì)于現(xiàn)代操作系統(tǒng)來說,C10K問題的核心在于線程(或進(jìn)程)無法隨著連接數(shù)的增加而無休止的創(chuàng)建。對(duì)于web應(yīng)用,我們平時(shí)最常用也是最熟悉的線程模型就是"一請(qǐng)求一線程"模型,但它難以解決C10K的原因是單個(gè)線程會(huì)占用較多的內(nèi)存資源,內(nèi)存的有限性就決定了線程的總數(shù)是有限制的。即便現(xiàn)在很多企業(yè)級(jí)服務(wù)器都是100G+的內(nèi)存,線程切換所帶來的開銷也會(huì)隨著線程數(shù)量的增加而增加,從而進(jìn)一步限制了有效線程的數(shù)量。這樣看來解決問題的唯一方法,就是想辦法讓單個(gè)線程能在不發(fā)生阻塞的前提下處理多個(gè)連接了。于是,在操作系統(tǒng)的支持下,reactor模式誕生。
Reactor模式
Reactor是一種基于事件驅(qū)動(dòng)的設(shè)計(jì)模式,它可以做到只用少量的線程就能處理大量的I/O操作。簡(jiǎn)單來說,一個(gè)Reactor就是一個(gè)事件循環(huán),執(zhí)行這個(gè)循環(huán)的線程會(huì)阻塞在多路復(fù)用器的select()
調(diào)用中,當(dāng)感興趣的I/O事件發(fā)生時(shí),操作系統(tǒng)會(huì)讓select()
函數(shù)返回,同時(shí)告訴你發(fā)生了哪些事件,然后當(dāng)前線程會(huì)將事件分發(fā)給事件對(duì)應(yīng)的處理器(Handler)來進(jìn)行處理, 處理完成后再進(jìn)入下一次循環(huán),以此類推。在這個(gè)過程中主要有以下三個(gè)核心組件:
- Reactor
由一條線程執(zhí)行的無限循環(huán),其任務(wù)就是等待操作系統(tǒng)通知I/O ready事件的發(fā)生然后將這些事件分派給對(duì)應(yīng)的處理器來處理。
- Demultiplex
即多路復(fù)用器,其作用為讓當(dāng)前線程阻塞在等待事件發(fā)生的過程上,然后在事件發(fā)生時(shí)返回。其實(shí)多路復(fù)用起初是通訊工程中的術(shù)語,本意是讓多種不同的信號(hào)在同一條物理線路上傳輸。在這里多路是指可同時(shí)監(jiān)聽多個(gè)I/O事件,復(fù)用是指對(duì)這些事件的處理復(fù)用同一條線程。前面我們?cè)谡fReactor的誕生有一個(gè)前提條件,即必須有操作系統(tǒng)的支持。在這里操作系統(tǒng)就扮演著通知應(yīng)用程序的角色,具體的通知方式在不同的OS有著不同的實(shí)現(xiàn),如epoll(Linux), k-queue(freeBSD), iocp(windows)等。
- Handler
事件處理器。事件處理器首先要向多路復(fù)用器告知自己對(duì)哪些事件感興趣,然后當(dāng)這些事件發(fā)生時(shí)自身會(huì)被調(diào)用。其實(shí)Handler就是我們需要實(shí)現(xiàn)的業(yè)務(wù)邏輯,但需要注意的一點(diǎn)是,無論如何都不能阻塞Handler, 因?yàn)檫@會(huì)直接block整個(gè)事件循環(huán)。
現(xiàn)在我們來看一下Reactor是如何做到用少量線程來處理大量I/O的。假定現(xiàn)在已經(jīng)建立了10個(gè)HTTP連接且我們想讓服務(wù)器同時(shí)為這10個(gè)client服務(wù)而不是像排隊(duì)一樣完成前一個(gè)再處理下一個(gè)。在一請(qǐng)求一線程的模型下,我們需要啟動(dòng)10條線程來調(diào)用receive()
方法并block在這個(gè)方法調(diào)用上, 這樣只要有請(qǐng)求數(shù)據(jù)到來這些線程就會(huì)馬上進(jìn)行處理。不過在Reactor模式里,我們可以只使用一條線程先向Demultiplex注冊(cè)一下我們對(duì)這10個(gè)連接的"讀就緒"事件感興趣并綁定對(duì)應(yīng)的Handler,之后block在select()
調(diào)用上; 當(dāng)事件發(fā)生后(如這10個(gè)連接的讀就緒事件同時(shí)發(fā)生),我們的主線程從select()
中返回,主線程遍歷所有的事件并調(diào)用對(duì)應(yīng)的事件處理器完成業(yè)務(wù)邏輯。這樣我們僅用一個(gè)線程就完成了先前10個(gè)線程才能完成的工作。當(dāng)然,這里是有一些限制條件的,比如Handler中絕對(duì)不允許出現(xiàn)阻塞代碼。但是業(yè)務(wù)邏輯難免會(huì)有像數(shù)據(jù)庫查詢這樣的阻塞且耗時(shí)的調(diào)用, 該怎么辦呢?答案是在Handler中只發(fā)起DB查詢?nèi)缓罅⒓捶祷兀l(fā)起后告知Demultiplex我們對(duì)DB查詢完成的事件感興趣, 當(dāng)DB返回結(jié)果時(shí)再喚醒Handler進(jìn)行后續(xù)處理。在Vert.x中,注冊(cè)和等待事件ready的邏輯框架都會(huì)為我們代勞,我們只需要專注于編寫Handler即可。
世面上各框架在實(shí)現(xiàn)Reactor時(shí)都會(huì)有很多變種,例如在Vert.x中,Reactor會(huì)有多個(gè),其負(fù)責(zé)執(zhí)行Reactor事件循環(huán)的NIO線程也會(huì)有多條, 而不是上面最簡(jiǎn)單的單Reactor模型。
從上面的討論可以看到,使用Reactor模式并不能降低服務(wù)器對(duì)于單個(gè)請(qǐng)求處理的總耗時(shí),而是能最大限度的減少線程阻塞(提高CPU利用率),從而大大減少了處理10K連接時(shí)需要的線程數(shù)量,進(jìn)而提高了服務(wù)器的并發(fā)處理能力。不過這樣也給程序員帶來了麻煩,即我們需要為各種會(huì)block線程的操作注冊(cè)各種回調(diào),導(dǎo)致業(yè)務(wù)邏輯從先前線性的"一本道"變成了被迫分散在各個(gè)回調(diào)方法中。魚和熊掌不可兼得,如果你真遇到了高并發(fā)問題,那么就只能犧牲一下代碼了【Go語言除外??】。
與Proactor的區(qū)別
談到reactor就不得不提一句proactor。這二者最根本的區(qū)別在于,Reactor中監(jiān)聽的是I/O就緒事件,此時(shí)數(shù)據(jù)還在操作系統(tǒng)的內(nèi)核緩沖區(qū),線程被喚醒后需要主動(dòng)將數(shù)據(jù)從內(nèi)核緩沖區(qū)讀取到用戶進(jìn)程中; 而Proactor里用戶進(jìn)程監(jiān)聽的是I/O完成事件,即當(dāng)線程被喚醒時(shí),數(shù)據(jù)已經(jīng)從內(nèi)核轉(zhuǎn)移到進(jìn)程中了,這個(gè)過程是操作系統(tǒng)幫你完成的,你只需要在發(fā)起I/O時(shí)提供一個(gè)buffer, 等I/O完成時(shí)buffer已經(jīng)被填滿,而不需要你手動(dòng)從read()
中獲取了。
Tomcat為什么"搞不定"高并發(fā)
首先,這不是tomcat的鍋,而是servlet規(guī)范(3.0之前)和業(yè)務(wù)代碼的問題。自Tomcat6開始就已經(jīng)支持了JDK的NIO, 可以使用少量的線程處理大量的I/O事件,問題在于servlet和你的業(yè)務(wù)代碼是同步的。也就是說,即便tomcat使用了某種黑魔法,僅用了一個(gè)線程就能搞定N個(gè)連接的創(chuàng)建和讀寫操作,但當(dāng)tomcat調(diào)用servlet處理業(yè)務(wù)邏輯時(shí)仍然需要從維護(hù)的worker線程池中取一個(gè)線程來執(zhí)行,這就又回到一請(qǐng)求一線程的模式了----只有同時(shí)啟動(dòng)10K條線程才能真正完成10K個(gè)請(qǐng)求的處理,否則后面的請(qǐng)求盡管已經(jīng)完成了連接的建立和數(shù)據(jù)的接收,也只能是在一味的等待,等待前面的worker線程干完活才能來處理后面的請(qǐng)求。所以,只要servlet和你的業(yè)務(wù)代碼也異步起來,Tomcat完全可以搞定C10K。只可惜,servlet3.0來的太晚了,異步編程的江山已經(jīng)被Netty, Vert.x, Akka和Node.js這樣的框架(工具)瓜分完畢了。
下一篇我們會(huì)介紹Vert.x中的核心組件,以及它是如何實(shí)現(xiàn)Reactor模式的。