設計高性能高并發(fā)網(wǎng)絡系統(tǒng)需考慮哪些因素(萬字長文)
“世間可稱之為天經(jīng)地義的事情沒幾樣,復雜的互聯(lián)網(wǎng)架構(gòu)也是如此,萬丈高樓平地起,架構(gòu)都是演變而來,那么演變的本質(zhì)是什么?”
— 1 —
引子
軟件復雜性來源于幾個方面:高并發(fā)高性能、高可用、可擴展、低成本、低規(guī)模、可維護、安全等。架構(gòu)演化、發(fā)展都是為了試圖降低復雜性:
- 高并發(fā)、高性能:互聯(lián)網(wǎng)系統(tǒng)特點,用戶量大,請求量大,高并發(fā)高性能成為必備要求。性能差體驗會差,用戶會有別選擇。
- 高可用:系統(tǒng)高可用可提升用戶體驗,也變?yōu)楸貍湟?。十幾年前我們買股票都需要T+N操作,而現(xiàn)在通過手機可以實時辦理。
- 可擴展、易迭代:在產(chǎn)品初期,采用單體或簡單的架構(gòu)。成熟期,演進為現(xiàn)在大中臺、小前臺的概念,把不變的和變得拆分開來。產(chǎn)品經(jīng)理、架構(gòu)師需避免無限放大需求,面向未來設計,進入尷尬境地。
- 低成本:是個過程。ROI投入產(chǎn)出比越往后越低。
- 低規(guī)模:規(guī)模小,成本肯定低,運維、擴展.... 都將方便。所以簡單、適用、演進架構(gòu)設計原則很重要。
- 易運維:除了傳統(tǒng)運維方面。業(yè)務的快速發(fā)展,灰度發(fā)布、快速發(fā)布回滾、部分功能升級、ab測試等對架構(gòu)層面提出更高要求,也是現(xiàn)在容器化技術這么流行原因之一。
本文主要從如何實現(xiàn)高并發(fā)、高性能系統(tǒng)角度,剖析網(wǎng)絡應用架構(gòu)演進過程中,解決的那些關鍵點,并找到一些規(guī)律。也可指導我們構(gòu)建高并發(fā)、高性能系統(tǒng)時,應該注意哪些環(huán)節(jié)。
- 如何更有效的利用單機資源?開源軟件在高性能、高并發(fā)中做了哪些實踐。
- 如何在高并發(fā)前提下,利用跨機器遠程調(diào)用提升并發(fā)及“性能”。分布式服務如何拆分,怎么拆分才能達到高性能高可用,并不浪費資源?
注:太多的調(diào)用鏈路,性能是有很大損耗的。
... ...
篇幅有限,文章不會鋪開講所有細節(jié)。
— 2 —
從網(wǎng)絡連接開始
瀏覽器/app與后端通信一般使用http、https協(xié)議,底層都是使用TCP(Transmission Control Protocol 傳輸控制協(xié)議),而RPC遠程調(diào)用可直接使用TCP連接。我們從TCP連接開始文章。
- 大家都知道TCP 三次握手建立連接、四次揮手斷開連接,簡述如下:
- 建立連接都是客戶端主動發(fā)起,經(jīng)過三次交替交互后(中間會有狀態(tài)),雙方狀態(tài)都變?yōu)?ESTABLISHED狀態(tài),可以開始雙工數(shù)據(jù)傳送。
斷開連接雙方都可以主動發(fā)起, 分別發(fā)起、回復一共四次交互(中間會有狀態(tài)),關閉連接。
注:詳細細節(jié)請參閱相關文檔,Windows和Linux服務器都可以使用netstat -an命令查看。
網(wǎng)絡編程中,關于連接這塊我們一般會關注以下指標:
1、連接相關
服務端能保持,管理,處理多少客戶端的連接。
- 活躍連接數(shù):所有ESTABLISHED狀態(tài)的TCP連接,某個瞬時,這些連接正在傳輸數(shù)據(jù)。如果您采用的是長連接的情況,一個連接會同時傳輸多個請求。也可以間接考察后端服務并發(fā)處理能力,注意不同于并發(fā)量。
- 非活躍連接數(shù):表示除ESTABLISHED狀態(tài)的其它所有狀態(tài)的TCP連接數(shù)。
- 并發(fā)連接數(shù):所有建立的TCP連接數(shù)量。=活躍連接數(shù)+非活躍連接數(shù)。
- 新建連接數(shù):在統(tǒng)計周期內(nèi),從客戶端連接到服務器端,新建立的連接請求的平均數(shù)。主要考察應對 突發(fā)流量或從正常到高峰流量的能力。如:秒殺、搶票場景。
- 丟棄連接數(shù):每秒丟棄的連接數(shù)。如果連接服務器做了連接熔斷處理,這部分數(shù)據(jù)即熔斷的連接。
關于tcp連接數(shù)量,在linux下,跟文件句柄描述項有關,可以ulimit -n查看,也可修改。其它就是跟硬件資源cpu、內(nèi)存、網(wǎng)絡帶寬有關。單機可以做到數(shù)十萬級的并發(fā)連接數(shù),如何實現(xiàn)呢?后面IO模型時講解。
2、流量相關
主要是網(wǎng)絡帶寬的配置。
- 流入流量:從外部訪問服務器所消耗的流量。
- 流出流量:服務器對外響應的流量。
3、數(shù)據(jù)包數(shù)
數(shù)據(jù)包是TCP三次握手建立連接后,傳輸?shù)膬?nèi)容封裝
- 流入數(shù)據(jù)包數(shù):服務器每秒接到的請求數(shù)據(jù)包數(shù)量。
- 流出數(shù)據(jù)包數(shù):服務器每秒發(fā)出的數(shù)據(jù)包數(shù)量。
關于TCP/IP包的細節(jié)請查閱相關文檔。但是有一點一定注意,我們單次請求可能會分成多個包發(fā)送,拆包、粘包問題網(wǎng)絡中間件都會為我們處理(比如消息補齊、回車結(jié)尾、自定義消息頭體、自定義協(xié)議等解決方案)。如果我們傳遞的用戶數(shù)據(jù)較小,那么效率肯定會提升。反過來無限制的壓縮傳輸包的大小,解壓也會耗費cpu資源,需平衡處理。
4、應用傳輸協(xié)議
傳輸協(xié)議壓縮率好,傳輸性能好,對并發(fā)性能提升高。但是也需要看調(diào)用雙方的語言可以使用協(xié)議才行??梢宰约憾x,也可以使用成熟的傳輸協(xié)議。比如redis的序列化傳輸協(xié)議、json傳輸協(xié)議、Protocol Buffers傳輸協(xié)議、http協(xié)議等。 尤其在 rpc調(diào)用過程中,這個傳輸協(xié)議選擇需要仔細甄別選型。
5、長、短連接
- 長連接是指在一個TCP連接上,可以重用多次發(fā)送數(shù)據(jù)包,在TCP連接保持期間,如果沒有數(shù)據(jù)包發(fā)送,需要雙方發(fā)檢測包以維持此連接。
- 半開連接的處理:當客戶端與服務器建立起正常的TCP連接后,如果客戶主機掉線(網(wǎng)線斷開)、電源掉電、或系統(tǒng)崩潰,服務器將永遠不會知道。長連接中間件,需要處理這個細節(jié)。linux默認配置2小時,可以配置修改。
- 短連接是指通信雙方有數(shù)據(jù)交互時,就建立一個TCP連接,數(shù)據(jù)發(fā)送完成后,則斷開此TCP連接。但是每次建立連接需要三次握手、斷開連接需要四次揮手。
- 關閉連接最好由客戶端主動發(fā)起,TIME_WAIT這個狀態(tài)最好不要在服務器端,減少占用資源。
選擇建議:
在客戶端數(shù)量少場景一般使用長連接。后端中間件、微服務之間通信最好使用長連接。如:數(shù)據(jù)庫連接,duboo默認協(xié)議等。
而大型web、app應用,使用http短連接(http1.1的keep alive變相的支持長連接,但還是串行請求/響應交互)。http2.0支持真正的長連接。
長連接會對服務端耗費更多的資源,上百萬用戶,每個用戶獨占一個連接,對服務端壓力多大,成本多高。IM、push應用會使用長連接,但是會做很多優(yōu)化工作。
由于https需要加解密運算等,最好使用http2.0(強制ssl),傳輸性能很好。但是服務端需要維持更多的連接。
6、關于并發(fā)連接與并發(fā)量
- 并發(fā)連接數(shù):=活躍連接數(shù)+非活躍連接數(shù)。所有建立的TCP連接數(shù)量。網(wǎng)絡服務器能并行管理的連接數(shù)。
- 活躍連接數(shù):所有ESTABLISHED狀態(tài)的TCP連接。
- 并發(fā)量:瞬時通過活躍連接傳輸數(shù)據(jù)的量,這個量一般在處理端好評估。跟活躍連接數(shù)沒有絕對的關系。網(wǎng)絡服務器能并行處理的業(yè)務請求數(shù)。
- rt響應時間:各類操作單機rt肯定不相同。比如:從cache中讀數(shù)據(jù)和分布式事務寫數(shù)據(jù)庫,資源的消耗不同,操作時間本身就不同。
- 吞吐量:QPS/TPS,每秒可以處理的查詢或事務數(shù),這個是關鍵指標。
從系統(tǒng)整體層面、各個服務個體、服務中某個方法都需綜合考慮。
舉例如下:
- 打開商品詳情頁操作,需要動靜分離。后續(xù)一連串的動態(tài)服務、cache機制,整體rt本身會短,單機可以支持的qps較高。(服務間、方法間也有差別)
- 而提交訂單操作需要分布式事務、分布式鎖等,rt本身會長,單機可支持的qps較低。
- 那是否我們就會針對訂單提交的服務部署更多機器呢?答案是不一定。因為用戶瀏覽商品的頻度會很高,而提交訂單的頻度很低。如何正確的評估呢?
- 需要服務分類:關鍵服務/非關鍵服務、高峰各服務的qps需求,來均衡考慮。
系統(tǒng)整體吞吐量、RT響應時間、支持并發(fā)數(shù) 是由小的操作、微服務組成的,各個微服務、操作也需要分別評估。平衡組合后,形成系統(tǒng)整體的各項指標。
7、小節(jié)
首先看一個典型的互聯(lián)網(wǎng)服務端處理網(wǎng)絡請求的典型過程:
注:另外關于用戶態(tài)、內(nèi)核態(tài)數(shù)據(jù)轉(zhuǎn)換,有些特殊場景中,中間件如kafka可以使用zero copy技術,避免兩態(tài)切換開銷。
a、(1,2,3 )三個步驟表示客戶端網(wǎng)絡請求,建立連接(管理連接),發(fā)送請求,服務器接收請求數(shù)據(jù)。
b、(4)構(gòu)建響應,在用戶空間處理客戶端的請求,構(gòu)建響應完成。
c、(5,6,7) 服務器把響應,通過a中fd連接,send發(fā)送響應客戶端。
可以把上面分為兩個關鍵點:
- a和c 服務器如何管理網(wǎng)絡連接,從客戶端獲得輸入數(shù)據(jù),為客戶端響應數(shù)據(jù)。
- b服務器如處理請求。
網(wǎng)絡應用應該考慮平衡a+c和b,處理這些連接的能力 與 能管理的連接請求達到平衡。
比如:有個應用并發(fā)連接數(shù)十萬;而這些連接大約每秒請求2萬次;需要管理10萬連接,每秒處理2萬請求能能力,才能達到平衡。如何達到處理高qps呢,兩個方向:
- 單機優(yōu)化(見后中間件例子)
- 轉(zhuǎn)發(fā)到別多臺機器處理(遠程調(diào)用)
注:一般系統(tǒng)管理連接能力遠遠大于處理能力。
如上圖,客戶端的請求會形成一個大隊列;服務器會處理這個大隊列中的任務。這個隊列能有多大,看連接管理能力;如何保證進入隊列任務的速率和處理移除任務的速度平衡,是關鍵。達到平衡是目的。
— 3 —
網(wǎng)絡編程中常用IO模型
客戶端與服務器的交互都會產(chǎn)生個連接,linux中在服務器端由文件描述項 fd、socket編程中socket連接、java語言api中channel等體現(xiàn)。而IO模型,可以理解為管理fd,并通過fd從客戶端read獲取數(shù)據(jù)(客戶端請求)和通過fd往客戶端write數(shù)據(jù)(響應客戶端)的機制。
關于同步,異步、阻塞、非阻塞 IO操作,網(wǎng)上、書籍上描述都不相同,也找不到準確描述。我們按照《UNIX網(wǎng)絡編程:卷一》第六章——I/O復用為標準。書中向我們提及了5種類UNIX下可用的I/O模型:阻塞式I/O、非阻塞式I/O、I/O復用(selece,poll,epoll)、信號驅(qū)動式I/O、異步I/O。(詳細可以查閱相關書籍資料)
1、阻塞式I/O:進程會卡在recvfrom的調(diào)用,等到最終結(jié)果數(shù)據(jù)返回??隙▽儆谕健?/p>
2、非阻塞式I/O:進程反復輪訓調(diào)用recvfrom,直到最終結(jié)果數(shù)據(jù)返回。也是同步調(diào)用,但是IO內(nèi)核處理時非阻塞的。沒什么實用意義,不討論應用。
3、I/O復用也屬于同步:進程卡在select、epoll調(diào)用上,不會卡在recvfrom上,直到最終結(jié)果返回。
注:select 模型:把要管理的fd放到一個數(shù)據(jù)里,循環(huán)這個數(shù)據(jù)。數(shù)組大小1024,可管理連接有限。poll 與select類似,只是把數(shù)組類型改為鏈表,沒有1024大小限制。
而epoll 為 event poll,只會管理有事件發(fā)生的 fd,也就是只會處理活躍的連接。epoll通過內(nèi)核和用戶空間共享一塊mmap()文件映射內(nèi)存來實現(xiàn)的消息傳遞。參考 http://libevent.org/
4、信號驅(qū)動式I/O:也是同步。只有unix實現(xiàn),不討論。
5、異步:只有異步I/O屬于異步。底層操作系統(tǒng)只有window實現(xiàn),不討論。nodejs中間件通過回調(diào)實現(xiàn),java AIO也有實現(xiàn)。開發(fā)難度較大。
IO模型中同步/異步、阻塞/非阻塞的差別(好繞):
- 同步異步:訪問數(shù)據(jù)的方式,同步需主動讀寫數(shù)據(jù),要求被調(diào)用方IO返回最終的結(jié)果。而異步發(fā)出請求后,只需等待IO操作完成的通知,并不主動讀寫數(shù)據(jù),由系統(tǒng)內(nèi)核完成;
- 而阻塞和非租塞的區(qū)別在于,進程或線程要訪問的數(shù)據(jù)是否就緒,進程或線程是否需要等待;等待就是阻塞,不需要等待就是非阻塞。
而我們平時在編程、函數(shù)接口調(diào)用過程中,除了超時以外,都會返回一個結(jié)果。同步異步調(diào)用按照以下區(qū)分:
- 如果返回的結(jié)果是最終結(jié)果,就是同步調(diào)用,如:調(diào)用數(shù)據(jù)查詢sql。
- 如果返回的結(jié)果是個中間通知,那么是異步:如:發(fā)送消息給mq,只會返回ack信息。對于發(fā)消息來說,是同步;如果從系統(tǒng)架構(gòu)層面看,算異步,因為處理結(jié)果由消息消費者來處理產(chǎn)生。如果發(fā)送成功,但是突然斷網(wǎng)沒有收到ack,這是屬于故障,不在討論范圍內(nèi)。
- 同步調(diào)用,參數(shù)中可以傳遞一個回調(diào)函數(shù)的方式:需要語言或中間件引擎執(zhí)行。如jvm支持,node v8引擎支持。(需要回調(diào)函數(shù)的執(zhí)行,跟調(diào)用端在一個context內(nèi),共享棧變量等)
注:select關鍵字可別混淆!!!IO多路復用從技術實現(xiàn)上有多種:select、poll、epoll 詳細自己參閱資料,幾乎所有中間件都會使用epoll模式。另外由于各個操作系統(tǒng)對多路復用實現(xiàn)機制不同,epoll、kqueue、IOCP接口都有自己的特點,第三方庫的封裝了這些差異,提供統(tǒng)一的API,如Libevent。另外如java語言,netty提供更高層面的封裝,javaNIO和netty使用保留了select方法,也引起一些混淆。
小節(jié):現(xiàn)在網(wǎng)絡中間件都是用 阻塞IO和IO多路復用這兩個模型來管理連接,通過網(wǎng)絡IO獲取數(shù)據(jù)。下節(jié)講解,使用IO模型的一些中間件案例。
— 4 —
同步阻塞IO模型的具體實現(xiàn)模型-PPC,TPC
服務器處理數(shù)據(jù)問題,從純網(wǎng)絡編程技術角度看,主要思路有兩個:
- 一個是對于每個連接處理分配一個獨立的進程/線程,直到處理完成。PPC,TPC模式;
- 另一個思路是用同一進程/線程來同時處理若干連接,處理連接中數(shù)據(jù),通過多線程、多進程技術。Reactor模式;
每個進程/線程處理一個連接,叫PPC或TPC。PPC是Process Per Connection TPC是Thread Per Conection ,傳統(tǒng)阻塞IO模型實現(xiàn)的網(wǎng)絡服務器采用這種模式。
注:close特指主進程對連接的計數(shù),連接實際在子進程中關閉。而多線程實現(xiàn)中,主線程不需要close操作,因為父子線程共享存儲。如:java中jmm
注:pre模式,預先創(chuàng)建線程和進行,連接進來,分配到預先創(chuàng)建好的線程或進程。多進程時有驚群現(xiàn)象。
申請線程或進程會占用很多系統(tǒng)資源,操作系統(tǒng)cpu、內(nèi)存有限度,能同時管理的線程有限,處理連接的線程不能太多。雖然可以提前建立好進程或線程來處理數(shù)據(jù)(prefork/prethead)或通過線程池來減少線程建立壓力。但是線程池的大小是個天花板。另外父子進程通信也比較復雜。
apache MPM prefork(ppc),可支持256的并發(fā)連接,tomcat 同步IO(tpc)采用阻塞IO方式工作,可支持500個并發(fā)連接。java可以創(chuàng)建線程池來降低一定創(chuàng)建線程資源開銷來處理。
網(wǎng)絡連接fd可以支持上萬個,但是每個線程需要占有系統(tǒng)內(nèi)存,線程同時存在的總數(shù)有限。linux下用命令ulimit -s可以查看棧內(nèi)存分配。線程多了對cup的資源調(diào)度開銷。失衡情況發(fā)生,如何解決呢?
小節(jié):ppc、tpc瓶頸是能夠管理的連接數(shù)少。本來多線程處理業(yè)務能力夠,這下與fd綁定了,線程生命周期與fd一樣了,限定了線程處理能力。拆分:把fd生命周期與線程的生命周期拆分開來。
— 5 —
IO模型的具體實現(xiàn)模型-Reactor
每個進程/線程同時處理多個連接(IO多路復用),多個連接共用一個阻塞對象,應用程序只需要在一個阻塞對象上等待,無需阻塞等待所有連接。當某條連接有新的數(shù)據(jù)可以處理時,操作系統(tǒng)通知應用程序,線程從阻塞狀態(tài)返回(還有更好優(yōu)化,見下小節(jié)),開始進行業(yè)務處理;就是Reactor模式思想。
Reactor 模式,是指通過一個或多個輸入同時傳遞給服務處理器的服務請求的事件驅(qū)動處理模式。服務端程序處理客戶端傳入的多路請求,并將它們同步分派給請求對應的處理線程,Reactor 模式也叫 Dispatcher 模式。即 I/O 多了復用統(tǒng)一監(jiān)聽事件,收到事件后分發(fā)(Dispatch 給某進程),是編寫高性能網(wǎng)絡服務器的必備技術之一。很多優(yōu)秀的網(wǎng)絡中間件都是基于該思想的實現(xiàn)。
注:由于epoll比select管理的連接數(shù)大了好多,libevent,netty等框架中底層實現(xiàn)都是epoll方式,但是編程API保留了select關鍵字。所以文章中epoll_wait跟select等同。
Reactor模式有幾個關鍵的組成:
- Reactor:Reactor在一個單獨的線程運行,負責監(jiān)聽fd事件,分發(fā)給適當?shù)奶幚沓绦驅(qū)O事件做出反應。建立連接事件分發(fā)給Acceptor;分發(fā)read/write處理事件給Handler。
- Acceptor:負責處理建立連接事件,并建立對應的Handler對象。
- Handlers:負責處理read和write事件。從fd中獲取請求數(shù)據(jù);處理數(shù)據(jù)得到相應數(shù)據(jù);send相應數(shù)據(jù)。處理程序執(zhí)行IO事件要完成的實際事情。
對于IO密集型(IO bound)場景,可以使用Reactor場景,但是ThreadLocal將不能使用。開發(fā)調(diào)試難度較大,一般不建議自己實現(xiàn),使用現(xiàn)有框架即可。
小節(jié):Reactor解決可管理的網(wǎng)絡連接數(shù)量提升到幾十萬。但是如此多連接上請求任務,還是需要通過多線程、多進程機制處理。甚至負載轉(zhuǎn)發(fā)到其它服務器處理。
— 6 —
Reactor模式實踐案例(C語言)
通過幾個開源框架的例子,了解不同場景下的網(wǎng)絡框架,是如何使用Reactor模式,做了哪些細節(jié)調(diào)整。
注:實際實現(xiàn)肯定與圖差別很大??蛻舳薸o及send比較簡單,圖中省略。
A、單Reactor+單線程處理(整體一個線程)redis為代表
如圖所示:
- 客戶端請求->Reactor對象接受請求,并通過select(epoll_wait)監(jiān)聽請求事件->通過dispatch分發(fā)事件;
- 如果是連接請求事件->dispatch->Acceptor(accept建立連接)->為這個連接創(chuàng)建一個Handler 對象等待后續(xù)業(yè)務處理。
- 如果不是建立連接事件->dispatch分發(fā)事件->觸發(fā)到為這個連接創(chuàng)建的那個Handler對象(read、業(yè)務處理、send),形成一個任務/命令隊列。
- Handler對象完成read->業(yè)務處理->send整體流程。
把請求轉(zhuǎn)化為命令隊列,單進程處理。注意圖中 隊列,單線程處理,是沒有競爭的。
優(yōu)點:
模型簡單。這個模型是最簡單的,代碼實現(xiàn)方便,適合計算密集型應用
不用考慮并發(fā)問題。模型本身是單線程的,使得服務的主邏輯也是單線程的,那么就不用考慮許多并發(fā)的問題,比如鎖和同步
適合短耗時服務。對于像redis這種每個事件基本都是查內(nèi)存,是十分適合的,一來并發(fā)量可以接受,二來redis內(nèi)部眾多數(shù)據(jù)結(jié)構(gòu)都是非常簡單地實現(xiàn)
缺點:
性能問題,只有一個線程,無法完全發(fā)揮多核 CPU 的性能。
順序執(zhí)行影響后續(xù)事件。因為所有處理都是順序執(zhí)行的,所以如果面對長耗時的事件,會延遲后續(xù)的所有任務,特別對于io密集型的應用,是無法承受的
這也是為什么redis禁止大家使用耗時命令
注:redis是自己實現(xiàn)的io多路復用,沒有使用libevent,實現(xiàn)與圖不符,更加輕巧。
這種模型對于處理讀寫事件操作很短很短時間內(nèi)執(zhí)行完。大約可達到10萬QPS吞吐量(redis各種命令差別很大)。
注:redis發(fā)布版本中自帶了redis-benchmark性能測試工具,可以使用它計算qps。示例:使用50個并發(fā)連接,發(fā)出100000個請求,每個請求的數(shù)據(jù)為2kb,測試host為127.0.0.1端口為6379的redis服務器性能:./redis-benchmark -h127.0.0.1 -p 6379 -c 50 -n 100000 -d 2
對于客戶端數(shù)量多的網(wǎng)絡系統(tǒng),強調(diào)多客戶端,也就是并發(fā)連接數(shù)。 對于后端連接數(shù)少的的網(wǎng)絡系統(tǒng),采用長連接,并發(fā)連接數(shù)少,但是每個連接發(fā)起的請求數(shù)多。
B、單 Reactor+單隊列+業(yè)務線程池
如圖所示,我們按把真正的業(yè)務處理從 Reactor線程中剝離出來,通過業(yè)務線程池來實現(xiàn)。那么Reactor中每個fd的Handler對象如何與 Worker線程池通信的,通過待處理請求隊列 ??蛻舳藢Ψ掌鞯恼埱?,本來可以想象成一個請求隊列IO, 這里經(jīng)過Reactor(多路復用)處理后,(拆分)轉(zhuǎn)化為一個待處理工作任務的隊列。
注:處處是拆分啊!
業(yè)務線程池線程池分配獨立的線程池,從隊列中拿到數(shù)據(jù)進行真正的業(yè)務處理,將結(jié)果返回Handler。Handler收到響應結(jié)果后,send結(jié)果給客戶端。
與A模型相比,利用線程池技術加快了客戶端請求處理能力。例如:thrift0.10.0版本中 nonblocking server 采用這種模型,能達到幾萬級別的QPS。
缺點:這種模型的缺點就在于這個隊列上,是性能瓶頸。線程池從隊列獲取任務需要加鎖,會采用高性能的讀寫鎖實現(xiàn)隊列。
C、單 Reactor+N隊列+N線程
這種模型是 A和B的變種模型,memcached采用這種模型。待處理工作隊列分為多個,每個隊列綁定一個線程來處理,這樣最大的發(fā)揮了IO多路復用對網(wǎng)絡連接的管理,把單隊列引起的瓶頸得到釋放。QPS估計可達到20萬。
但是這種方案有個很大的缺點,負載均衡可能導致有些隊列忙,有些空閑。好在memcached 也是內(nèi)存的操作,對負載問題不是很敏感,可以使用該模型。
D、單進程Reactor監(jiān)聽+N進程(accept+epoll_wait+處理)模型
流程:
master(Reactor主進程)進程監(jiān)聽新連接的到來,并讓其中一個worker進程accept。這里需要處理驚群效應問題,詳見nginx的accept_mutex設計
worker(subReactor進程)進程accept到fd之后,把fd注冊到到本進程的epoll句柄里面,由本進程處理這個fd的后續(xù)讀寫事件
worker進程根據(jù)自身負載情況,選擇性地不去accept新fd,從而實現(xiàn)負載均衡
優(yōu)點:
進程掛掉不會影響這個服務
是由worker主動實現(xiàn)負載均衡的,這種負載均衡方式比由master來處理更簡單
缺點:
多進程模型編程比較復雜,進程間同步?jīng)]有線程那么簡單
進程的開銷比線程更多
nginx使用這種模型,由于nginx主要提供反向代理與靜態(tài)內(nèi)容web服務功能,qps指標與被nginx代理的處理服務器有關系。
注:nodejs多進程部署方式與nginx方式類似。
小節(jié):期望從這幾個 Reactor的實例中,找到拆分解決了哪些問題,引起的哪些問題。
— 7 —
Reactor模式實踐案例(Java語言Netty)
Netty是 一個異步事件驅(qū)動的網(wǎng)絡應用程序框架,用于快速開發(fā)可維護的高性能協(xié)議服務器和客戶端,java語言的很多開源網(wǎng)絡中間件使用了netty,本文只描述針對NIO多路復用相關部分,很多拆包粘包、定時任務心跳監(jiān)測、序列化鉤子等等可參閱資料。如圖所示:
netty可以通過配置,來實現(xiàn)各個模塊在哪個線程(池)中運行:
1、單Reactor單線程
- EventLoopGroup bossGroup = new NioEventLoopGroup(1);//netty默認只會單Reactor
- EventLoopGroup workerGroup = bossGroup ;//監(jiān)聽線程和工作線程使用一個
- ServerBootstrap server = new ServerBootstrap();
- server.group(bossGroup, workerGroup);
2、單Reactor多線程subReactor
- EventLoopGroup bossGroup = new NioEventLoopGroup(1);
- EventLoopGroup workerGroup = new NioEventLoopGroup();//默認cup核心*2
- ServerBootstrap server = new ServerBootstrap();
- server.group(bossGroup, workerGroup);//主線程和工作線程分開
3、單Reactor、多線程subReactor、指定線程池處理業(yè)務
https://netty.io/4.1/api/io/netty/channel/ChannelPipeline.html
我們在一個pipeline中定義多個ChannelHandler,用以接收I / O事件(例如,讀取)和請求I / O操作(例如,寫入和關閉)。例如,典型的服務器在每channel的pipiline中,都有以下Handler:(具體取決于使用的協(xié)議和業(yè)務邏輯的復雜性和特征):
- Protocol Decoder - 將二進制數(shù)據(jù)(例如ByteBuf)轉(zhuǎn)換為Java對象。
- Protocol Encoder - 將Java對象轉(zhuǎn)換為二進制數(shù)據(jù)。
- Business Logic Handler - 執(zhí)行實際的業(yè)務邏輯(例如數(shù)據(jù)庫訪問)。
如下例所示:
- static final EventExecutorGroupgroup = new DefaultEventExecutorGroup(16);
- ...
- ChannelPipeline pipeline = ch.pipeline();
- pipeline.addLast(“decoder”,new MyProtocolDecoder());
- pipeline.addLast(“encoder”,new MyProtocolEncoder());
- //告訴這個MyBusinessLogicHandler的事件處理程序方法不在I / O線程中,
- //以便I / O線程不被阻塞,一項耗時的任務運行在自定義線程組(池)
- //如果您的業(yè)務邏輯完全異步或很快完成,則不需要額外指定一個線程組。
- pipeline.addLast(group,“handler”,new MyBusinessLogicHandler());
前文中提到過,web應用程序接受百萬、千萬的網(wǎng)絡連接,并管理轉(zhuǎn)化為請求、響應,就像一個大隊列一樣,如何更好的處理隊列里面的任務,牽扯到負載均衡分配、鎖、阻塞、線程池、多進程、轉(zhuǎn)發(fā)、同步異步等一系列負載問題。 單機及分布式都要優(yōu)化,netty做了很多優(yōu)化,這部分netty源碼不好讀懂:
業(yè)務處理與IO任務公用線程池
自定義線程池處理業(yè)務
如圖所示:netty中, 不固定數(shù)量的channel、固定的NioEventLoop、可外置線程池的EventExecutor,在眾多channel不定時的事件驅(qū)動下,如何協(xié)調(diào)線程很是復雜。
留個問題:基于netty的spring webflux 、nodejs,為什么能支撐大量連接,而cpu成為瓶頸?
小節(jié):這樣我們從 客戶端發(fā)起請求->到服務端建立連接->服務端非阻塞監(jiān)聽傳輸->業(yè)務處理->響應 整個流程,通過IO多路復用、線程池、業(yè)務線程池 讓整個處理鏈條沒有處理瓶頸、處理短板,達到整體高性能、高吞吐。
但是耗時處理能力遠遠低于IO連接的管理能力,單機都會達到天花板,繼續(xù)拆分(專業(yè)中間件干專業(yè)事),RPC、微服務調(diào)用是解決策略。
— 8 —
分布式遠程調(diào)用(不是結(jié)尾才是開始)
由前文看出,單機的最終瓶頸會出在業(yè)務處理上。對java語言來說,線程數(shù)量不可能無限擴大。就算使用go語言更小開銷的協(xié)程,cpu也會成為單機瓶頸。所以跨機器的分布式遠程調(diào)用肯定是解決問題的方向。業(yè)內(nèi)已經(jīng)有很多實踐,我們從三個典型架構(gòu)圖,看看演進解決的問題是什么,靠什么解決的:
注:本文不從soa,rpc,微服務等方面討論,只關注拆分的依據(jù)和目標。
A、單體應用
B、把網(wǎng)絡連接管理和靜態(tài)內(nèi)容拆分
C、業(yè)務功能性拆分
A:典型單體應用。
A->B:連接管理與業(yè)務處理拆分。使用網(wǎng)絡連接管理能力強大的nginx,業(yè)務處理單獨拆分為多臺機器。
B->C:業(yè)務處理從功能角度拆分。有些業(yè)務側(cè)重協(xié)議解析、有些側(cè)重業(yè)務判斷、有些側(cè)重數(shù)據(jù)庫操作,繼續(xù)拆分。
通過圖C,從高性能角度,看服務分層(各層技術選型也有很多)的準則及需要注意點:
1、反向代理層(關聯(lián)https連接)
- 可以通過nginx集群實現(xiàn),也可以通過lvs,f5實現(xiàn)。
- 通過上層nginx實現(xiàn),可以知道該層應對的是大量http或https請求。
- 核心指標是:并發(fā)連接數(shù)、活躍連接數(shù)、出入流量、出入包數(shù)、吞吐量等。
- 內(nèi)部關于協(xié)議解析模塊、壓縮模塊、包處理模塊優(yōu)化等。關鍵方向代理出去的請求吞吐量,也就是nginx轉(zhuǎn)發(fā)到后端應用服務器的處理能力,決定整體吞吐量。
- 靜態(tài)文件都走cdn。
- 關于https認證比較費時,建議使用http2.0,或保持連接時間長點。但這也與業(yè)務情況有關。如:每個app與后端交互是否頻繁。畢竟維護太多連接,成本也很高,影響多路復用性能。
2、網(wǎng)關層(通用無業(yè)務的操作)
反向代理層通過http協(xié)議連接網(wǎng)關層,二者之間通過內(nèi)網(wǎng)ip通信,效率高很多。我們假定網(wǎng)關層往下游都使用tcp長連接,java語言中dobbo等rpc框架都可以實現(xiàn)。
網(wǎng)關層主要做幾個事情:
- 鑒權(quán)
- 數(shù)據(jù)包完整性檢查
- http json 傳輸協(xié)議轉(zhuǎn)化為java對象
- 路由轉(zhuǎn)義(轉(zhuǎn)化為微服務調(diào)用)
- 服務治理相關(限流、降級、熔斷等)功能
- 負載均衡
網(wǎng)關層可以由:有開源的Zuul,spring cloud gateway,nodejs等實現(xiàn)。nginx也可以做網(wǎng)關需要定制開發(fā),與反向代理層物理上合并。
3、業(yè)務邏輯層(業(yè)務層面的操作)
從這層可以考慮按照業(yè)務邏輯垂直分層。例如:用戶邏輯層、訂單邏輯層等。如果這樣拆分,可能會抽象一層通過的業(yè)務邏輯層。我們盡量保證業(yè)務邏輯層不橫向調(diào)用,只上游調(diào)用下游。
- 業(yè)務邏輯判斷
- 業(yè)務邏輯處理(組合)
- 分布式事務實現(xiàn)
- 分布式鎖實現(xiàn)
- 業(yè)務緩存
4、數(shù)據(jù)訪問層(數(shù)據(jù)庫存儲相關的操作)
- 專注數(shù)據(jù)增刪改查操作。
- orm封裝
- 隱藏分庫分表的細節(jié)。
- 緩存設計
- 屏蔽存儲層差異
- 數(shù)據(jù)存儲冪等實現(xiàn)
注:本節(jié)引用了孫玄老師《百萬年薪架構(gòu)師課程》中一些觀點,推薦一下這門課,從架構(gòu)實踐、微服務實現(xiàn)、服務治理等方面,從本質(zhì)到實戰(zhàn)面面俱到。
網(wǎng)關層以下,數(shù)據(jù)庫以上,RPC中間件技術選型及技術指標如下(來源dubbo官網(wǎng)):
- 核心指標是:并發(fā)量、TQps、Rt響應時間。
- 選擇協(xié)議因素:dubbo、rmi、hesssion、webservice、thrift、memached、redis、rest
- 連接個數(shù):長連接一般單個;短連接需要多個
- 是否長連接:長短連接
- 傳輸協(xié)議:TCP、http
- 傳輸方式::同步、NIO非阻塞
- 序列化:二進制(hessian)
- 使用范圍:大文件、超大字符串、短字符串等
- 根據(jù)應用場景選擇,一般默認dubbo即可。
小節(jié):
單機時代:從每個線程管理一個網(wǎng)絡連接;再到通過io多路復用,單個線程管理網(wǎng)絡連接,騰出資源處理業(yè)務;再到io線程池和業(yè)務線程池分離;大家能發(fā)現(xiàn)個規(guī)律,客戶端連接請求是總起點->后端處理能力逐步平衡加強的過程。業(yè)務處理能力總是趕不上接受處理的能力。
反向代理時代:nginx能夠管理的連接足夠的多了,后端可以轉(zhuǎn)發(fā)到N臺應用服務器tomcat。從某種程度上,更加有效的利用的資源,通過硬件、軟件選型,把 管理連接(功能)和處理連接(功能)物理上拆分開,軟件和硬件配合處理自己更擅長的事情。
SOA、微服務時代:(SOA的出現(xiàn)其實是為了低耦合,跟高性能高并發(fā)關系不大)業(yè)務處理有很多種類型。有的是運算密集型;有的需要操作數(shù)據(jù)庫;有的只需從cache讀一些數(shù)據(jù);有些業(yè)務使用率很高;有些使用頻度很低。為了更好利用又有了兩種拆分機制。把操作數(shù)據(jù)庫的服務單獨拆出來(數(shù)據(jù)訪問層),把業(yè)務邏輯處理的拆分出來(業(yè)務邏輯層);按照以上邏輯推斷:可能一臺nginx+3臺tomcat網(wǎng)關+5臺duboo業(yè)務邏輯+10臺duboo數(shù)據(jù)訪問配置合適。 我們配置的目的是,各層處理的專屬的業(yè)務都能把服務器壓到60%資源占用。
注:文章只關注了功能層面的水平分層。而垂直層面也需要分層。例如:用戶管理和訂單管理是兩類不同的業(yè)務,業(yè)務技術特點、訪問頻次也不同。 存儲層面也需要垂直分庫、分表。 本文暫且略過。
單機階段,多線程多進程其實相當于一種垂直并發(fā)拆分,盡量保證無狀態(tài),盡量避免鎖等,跟微服務無狀態(tài)、分布式鎖原理上是一致的。
— 9 —
總結(jié)
回顧前文,客戶端連接到服務器端后都要干什么呢?性能瓶頸是維護這么多連接?還是針對每個連接的處理達不到要求失衡?如何破局?從單機內(nèi)部、再到物理機器拆分的描述看來,有三點及其重要:
關注平衡:達到平衡的架構(gòu),才可能是高性能、高并發(fā)架構(gòu)。任何性能問題都會由某個點引起。甚至泛指業(yè)務需求與復雜度也要平衡。
拆分之道:合適的事情,讓合適的技術、合適的中間件解決。具體:如何橫向、縱向拆分還需分析場景。
了解業(yè)務場景、問題本質(zhì)&&了解常用場景下解決方案: 按照發(fā)現(xiàn)問題、分析問題、解決問題思路來看,我們把彈藥庫備齊,解決問題的過程,就是個匹配的過程。
除了文中提到的技術以及拆分方案,很多技術點,都可以提升吞吐及性能,列舉如下:
- IO多路復用:管理更多的連接
- 線程池技術:挖掘多核cpu的潛力
- zero-copy:減少用戶態(tài)和內(nèi)核態(tài)交互次數(shù)。如java中transferTo,linux中sendfile系統(tǒng)接口;
- 磁盤順序?qū)懀航档蛯ぶ烽_銷。消息隊列或數(shù)據(jù)庫日志,都會采用此技術。
- 壓縮更好的協(xié)議:網(wǎng)絡傳輸上減少開支,如:自定義或二進制傳輸協(xié)議;
- 分區(qū):在存儲系統(tǒng)中,分庫分表都算分區(qū);而微服務中,設計服務無狀態(tài),本身也可以理解為分區(qū)。
- 批量傳輸:典型數(shù)據(jù)庫 batch技術。很多網(wǎng)絡中間件也可以使用,如消息隊列中。
- 索引技術:這里不是特指數(shù)據(jù)庫的索引技術。而是我們設計切合業(yè)務場景的索引,提供效率。例如:kafka針對文件的存儲,采用一些hack的索引技巧。
- 緩存設計:當數(shù)據(jù)生命修改不頻繁、變更規(guī)律性很強、生成一次成本太高時,可以考慮緩存
- 空間換時間:其實分區(qū)、索引技術、緩存技術都可歸為這類。例如:我們使用倒排索引存儲數(shù)據(jù)、使用多份數(shù)據(jù)多份節(jié)點提供服務等。
- 網(wǎng)絡連接的選型:長短連接,可靠、非可靠協(xié)議等。
- 拆包粘包:batch、協(xié)議選型于此有些關系。
- 高性能分布式鎖:并發(fā)編程中,鎖不可避免。盡量使用高性能的分布式鎖,能cas樂觀鎖,盡量避免悲觀鎖。如果業(yè)務允許,盡量異步鎖,不要同步阻塞鎖,減少鎖競爭。
- 柔性事務代替剛性事務:有些異常或者故障,試圖通過重試是恢復不了的。
- 最終一致性:如果業(yè)務場景允許,盡量保證數(shù)據(jù)最終一致性。
- 非核心業(yè)務異步化:把某些任務轉(zhuǎn)化為另外一個隊列(消息隊列),消費端可以批量、多消費者處理。
- direct IO:例如數(shù)據(jù)庫等自己構(gòu)建緩存機制的應用程序,直接使用directIO,放棄操作系統(tǒng)提供的緩存。
- 注:脫離業(yè)務場景,很多只能是紙上談兵。但不了解手段,遇到場景也會懵逼??蛻舳苏埱笮纬傻某夑犃校蠖巳绾畏侄沃?、分散逐個擊破,是整體思想。