背景
在微服務架構下,我們習慣使用多機器、分布式存盤、快取去支持一個高并發的請求模型,而忽略了單機高并發模型是如何作業的,這篇文章通過解構客戶端與服務端的建立連接和資料傳輸程序,闡述下如何進行單機高并發模型設計,
經典C10K問題
如何在一臺物理機上同時服務10K用戶,及10000個用戶,對于java程式員來說,這不是什么難事,使用netty就能構建出支持并發超過10000的服務端程式,那么netty是如何實作的?首先我們忘掉netty,從頭開始分析,
每個用戶一個連接,對于服務端就是兩件事
- 管理這10000個連接
- 處理10000個連接的資料傳輸
TCP連接與資料傳輸
連接建立
我們以常見TCP連接為例,

一張很熟悉的圖,這篇重點在服務端分析,所以先忽略客戶端細節,
服務器端通過創建socket,bind埠,listen準備好了,最后通過accept和客戶端建立連接,得到一個connectFd,即連接套接字(在Linux都是檔案描述符),用來唯一標識一個連接,之后資料傳輸都基于這個,
資料傳輸

為了進行資料傳輸,服務端開辟一個執行緒處理資料,具體程序如下
-
select應用程式向系統內核空間,詢問資料是否準備好(因為有視窗大小限制,不是有資料,就可以讀),資料未準備好,應用程式一直阻塞,等待應答, -
read內核判斷資料準備好了,將資料從內核拷貝到應用程式,完成后,成功回傳, -
應用程式進行decode,業務邏輯處理,最后encode,再發送出去,回傳給客戶端
因為是一個執行緒處理一個連接資料,對應的執行緒模型是這樣

多路復用
阻塞vs非阻塞
因為一個連接傳輸,一個執行緒,需要的執行緒數太多,占用的資源比較多,同時連接結束,資源銷毀,又得重新創建連接,所以一個自然而然的想法是復用執行緒,即多個連接使用同一個執行緒,這樣就引發一個問題,
原本我們進行資料傳輸的入口處,,假設執行緒正在處理某個連接的資料,但是資料又一直沒有好時,因為select是阻塞的,這樣即使其他連接有資料可讀,也讀不到,所以不能是阻塞的,否則多個連接沒法共用一個執行緒,所以必須是非阻塞的,
輪詢 VS 事件通知
改成非阻塞后,應用程式就需要不斷輪詢內核空間,判斷某個連接是否ready.
for (connectfd fd: connectFds) {
if (fd.ready) {
process();
}
}
輪詢這種方式效率比較低,非常耗CPU,所以一種常見的做法就是被呼叫方發事件通知告知呼叫方,而不是呼叫方一直輪詢,這就是IO多路復用,一路指的就是標準輸入和連接套接字,通過提前注冊一批套接字到某個分組中,當這個分組中有任意一個IO事件時,就去通知阻塞物件準備好了,
select/poll/epoll
IO多路復用技術實作常見有select,poll,select與poll區別不大,主要就是poll沒有最大檔案描述符的限制,
從輪詢變成事件通知,使用多路復用IO優化后,雖然應用程式不用一直輪詢內核空間了,但是收到內核空間的事件通知后,應用程式并不知道是哪個對應的連接的事件,還得遍歷一下
onEvent() {
// 監聽到事件
for (connectfd fd: registerConnectFds) {
if (fd.ready) {
process();
}
}
}
可預見的,隨著連接數增加,耗時在正比增加,相比較與poll回傳的是事件個數,epoll回傳是有事件發生的connectFd陣列,這樣就避免了應用程式的輪詢,
onEvent() {
// 監聽到事件
for (connectfd fd: readyConnectFds) {
process();
}
}
當然epoll的高性能不止是這個,還有邊緣觸發(edge-triggered),就不在本篇闡述了,
非阻塞IO+多路復用整理流程如下:

-
select應用程式向系統內核空間,詢問資料是否準備好(因為有視窗大小限制,不是有資料,就可以讀),直接回傳,非阻塞呼叫, -
內核空間中有資料準備好了,發送ready read給應用程式
-
應用程式讀取資料,進行decode,業務邏輯處理,最后encode,再發送出去,回傳給客戶端
執行緒池分工
上面我們主要是通過非阻塞+多路復用IO來解決區域的select 和read問題,我們再重新梳理下整體流程,看下整個資料處理程序可以如何進行分組,這個每個階段使用不同的執行緒池來處理,提高效率,
首先事件分兩種
- 連接事件
accept動作來處理 - 傳輸事件
select,read,send動作來處理,
連接事件處理流程比較固定,無額外邏輯,不需要進一步拆分,傳輸事件 read,send是相對比較固定的,每個連接的處理邏輯相似,可以放在一個執行緒池處理,而具體邏輯decode,logic,encode 各個連接處理邏輯不同,整體可以放在一個執行緒池處理,
服務端拆分成3部分
- reactor部分,統一處理事件,然后根據型別分發
- 連接事件分發給acceptor,資料傳輸事件分發給handler
- 如果是資料傳輸型別,handler read完再交給processorc處理
因為1,2處理都比較快,放在執行緒池處理,業務邏輯放在另外一個執行緒池處理,
以上就是大名鼎鼎的reactor高并發模型,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/498999.html
標籤:其他
