Socket Server 的 N 種併發模型彙總

aceld發表於2020-04-06

原創宣告
作者: 劉丹冰Aceld,微信公眾號同名

本文主要介紹常見的Server的併發模型,這些模型與程式語言本身無關,有的程式語言可能在語法上直接透明瞭模型本質,所以開發者沒必要一定要基於模型去編寫,只是需要知道和了解併發模型的構成和特點即可。

那麼在瞭解併發模型之前,我們需要兩個必備的前置知識:

  • socket網路程式設計
  • 多路IO複用機制
  • 多執行緒/多程式等併發程式設計理論

模型一、單執行緒Accept(無IO複用)

(1) 模型結構圖

(2) 模型分析

① 主執行緒main thread執行阻塞Accept,每次客戶端Connect連結過來,main thread中accept響應並建立連線

② 建立連結成功,得到Connfd1套接字後, 依然在main thread序列處理套接字讀寫,並處理業務。

③ 在②處理業務中,如果有新客戶端Connect過來,Server無響應,直到當前套接字全部業務處理完畢。

④ 當前客戶端處理完後,完畢連結,處理下一個客戶端請求。

(3) 優缺點

優點

  • socket程式設計流程清晰且簡單,適合學習使用,瞭解socket基本程式設計流程。

缺點

  • 該模型並非併發模型,是序列的伺服器,同一時刻,監聽並響應最大的網路請求量為1。 即併發量為1

  • 僅適合學習基本socket程式設計,不適合任何伺服器Server構建。


模型二、單執行緒Accept+多執行緒讀寫業務(無IO複用)

(1) 模型結構圖

(2) 模型分析

① 主執行緒main thread執行阻塞Accept,每次客戶端Connect連結過來,main thread中accept響應並建立連線

② 建立連結成功,得到Connfd1套接字後,建立一個新執行緒thread1用來處理客戶端的讀寫業務。main thead依然回到Accept阻塞等待新客戶端。

thread1通過套接字Connfd1與客戶端進行通訊讀寫。

④ server在②處理業務中,如果有新客戶端Connect過來,main threadAccept依然響應並建立連線,重複②過程。

(3) 優缺點

優點

  • 基於模型一:單執行緒Accept(無IO複用) 支援了併發的特性。
  • 使用靈活,一個客戶端對應一個執行緒單獨處理,server處理業務內聚程度高,客戶端無論如何寫,服務端均會有一個執行緒做資源響應。

缺點

  • 隨著客戶端的數量增多,需要開闢的執行緒也增加,客戶端與server執行緒數量1:1正比關係,一次對於高併發場景,執行緒數量收到硬體上限瓶頸。
  • 對於長連結,客戶端一旦無業務讀寫,只要不關閉,server的對應執行緒依然需要保持連線(心跳、健康監測等機制),佔用連線資源和執行緒開銷資源浪費。
  • 僅適合客戶端數量不大,並且數量可控的場景使用。

僅適合學習基本socket程式設計,不適合任何伺服器Server構建。


模型三、單執行緒多路IO複用

(1) 模型結構圖

(2) 模型分析

① 主執行緒main thread建立listenFd之後,採用多路I/O複用機制(如:select、epoll)進行IO狀態阻塞監控。有Client1客戶端Connect請求,I/O複用機制檢測到ListenFd觸發讀事件,則進行Accept建立連線,並將新生成的connFd1加入到監聽I/O集合中。

Client1再次進行正常讀寫業務請求,main thread多路I/O複用機制阻塞返回,會觸該套接字的讀/寫事件等。

③ 對於Client1的讀寫業務,Server依然在main thread執行流程提繼續執行,此時如果有新的客戶端Connect連結請求過來,Server將沒有即時響應。

④ 等到Server處理完一個連線的Read+Write操作,繼續回到多路I/O複用機制阻塞,其他連結過來重複 ②、③流程。

(3) 優缺點

優點

  • 單流程解決了可以同時監聽多個客戶端讀寫狀態的模型,不需要1:1與客戶端的執行緒數量關係。
  • 多路I/O複用阻塞,非忙詢狀態,不浪費CPU資源, CPU利用率較高。

