背景在微服務(wù)架構(gòu)下,我們習(xí)慣使用多機(jī)器、分布式存儲(chǔ)、緩存去支持一個(gè)高并發(fā)的請求模型,而忽略了單機(jī)高并發(fā)模型是如何工作的。這篇文章通過解構(gòu)客戶端與服務(wù)端的建立連接和數(shù)據(jù)傳輸過程,闡述下如何進(jìn)行單機(jī)高并發(fā)模型設(shè)計(jì)。 經(jīng)典C10K問題如何在一臺(tái)物理機(jī)上同時(shí)服務(wù)10K用戶,及10000個(gè)用戶,對于java程序員來說,這不是什么難事,使用netty就能構(gòu)建出支持并發(fā)超過10000的服務(wù)端程序。那么netty是如何實(shí)現(xiàn)的?首先我們忘掉netty,從頭開始分析。 每個(gè)用戶一個(gè)連接,對于服務(wù)端就是兩件事
TCP連接與數(shù)據(jù)傳輸連接建立我們以常見TCP連接為例。 一張很熟悉的圖。這篇重點(diǎn)在服務(wù)端分析,所以先忽略客戶端細(xì)節(jié)。 服務(wù)器端通過創(chuàng)建socket,bind端口,listen準(zhǔn)備好了。最后通過accept和客戶端建立連接。得到一個(gè)connectFd,即連接套接字(在Linux都是文件描述符),用來唯一標(biāo)識(shí)一個(gè)連接。之后數(shù)據(jù)傳輸都基于這個(gè)。 數(shù)據(jù)傳輸為了進(jìn)行數(shù)據(jù)傳輸,服務(wù)端開辟一個(gè)線程處理數(shù)據(jù)。具體過程如下
因?yàn)槭且粋€(gè)線程處理一個(gè)連接數(shù)據(jù),對應(yīng)的線程模型是這樣 多路復(fù)用阻塞vs非阻塞因?yàn)橐粋€(gè)連接傳輸,一個(gè)線程,需要的線程數(shù)太多,占用的資源比較多。同時(shí)連接結(jié)束,資源銷毀。又得重新創(chuàng)建連接。所以一個(gè)自然而然的想法是復(fù)用線程。即多個(gè)連接使用同一個(gè)線程。這樣就引發(fā)一個(gè)問題, 原本我們進(jìn)行數(shù)據(jù)傳輸?shù)娜肟谔?,,假設(shè)線程正在處理某個(gè)連接的數(shù)據(jù),但是數(shù)據(jù)又一直沒有好時(shí),因?yàn)?code>select是阻塞的,這樣即使其他連接有數(shù)據(jù)可讀,也讀不到。所以不能是阻塞的,否則多個(gè)連接沒法共用一個(gè)線程。所以必須是非阻塞的。 輪詢 VS 事件通知改成非阻塞后,應(yīng)用程序就需要不斷輪詢內(nèi)核空間,判斷某個(gè)連接是否ready.
輪詢這種方式效率比較低,非常耗CPU,所以一種常見的做法就是被調(diào)用方發(fā)事件通知告知調(diào)用方,而不是調(diào)用方一直輪詢。這就是IO多路復(fù)用,一路指的就是標(biāo)準(zhǔn)輸入和連接套接字。通過提前注冊一批套接字到某個(gè)分組中,當(dāng)這個(gè)分組中有任意一個(gè)IO事件時(shí),就去通知阻塞對象準(zhǔn)備好了。 select/poll/epollIO多路復(fù)用技術(shù)實(shí)現(xiàn)常見有select,poll。select與poll區(qū)別不大,主要就是poll沒有最大文件描述符的限制。 從輪詢變成事件通知,使用多路復(fù)用IO優(yōu)化后,雖然應(yīng)用程序不用一直輪詢內(nèi)核空間了。但是收到內(nèi)核空間的事件通知后,應(yīng)用程序并不知道是哪個(gè)對應(yīng)的連接的事件,還得遍歷一下
可預(yù)見的,隨著連接數(shù)增加,耗時(shí)在正比增加。相比較與poll返回的是事件個(gè)數(shù),epoll返回是有事件發(fā)生的connectFd數(shù)組,這樣就避免了應(yīng)用程序的輪詢。
當(dāng)然epoll的高性能不止是這個(gè),還有邊緣觸發(fā)(edge-triggered),就不在本篇闡述了。 非阻塞IO+多路復(fù)用整理流程如下:
線程池分工上面我們主要是通過非阻塞+多路復(fù)用IO來解決局部的
連接事件處理流程比較固定,無額外邏輯,不需要進(jìn)一步拆分。傳輸事件 服務(wù)端拆分成3部分
因?yàn)?,2處理都比較快,放在線程池處理,業(yè)務(wù)邏輯放在另外一個(gè)線程池處理。 以上就是大名鼎鼎的reactor高并發(fā)模型。 |
|