背景
在微服務(wù)架構(gòu)下寸潦,我們習(xí)慣使用多機(jī)器授账、分布式存儲(chǔ)瘪匿、緩存去支持一個(gè)高并發(fā)的請(qǐng)求模型仔戈,而忽略了單機(jī)高并發(fā)模型是如何工作的撼短。這篇文章通過(guò)解構(gòu)客戶(hù)端與服務(wù)端的建立連接和數(shù)據(jù)傳輸過(guò)程,闡述下如何進(jìn)行單機(jī)高并發(fā)模型設(shè)計(jì)腮考。
經(jīng)典C10K問(wèn)題
如何在一臺(tái)物理機(jī)上同時(shí)服務(wù)10K用戶(hù),及10000個(gè)用戶(hù)踩蔚,對(duì)于java程序員來(lái)說(shuō),這不是什么難事馅闽,使用netty就能構(gòu)建出支持并發(fā)超過(guò)10000的服務(wù)端程序。那么netty是如何實(shí)現(xiàn)的福也?首先我們忘掉netty局骤,從頭開(kāi)始分析暴凑。
每個(gè)用戶(hù)一個(gè)連接峦甩,對(duì)于服務(wù)端就是兩件事
- 管理這10000個(gè)連接
- 處理10000個(gè)連接的數(shù)據(jù)傳輸
TCP連接與數(shù)據(jù)傳輸
連接建立
我們以常見(jiàn)TCP連接為例现喳。
[圖片上傳失敗...(image-12267d-1657446196326)]
一張很熟悉的圖。這篇重點(diǎn)在服務(wù)端分析拿穴,所以先忽略客戶(hù)端細(xì)節(jié)泣洞。
服務(wù)器端通過(guò)創(chuàng)建socket,bind端口忧风,listen準(zhǔn)備好了默色。最后通過(guò)accept和客戶(hù)端建立連接。得到一個(gè)connectFd,即連接套接字(在Linux都是文件描述符),用來(lái)唯一標(biāo)識(shí)一個(gè)連接腿宰。之后數(shù)據(jù)傳輸都基于這個(gè)呕诉。
數(shù)據(jù)傳輸
[圖片上傳失敗...(image-c9c08f-1657446196326)]
為了進(jìn)行數(shù)據(jù)傳輸,服務(wù)端開(kāi)辟一個(gè)線程處理數(shù)據(jù)吃度。具體過(guò)程如下
select
應(yīng)用程序向系統(tǒng)內(nèi)核空間,詢(xún)問(wèn)數(shù)據(jù)是否準(zhǔn)備好(因?yàn)橛写翱诖笮∠拗扑Υ欤皇怯袛?shù)據(jù),就可以讀),數(shù)據(jù)未準(zhǔn)備好椿每,應(yīng)用程序一直阻塞伊者,等待應(yīng)答。read
內(nèi)核判斷數(shù)據(jù)準(zhǔn)備好了间护,將數(shù)據(jù)從內(nèi)核拷貝到應(yīng)用程序亦渗,完成后,成功返回汁尺。應(yīng)用程序進(jìn)行decode,業(yè)務(wù)邏輯處理,最后encode法精,再發(fā)送出去,返回給客戶(hù)端
因?yàn)槭且粋€(gè)線程處理一個(gè)連接數(shù)據(jù)痴突,對(duì)應(yīng)的線程模型是這樣
[圖片上傳失敗...(image-47a077-1657446196326)]
多路復(fù)用
阻塞vs非阻塞
因?yàn)橐粋€(gè)連接傳輸搂蜓,一個(gè)線程,需要的線程數(shù)太多辽装,占用的資源比較多帮碰。同時(shí)連接結(jié)束,資源銷(xiāo)毀拾积。又得重新創(chuàng)建連接收毫。所以一個(gè)自然而然的想法是復(fù)用線程。即多個(gè)連接使用同一個(gè)線程殷勘。這樣就引發(fā)一個(gè)問(wèn)題此再,
原本我們進(jìn)行數(shù)據(jù)傳輸?shù)娜肟谔帲嵯僭O(shè)線程正在處理某個(gè)連接的數(shù)據(jù)输拇,但是數(shù)據(jù)又一直沒(méi)有好時(shí),因?yàn)?code>select是阻塞的贤斜,這樣即使其他連接有數(shù)據(jù)可讀策吠,也讀不到。所以不能是阻塞的瘩绒,否則多個(gè)連接沒(méi)法共用一個(gè)線程猴抹。所以必須是非阻塞的。
輪詢(xún) VS 事件通知
改成非阻塞后锁荔,應(yīng)用程序就需要不斷輪詢(xún)內(nèi)核空間蟀给,判斷某個(gè)連接是否ready.
for (connectfd fd: connectFds) {
if (fd.ready) {
process();
}
}
輪詢(xún)這種方式效率比較低择克,非常耗CPU,所以一種常見(jiàn)的做法就是被調(diào)用方發(fā)事件通知告知調(diào)用方肚邢,而不是調(diào)用方一直輪詢(xún)拭卿。這就是IO多路復(fù)用,一路指的就是標(biāo)準(zhǔn)輸入和連接套接字勺鸦。通過(guò)提前注冊(cè)一批套接字到某個(gè)分組中目木,當(dāng)這個(gè)分組中有任意一個(gè)IO事件時(shí)刽射,就去通知阻塞對(duì)象準(zhǔn)備好了。
select/poll/epoll
IO多路復(fù)用技術(shù)實(shí)現(xiàn)常見(jiàn)有select懈息,poll摹恰。select與poll區(qū)別不大,主要就是poll沒(méi)有最大文件描述符的限制姑宽。
從輪詢(xún)變成事件通知闺阱,使用多路復(fù)用IO優(yōu)化后,雖然應(yīng)用程序不用一直輪詢(xún)內(nèi)核空間了瘦穆。但是收到內(nèi)核空間的事件通知后赊豌,應(yīng)用程序并不知道是哪個(gè)對(duì)應(yīng)的連接的事件,還得遍歷一下
onEvent() {
// 監(jiān)聽(tīng)到事件
for (connectfd fd: registerConnectFds) {
if (fd.ready) {
process()熙兔;
}
}
}
可預(yù)見(jiàn)的,隨著連接數(shù)增加黔姜,耗時(shí)在正比增加蒂萎。相比較與poll返回的是事件個(gè)數(shù),epoll返回是有事件發(fā)生的connectFd數(shù)組纳寂,這樣就避免了應(yīng)用程序的輪詢(xún)泻拦。
onEvent() {
// 監(jiān)聽(tīng)到事件
for (connectfd fd: readyConnectFds) {
process();
}
}
當(dāng)然epoll的高性能不止是這個(gè)腋粥,還有邊緣觸發(fā)(edge-triggered),就不在本篇闡述了架曹。
非阻塞IO+多路復(fù)用整理流程如下:
[圖片上傳失敗...(image-b5192c-1657446196326)]
select
應(yīng)用程序向系統(tǒng)內(nèi)核空間,詢(xún)問(wèn)數(shù)據(jù)是否準(zhǔn)備好(因?yàn)橛写翱诖笮∠拗瓢笮郏皇怯袛?shù)據(jù),就可以讀),直接返回罗珍,非阻塞調(diào)用脚粟。內(nèi)核空間中有數(shù)據(jù)準(zhǔn)備好了,發(fā)送ready read給應(yīng)用程序
應(yīng)用程序讀取數(shù)據(jù)通殃,進(jìn)行decode,業(yè)務(wù)邏輯處理,最后encode厕宗,再發(fā)送出去,返回給客戶(hù)端
線程池分工
上面我們主要是通過(guò)非阻塞+多路復(fù)用IO來(lái)解決局部的select
和read
問(wèn)題曲聂。我們?cè)僦匦率崂硐抡w流程佑惠,看下整個(gè)數(shù)據(jù)處理過(guò)程可以如何進(jìn)行分組齐疙。這個(gè)每個(gè)階段使用不同的線程池來(lái)處理贞奋,提高效率穷绵。
首先事件分兩種
- 連接事件
accept
動(dòng)作來(lái)處理 - 傳輸事件
select
,read
,send
動(dòng)作來(lái)處理勾缭。
連接事件處理流程比較固定俩由,無(wú)額外邏輯癌蚁,不需要進(jìn)一步拆分。傳輸事件 read
碘梢,send
是相對(duì)比較固定的,每個(gè)連接的處理邏輯相似洽洁,可以放在一個(gè)線程池處理。而具體邏輯decode
,logic
,encode
各個(gè)連接處理邏輯不同汰翠。整體可以放在一個(gè)線程池處理昭雌。
服務(wù)端拆分成3部分
- reactor部分,統(tǒng)一處理事件佛纫,然后根據(jù)類(lèi)型分發(fā)
- 連接事件分發(fā)給acceptor总放,數(shù)據(jù)傳輸事件分發(fā)給handler
- 如果是數(shù)據(jù)傳輸類(lèi)型,handler read完再交給processorc處理
因?yàn)?,2處理都比較快甥啄,放在線程池處理炬搭,業(yè)務(wù)邏輯放在另外一個(gè)線程池處理穆桂。
以上就是大名鼎鼎的reactor高并發(fā)模型享完。