缺點

  • 雖然可以監聽多個客戶端的讀寫狀態,但是同一時間內,只能處理一個客戶端的讀寫操作,實際上讀寫的業務併發為1。
  • 多客戶端訪問Server,業務為序列執行,大量請求會有排隊延遲現象,如圖中⑤所示,當Client3佔據main thread流程時,Client1,Client2流程卡在IO複用等待下次監聽觸發事件。

模型四、單執行緒多路IO複用+多執行緒讀寫業務(業務工作池)

(1) 模型結構圖

(2) 模型分析

① 主執行緒main thread建立listenFd之後,採用多路I/O複用機制(如:select、epoll)進行IO狀態阻塞監控。有Client1客戶端Connect請求,I/O複用機制檢測到ListenFd觸發讀事件,則進行Accept建立連線,並將新生成的connFd1加入到監聽I/O集合中。

② 當connFd1有可讀訊息,觸發讀事件,並且進行讀寫訊息

main thread按照固定的協議讀取訊息,並且交給worker pool工作執行緒池, 工作執行緒池在server啟動之前就已經開啟固定數量的thread,裡面的執行緒只處理訊息業務,不進行套接字讀寫操作。

④ 工作池處理完業務,觸發connFd1寫事件,將回執客戶端的訊息通過main thead寫給對方。

(3) 優缺點

優點

  • 對於模型三, 將業務處理部分,通過工作池分離出來,減少多客戶端訪問Server,業務為序列執行,大量請求會有排隊延遲時間。
  • 實際上讀寫的業務併發為1,但是業務流程併發為worker pool執行緒數量,加快了業務處理並行效率。

缺點

  • 讀寫依然為main thread單獨處理,最高讀寫並行通道依然為1.
  • 雖然多個worker執行緒處理業務,但是最後返回給客戶端,依舊需要排隊,因為出口還是main threadRead + Write

模型五、單執行緒IO複用+多執行緒IO複用(連結執行緒池)

(1) 模型結構圖

(2) 模型分析

① Server在啟動監聽之前,開闢固定數量(N)的執行緒,用Thead Pool執行緒池管理

② 主執行緒main thread建立listenFd之後,採用多路I/O複用機制(如:select、epoll)進行IO狀態阻塞監控。有Client1客戶端Connect請求,I/O複用機制檢測到ListenFd觸發讀事件,則進行Accept建立連線,並將新生成的connFd1分發給Thread Pool中的某個執行緒進行監聽。

Thread Pool中的每個thread都啟動多路I/O複用機制(select、epoll),用來監聽main thread建立成功並且分發下來的socket套接字。

④ 如圖, thread監聽ConnFd1、ConnFd2, thread2監聽ConnFd3,thread3監聽ConnFd4. 當對應的ConnFd有讀寫事件,對應的執行緒處理該套接字的讀寫及業務。

(3) 優缺點

優點

  • main thread的單流程讀寫,分散到多執行緒完成,這樣增加了同一時刻的讀寫並行通道,並行通道數量NN為執行緒池Thread數量。
  • server同時監聽的ConnFd套接字數量幾乎成倍增大,之前的全部監控數量取決於main thread多路I/O複用機制的最大限制(select 預設為1024, epoll預設與記憶體大小相關,約3~6w不等),所以理論單點Server最高響應併發數量為N*(3~6W)(N為執行緒池Thread數量,建議與CPU核心成比例1:1)。
  • 如果良好的執行緒池數量和CPU核心數適配,那麼可以嘗試CPU核心與Thread進行繫結,從而降低CPU的切換頻率,提升每個Thread處理合理業務的效率,降低CPU切換成本開銷。

缺點

  • 雖然監聽的併發數量提升,但是最高讀寫並行通道依然為N,而且多個身處同一個Thread的客戶端,會出現讀寫延遲現象,實際上每個Thread的模型特徵與模型三:單執行緒多路IO複用一致。

模型五(程式版)、單程式多路I/O複用+多程式多路I/O複用(程式池)

(1) 模型結構圖

(2) 模型分析

五、單執行緒IO複用+多執行緒IO複用(連結執行緒池)無大差異。

不同處

  • 程式和執行緒的記憶體佈局不同導致,main process(主程式)不再進行Accept操作,而是將Accept過程分散到各個子程式(process)中.
  • 程式的特性,資源獨立,所以main process如果Accept成功的fd,其他程式無法共享資源,所以需要各子程式自行Accept建立連結
  • main process只是監聽ListenFd狀態,一旦觸發讀事件(有新連線請求). 通過一些IPC(程式間通訊:如訊號、共享記憶體、管道)等, 讓各自子程式Process競爭Accept完成連結建立,並各自監聽。
