圖解 | 深入理解Linux高性能網(wǎng)絡(luò)架構(gòu)的那些事
本文轉(zhuǎn)載自微信公眾號(hào)「后端技術(shù)指南針」,作者指南針氪金入口。轉(zhuǎn)載本文請(qǐng)聯(lián)系后端技術(shù)指南針公眾號(hào)。
1. 落寞的小黑
上周北京很冷,周五晚上大白下班奔地鐵站,收到了好基友小黑的微信:
于是大白掉頭掃了個(gè)單車(chē)奔五道口了,小黑靠譜地選了個(gè)不錯(cuò)的位置。
小黑: 你今天下班挺早呀!
大白: 就咱這覺(jué)悟,心里有工作,哪里都是辦公桌,不要拘泥于形式嘛。
明顯能感覺(jué)得到小黑哥最近好像比較累,之前眼里bulingbuling閃的光是看不到了。
大白: 下午去面的哪家?啥崗位?咋樣?
小黑: 是一家做自動(dòng)駕駛的創(chuàng)業(yè)公司,網(wǎng)站是看團(tuán)隊(duì)介紹還不錯(cuò),就去看看了,這次沒(méi)咋準(zhǔn)備,很多問(wèn)題其實(shí)都熟悉,但是回答的不到位。
大白: 哦,明白了,那就是當(dāng)時(shí)理解的不到位,稀里糊涂過(guò)去了,現(xiàn)在忽然問(wèn)起來(lái),想不起重點(diǎn)。
小黑: 差不多吧,問(wèn)我都做過(guò)哪些高性能的網(wǎng)絡(luò)框架模型,也就是IO和事件驅(qū)動(dòng)那一套。
話(huà)說(shuō)完,小黑喝了一大口啤酒,大白看出了小黑心里有一些落寞。畢竟在帝都這個(gè)地方競(jìng)爭(zhēng)和工作壓力,以及生活瑣事都一直圍繞著我們,但是金錢(qián)和好運(yùn)都巧妙地避開(kāi)了自己...
想到這里,大白也深深喝了一大口,我命由我不由天,開(kāi)整!
大白:黑哥,你說(shuō)這個(gè)問(wèn)題確實(shí)不好回答,全是術(shù)語(yǔ)和略帶歧義的東西,我覺(jué)得我們抓住本質(zhì)去闡述就好。
小黑:來(lái),請(qǐng)開(kāi)始你的表演,我學(xué)習(xí)學(xué)習(xí)。
大白決定和小黑好好聊聊,Linux開(kāi)發(fā)中常用的高性能網(wǎng)絡(luò)框架中的一些事兒,火鍋的映襯下讓夜色和天氣都不那么寒冷了。
通過(guò)本文你將會(huì)了解到以下內(nèi)容:
- IO事件和IO復(fù)用
- 線(xiàn)程模型和事件驅(qū)動(dòng)模型的架構(gòu)
- 基于事件驅(qū)動(dòng)的Reactor模式詳解
- 同步IO和異步IO簡(jiǎn)介
2. IO事件和IO復(fù)用
2.1 什么是IO事件
IO指的是輸入Input/輸出Output,但是從漢語(yǔ)角度來(lái)說(shuō),出和入是相對(duì)的,所以我們需要個(gè)參照物。這里我們的參照物選擇為程序運(yùn)行時(shí)的主存儲(chǔ)空間,外部通常包括網(wǎng)卡、磁盤(pán)等。有了上述的設(shè)定理解起來(lái)就方便多了,我們來(lái)一起看下:
IO的本質(zhì)是數(shù)據(jù)的流動(dòng),數(shù)據(jù)可以從網(wǎng)卡到程序內(nèi)存,也可以從程序內(nèi)存寫(xiě)到網(wǎng)卡,磁盤(pán)操作也是如此。
所以可以把常見(jiàn)的IO分為:
- 網(wǎng)絡(luò)IO:內(nèi)存和網(wǎng)卡的數(shù)據(jù)交互
- 文件IO:內(nèi)存和磁盤(pán)的數(shù)據(jù)交互
那什么又是IO事件呢?事件可以理解為一種狀態(tài)或者動(dòng)作,也就是狀態(tài)的遷移會(huì)觸發(fā)一種相應(yīng)的動(dòng)作。網(wǎng)絡(luò)IO的事件通常包括:
- 可讀事件
- 可寫(xiě)事件
- 異常事件
理解可讀可寫(xiě)事件是非常有必要的,一般來(lái)說(shuō)一個(gè)socket大部分時(shí)候是可寫(xiě)的,但是并不是都可讀。可讀一般代表是一個(gè)新連接或者原有連接有新數(shù)據(jù)交互,對(duì)于服務(wù)端程序來(lái)說(shuō)也是重點(diǎn)關(guān)注的事件。
2.2 什么是IO復(fù)用
設(shè)想假如有幾萬(wàn)個(gè)IO事件,那么應(yīng)用程序該如何管理呢?這就要提到IO復(fù)用了。IO復(fù)用從本質(zhì)上來(lái)說(shuō)就是應(yīng)用程序借助于IO復(fù)用函數(shù)向內(nèi)核注冊(cè)很多類(lèi)型的IO事件,當(dāng)這些注冊(cè)的IO事件發(fā)生變化時(shí)內(nèi)核就通過(guò)IO復(fù)用函數(shù)來(lái)通知應(yīng)用程序。
從圖中可以看到,IO復(fù)用中復(fù)用的就是一個(gè)負(fù)責(zé)監(jiān)聽(tīng)管理這些IO事件的線(xiàn)程。之所以可以實(shí)現(xiàn)一個(gè)線(xiàn)程管理成百上千個(gè)IO事件,是因?yàn)榇蟛糠謺r(shí)間里某個(gè)時(shí)刻只有少量IO事件被觸發(fā)。
大概就像這樣:草原上的一只大狗可以看管幾十只綿羊,因?yàn)榇蟛糠謺r(shí)候只有個(gè)別綿羊不守規(guī)矩亂跑,其他的都是乖乖吃草。
3. 網(wǎng)絡(luò)框架設(shè)計(jì)要素
要理解網(wǎng)絡(luò)框架有哪些,必須要清楚網(wǎng)絡(luò)框架完成了哪些事情。
大致描述下這個(gè)請(qǐng)求處理的流程:
- 遠(yuǎn)端的機(jī)器A發(fā)送了一個(gè)HTTP請(qǐng)求到服務(wù)器B,此時(shí)服務(wù)器B網(wǎng)卡接收到數(shù)據(jù)并產(chǎn)生一個(gè)IO可讀事件;
- 我們以同步IO為例,此時(shí)內(nèi)核將該可讀事件通知到應(yīng)用程序的Listen線(xiàn)程;
- Listen線(xiàn)程將任務(wù)甩給Handler線(xiàn)程,由Handler將數(shù)據(jù)從內(nèi)核讀緩沖區(qū)拷貝到用戶(hù)空間讀緩沖區(qū);
- 請(qǐng)求數(shù)據(jù)包在應(yīng)用程序內(nèi)部進(jìn)行計(jì)算和處理并封裝響應(yīng)包;
- Handler線(xiàn)程等待可寫(xiě)事件的到來(lái);
- 當(dāng)這個(gè)連接可寫(xiě)時(shí)將數(shù)據(jù)從用戶(hù)態(tài)寫(xiě)緩沖區(qū)拷貝到內(nèi)核緩沖區(qū),并通過(guò)網(wǎng)卡發(fā)送出去;
備注:上述例子是以同步IO為例,并且將線(xiàn)程中的角色分為L(zhǎng)isten線(xiàn)程、Handler線(xiàn)程、Worker線(xiàn)程,分別完成不同的工作,后續(xù)會(huì)詳細(xì)展開(kāi)。
所以我們可以知道,要完成一個(gè)數(shù)據(jù)交互,涉及了幾大塊內(nèi)容:
- IO事件監(jiān)聽(tīng)
- 數(shù)據(jù)拷貝
- 數(shù)據(jù)處理和計(jì)算
大白認(rèn)為,這三大塊內(nèi)容,不論什么形式的框架都繞不開(kāi),也是理解網(wǎng)絡(luò)架構(gòu)的關(guān)鍵所在。
4. 高性能網(wǎng)絡(luò)框架實(shí)踐
4.1 基于線(xiàn)程模型
在早期并發(fā)數(shù)不多的場(chǎng)景中,有一種One Request One Thread的架構(gòu)模式。該模式下每次接收一個(gè)新請(qǐng)求就創(chuàng)建一個(gè)處理線(xiàn)程,線(xiàn)程雖然消耗資源并不多,但是成千上萬(wàn)請(qǐng)求打過(guò)來(lái),性能也是扛不住的。
這是一種比較原始的架構(gòu),思路也非常清晰,創(chuàng)建多個(gè)線(xiàn)程來(lái)提供處理能力,但在高并發(fā)生產(chǎn)環(huán)境中幾乎沒(méi)有應(yīng)用,本文不再展開(kāi)。
4.2 基于事件驅(qū)動(dòng)模型
當(dāng)前流行的是基于事件驅(qū)動(dòng)的IO復(fù)用模型,相比多線(xiàn)程模型優(yōu)勢(shì)很明顯。