(3) 優缺點

五、單執行緒IO複用+多執行緒IO複用(連結執行緒池)無大差異。

不同處:

多程式記憶體資源空間佔用稍微大一些

多程式模型安全穩定型較強,這也是因為各自程式互不干擾的特點導致。


模型六、單執行緒多路I/O複用+多執行緒多路I/O複用+多執行緒

(1) 模型結構圖

(2) 模型分析

① Server在啟動監聽之前,開闢固定數量(N)的執行緒,用Thead Pool執行緒池管理

② 主執行緒main thread建立listenFd之後,採用多路I/O複用機制(如:select、epoll)進行IO狀態阻塞監控。有Client1客戶端Connect請求,I/O複用機制檢測到ListenFd觸發讀事件,則進行Accept建立連線,並將新生成的connFd1分發給Thread Pool中的某個執行緒進行監聽。

Thread Pool中的每個thread都啟動多路I/O複用機制(select、epoll),用來監聽main thread建立成功並且分發下來的socket套接字。一旦其中某個被監聽的客戶端套接字觸發I/O讀寫事件,那麼,會立刻開闢一個新執行緒來處理I/O讀寫業務。

④ 但某個讀寫執行緒完成當前讀寫業務,如果當前套接字沒有被關閉,那麼將當前客戶端套接字如:ConnFd3重新加回執行緒池的監控執行緒中,同時自身執行緒自我銷燬。

(3) 優缺點

優點

  • 模型五、單執行緒IO複用+多執行緒IO複用(連結執行緒池)基礎上,除了能夠保證同時響應的最高併發數,又能解決讀寫並行通道侷限的問題。

  • 同一時刻的讀寫並行通道,達到最大化極限,一個客戶端可以對應一個單獨執行流程處理讀寫業務,讀寫並行通道與客戶端數量1:1關係。

缺點

  • 該模型過於理想化,因為要求CPU核心數量足夠大。
  • 如果硬體CPU數量可數(目前的硬體情況),那麼該模型將造成大量的CPU切換成本浪費。因為為了保證讀寫並行通道與客戶端1:1的關係,那麼Server需要開闢的Thread數量就與客戶端一致,那麼執行緒池中做多路I/O複用的監聽執行緒池繫結CPU數量將變得毫無意義。
  • 如果每個臨時的讀寫Thread都能夠繫結一個單獨的CPU,那麼此模型將是最優模型。但是目前CPU的數量無法與客戶端的數量達到一個量級,目前甚至差的不是幾個量級的事。

總結

綜上,我們整理了7中Server的伺服器處理結構模型,每個模型都有各自的特點和優勢,那麼對於多少應付高併發和高CPU利用率的模型,目前多數採用的是模型五(或模型五程式版,如Nginx就是類似模型五程式版的改版)。

至於併發模型並非設計的約複雜越好,也不是執行緒開闢的越多越好,我們要考慮硬體的利用與和切換成本的開銷。模型六設計就極為複雜,執行緒較多,但以當今的硬體能力無法支撐,反倒導致該模型效能極差。所以對於不同的業務場景也要選擇適合的模型構建,並不是一定固定就要使用某個來應用。


###關於作者:

mail: danbing.at@gmail.com
github: https://github.com/aceld
原創書籍gitbook: http://legacy.gitbook.com/@aceld

創作不易, 共同學習進步, 歡迎關注作者, 回覆”zinx”有好禮

作者微信公眾號


文章推薦

開源軟體作品

(原創開源)Zinx-基於Golang輕量級伺服器併發框架-完整版(附教程視訊)

(原創開源)Lars-基於C++負載均衡遠端排程系統-完整版

精選文章

典藏版-Golang排程器GMP原理與排程全分析

典藏版-Golang三色標記、混合寫屏障GC模式圖文全分析

最常用的除錯 golang 的 bug 以及效能問題的實踐方法?

Golang中的Defer必掌握的7知識點

Golang中的區域性變數“何時棧?何時堆?”

使用Golang的interface介面設計原則

流?I/O操作?阻塞?epoll?

深入淺出Golang的協程池設計

Go語言構建微服務一站式解決方案


本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章