在此我們先理解一下什么是事件驅(qū)動(dòng)Event-Drive-Model。
事件驅(qū)動(dòng)編程是一種編程范式,程序的執(zhí)行流由外部事件來(lái)決定,它的特點(diǎn)是包含一個(gè)事件循環(huán),當(dāng)外部事件發(fā)生時(shí)使用回調(diào)機(jī)制來(lái)觸發(fā)相應(yīng)的處理。
通俗來(lái)說(shuō)就是:有一個(gè)循環(huán)裝置在一直等待各種事件的到來(lái),并將到達(dá)的事件放到隊(duì)列中,再由一個(gè)分揀裝置來(lái)調(diào)用對(duì)應(yīng)的處理裝置來(lái)響應(yīng)。
4.3 Reactor反應(yīng)堆模式
第一次聽(tīng)到這個(gè)模式的時(shí)候很困惑,究竟反應(yīng)堆是個(gè)啥?研究了一下發(fā)現(xiàn),反應(yīng)堆是個(gè)核物理的概念,大致是這個(gè)樣子的:
核反應(yīng)堆是核電站的心臟 ,它的工作原理是這樣的:原子由原子核與核外電子組成,原子核由質(zhì)子與中子組成。
當(dāng)鈾235的原子核受到外來(lái)中子轟擊時(shí),一個(gè)原子核會(huì)吸收一個(gè)中子分裂成兩個(gè)質(zhì)量較小的原子核,同時(shí)放出2-3個(gè)中子。
這裂變產(chǎn)生的中子又去轟擊另外的鈾235原子核,引起新的裂變,如此持續(xù)進(jìn)行就是裂變的鏈?zhǔn)椒磻?yīng)。
結(jié)合這種核裂變的圖,好像是一個(gè)請(qǐng)求打過(guò)來(lái),服務(wù)器內(nèi)部瞬間延伸出很多分支來(lái)完成響應(yīng),一變二,二變四,甚至更多,確實(shí)有種反應(yīng)堆的感覺(jué)。接下來(lái)我們看看究竟反應(yīng)堆模式是如何構(gòu)建高性能網(wǎng)絡(luò)框架的。
5.反應(yīng)堆模式詳解
反應(yīng)堆模式是一種思想,形式卻有很多種。
5.1 反應(yīng)堆模式的本質(zhì)是什么
從本質(zhì)上理解,無(wú)論什么網(wǎng)絡(luò)框架都要完成兩部分操作:
IO操作:數(shù)據(jù)包的讀取和寫(xiě)入
CPU操作:數(shù)據(jù)請(qǐng)求的處理和封裝
所以上述這些問(wèn)題由誰(shuí)來(lái)做以及多少線(xiàn)程來(lái)做,就衍生出了很多形式,所以不要被表面現(xiàn)象迷惑,出現(xiàn)必有原因,追溯之后我們才能真正掌握它。
反應(yīng)堆模式根據(jù)處理IO環(huán)節(jié)和處理數(shù)據(jù)環(huán)節(jié)的數(shù)量差異分為如下幾種:
- 單Reactor線(xiàn)程
- 單Reactor線(xiàn)程和線(xiàn)程池
- 多Reactor線(xiàn)程和線(xiàn)程池
我們來(lái)看看這三種常見(jiàn)模式的特點(diǎn)、原理、優(yōu)缺點(diǎn)、應(yīng)用場(chǎng)景等。
5.2 單Reactor線(xiàn)程模式
這種模式最為簡(jiǎn)潔,一個(gè)線(xiàn)程完成了連接的監(jiān)聽(tīng)、接收新連接、處理連接、讀取數(shù)據(jù)、寫(xiě)入數(shù)據(jù)全套工作。由于只使用了一個(gè)線(xiàn)程,對(duì)于多核利用率偏低,但是編程簡(jiǎn)單。是不是覺(jué)得這個(gè)種單線(xiàn)程的模式?jīng)]有市場(chǎng)?那可未必,不信你看Redis。
在這種模式種IO操作和CPU操作是沒(méi)有分開(kāi)的,都是由1個(gè)線(xiàn)程來(lái)完成的,顯然如果在Handler處理某個(gè)請(qǐng)求超時(shí)了將會(huì)阻塞客戶(hù)端的正常連接。在Redis中由于都是內(nèi)存操作,速度很快,這種瓶頸雖然存在但是不夠明顯。
5.3 單Reactor線(xiàn)程和線(xiàn)程池模式
為了解決IO操作和CPU操作的不匹配,也就是IO操作和CPU操作是在一個(gè)線(xiàn)程內(nèi)部串行執(zhí)行的,這樣就拉低了CPU操作效率。
一種解決方法就是將IO操作和CPU操作分別由單獨(dú)的線(xiàn)程來(lái)完成,各玩各的互不影響。單Reactor線(xiàn)程完成IO操作、復(fù)用工作線(xiàn)程池來(lái)完成CPU操作就是一種解決思路。
在這種模式種由Reactor線(xiàn)程完成連接的管理和數(shù)據(jù)讀取&寫(xiě)回,完全掌管IO操作。工作線(xiàn)程池處理來(lái)自上游分發(fā)的任務(wù),對(duì)其中的數(shù)據(jù)進(jìn)行解碼、計(jì)算、編碼再返回給Reactor線(xiàn)程和客戶(hù)端完成交互。這種模式有效利用了多核,但是單Reactor線(xiàn)程來(lái)完成IO操作在高并發(fā)場(chǎng)景中仍然會(huì)出現(xiàn)瓶頸。換句話(huà)說(shuō),連接實(shí)在太多了,一個(gè)Reactor線(xiàn)程忙不過(guò)來(lái)建立新連接和響應(yīng)舊連接這些事情,因此Reactor線(xiàn)程也需要幾個(gè)幫手。
5.4 多Reactor線(xiàn)程和線(xiàn)程池模式
水平擴(kuò)展往往是提供性能的有效方法。
我們將Reactor線(xiàn)程進(jìn)行擴(kuò)展,一個(gè)Reactor線(xiàn)程負(fù)責(zé)處理新連接,多個(gè)Reactor線(xiàn)程負(fù)責(zé)處理連接成功的IO數(shù)據(jù)讀寫(xiě)。也就是進(jìn)一步將監(jiān)聽(tīng)&創(chuàng)建連接 和 處理連接 分別由兩個(gè)及以上的線(xiàn)程來(lái)完成,進(jìn)一步提高了IO操作部分的效率。
這種模式算是比較高配的版本了,在實(shí)際生產(chǎn)環(huán)境也有使用。
5.5 拓展:同步IO和異步IO
我們可以輕易區(qū)分什么是阻塞IO和非阻塞IO,那么什么是同步IO和異步IO呢?前面提到Reactor模式其中非常重要的一環(huán)就是調(diào)用read/write函數(shù)來(lái)完成數(shù)據(jù)拷貝,這部分是應(yīng)用程序自己完成的,內(nèi)核只負(fù)責(zé)通知監(jiān)控的事件到來(lái)了,所以本質(zhì)上Reactor模式屬于非阻塞同步IO。還有一種Preactor模式,借助于系統(tǒng)本身的異步IO特性,由操作系統(tǒng)進(jìn)行數(shù)據(jù)拷貝,在完成之后來(lái)通知應(yīng)用程序來(lái)取就可以,效率更高一些,但是底層需要借助于內(nèi)核的異步IO機(jī)制來(lái)實(shí)現(xiàn)。
底層的異步IO機(jī)制可能借助于DMA和Zero-Copy技術(shù)來(lái)實(shí)現(xiàn),理論上性能更高。當(dāng)前Windows系統(tǒng)通過(guò)IOCP實(shí)現(xiàn)了真正的異步I/O,而在Linux 系統(tǒng)的異步I/O還不完善,比如Linux中的boost.asio模塊就是異步IO的支持,但是目前Linux系統(tǒng)還是以基于Reactor模式的非阻塞同步IO為主。
6. 小結(jié)
本文從IO事件和IO復(fù)用出發(fā),闡述了網(wǎng)絡(luò)架構(gòu)最底層的組成。繼續(xù)展開(kāi)了基于線(xiàn)程模型和基于事件驅(qū)動(dòng)模型的網(wǎng)絡(luò)框架特點(diǎn)及其設(shè)計(jì)要素。之后重點(diǎn)描述了反應(yīng)堆模式的核心本質(zhì),以及生產(chǎn)環(huán)境中的多種形式。最后簡(jiǎn)單介紹了同步IO和異步IO的區(qū)別,以及Preactor模式的優(yōu)勢(shì)。希望讀者朋友可以摒棄專(zhuān)業(yè)術(shù)語(yǔ)和表述,抓住問(wèn)題的本質(zhì)和重點(diǎn),找到一個(gè)適合自己思維方法去理解和掌握高性能網(wǎng)絡(luò)架構(gòu)的設(shè)計(jì)之道?;蛟S,高性能網(wǎng)絡(luò)框架只是一個(gè)紙老虎。