文章目錄
- 兩種服務器模型及三個模塊
- C/S模型
- P2P模型
- I/O處理單元、邏輯單元、存儲單元
- 并發
- 同步與異步
- 半同步/半異步模式
- 變體:半同步/半反應堆模式
- 改進:高效的半同步/半異步模式
- 領導者/追隨者模式
- 組件 :句柄集、線程集、事件處理器
- 工作流程
兩種服務器模型及三個模塊
C/S模型
即常說的 客戶端/服務器 模型,將資源(視頻、文本、圖片、軟件等)提供者視作服務器,資源請求者視為客戶端。
由于客戶端連接請求(connect函數
)是隨機到達的異步事件,服務器需要使用某種 I/O模型 來監聽這一事件。例如 I/O復用技術之一的 select系統調用:當監聽到連接請求后,服務器就調用 accept函數
接收它,并分配一個 邏輯單元(新創建的子進程、子線程等) 管理這個新連接。
工作流程如下圖所示:
服務器在處理一個客戶請求的同時還要繼續監聽其他客戶請求,否則就變成了效率低下的串行服務器了(必須先處理完前一個客戶的請求,才能繼續處理下一個客戶請求)。這一點上圖中是通過 select系統調用
實現的。
- 優點: 實現簡單、適合資源相對集中的場合。
- 缺點: 服務器是通信的中心,當訪問量過大時,可能所有客戶都將得到很慢的響應。
P2P模型
為了解決 C/S模型 的缺點而誕生,P2P(Peer to Peer,點對點)模型 比 C/S模型 更符合網絡通信的實際情況。摒棄了以服務器為中心的格局,讓網絡上所有主機重新回歸對等的地位。
- 優點: 每臺機器在消耗服務的同時給別人提供服務,這樣資源能夠充分、自由地共享。(P2P模型的典范:云計算機群)
- 缺點: 用戶之間傳輸的請求過多時,網絡的負載將加重。
P2P模型的實現: 主機之間很難互相發現,所以實際使用時通常帶有一個專門的發現服務器,其還提供查找服務(甚至還可以提供內容服務),使每個客戶都能盡快地找到自己需要的資源。
I/O處理單元、邏輯單元、存儲單元
可以將服務器解構為三個主要模塊:
模塊 | 單個服務器程序 | 服務器機群 |
---|---|---|
I/O處理單元 | 等待并接受新的客戶連接、讀寫網絡數據,將服務器響應數據返回給客戶端。 | 作為接入服務器,實現負載均衡,從所有邏輯服務器中選取負荷最小的一臺來為新客戶服務。 |
邏輯單元 | 通常是一個進程或線程,處理客戶數據并將結果傳遞給 I/O 處理單元或者直接發送給客戶端(取決于事件處理模式) | 一臺邏輯服務器 |
網絡存儲單元 | 本地數據庫、文件或緩存 | 數據庫服務器 |
請求隊列 | 各單元間的通信方式 | 各服務器間的永久的、靜態的TCP連接,避免了動態TCP連接導致的額外系統開銷。 |
實際編程中,I/O處理單元常被稱作主線程,邏輯單元常被稱為工作線程。
值得注意的是:
- 服務器通常擁有多個邏輯單元,以實現對多個客戶任務的并行處理。
- 網絡存儲單元不是必須的,如
ssh
、telnet
等登陸服務就不需要這個單元。 - 請求隊列通產被實現為池的一部分,是各個單元之間的通信方式的抽象。
并發
缺點與優點:
- 缺點: 如果程序是計算密集型,并發編程引起的任務切換反而使得效率降低。(任務切換耗時大于計算耗時)
- 優點: 如果程序是I/O密集型,由于I/O操作耗時遠大于CPU計算耗時,因此如果程序阻塞于I/O操作將浪費大量CPU時間,解決方法是:當前被I/O操作阻塞的執行線程可以主動放棄CPU(或由操作系統調度),將執行權轉移到其他線程。此時并發引起的任務切換可以大大提高CPU利用率。
并發模式: I/O處理單元和多個邏輯單元之間協調完成任務的方法。
服務器主要有兩種并發編程模式:半同步/半異步模式(half-sync/half-async
)、領導者/追隨者模式(Leader/Followers
)。
同步與異步
- I/O模型中,同步 or 異步 區分的是內核向應用程序通知的是 I/O就緒事件 or I/O完成事件,以及由 應用程序 還是 內核 來完成I/O讀寫。(詳見兩種高效事件處理模式一文)
- 并發模式中,同步指程序完全按照代碼序列的順序執行;異步指程序的執行需要由系統事件(中斷、信號等)來驅動。
半同步/半異步模式
按照同步/異步方式運行的線程被稱為同步線程/異步線程:
- 同步線程: 效率低、實時性差,但邏輯簡單。
- 異步線程: 效率高、實時性強,但編程復雜且難于調試、擴展。
服務器同時使用同步線程和異步線程實現,即半同步/半異步模式。
工作流程:
- 同步線程用于處理客戶邏輯(類似于工作線程)、異步線程用于處理注冊的I/O事件(類似于主線程)。
- 異步線程監聽到客戶請求后,就將其封裝成一個請求對象并插入請求隊列中。
- 請求隊列通知某個同步模式的工作線程來讀取并處理該請求對象。
變體:半同步/半反應堆模式
結合兩種事件處理模式和幾種I/O模型的話,半同步/半異步模式就存在多種變體,其中一種稱為半同步/半反應堆(half-sync/half-reactive
)模式。
工作流程
- 異步線程只有一個,由主線程充當,負責監聽所有
socket
上的事件。 - 如果 監聽
socket
上有可讀事件發生時(即有新的連接請求到來),主線程就接受新的socket
連接,然后往epoll
內核事件表中注冊該socket
上的讀寫事件。 - 如果接受的 連接
socket
上有讀寫事件發生(上一步注冊的),即 有新的客戶請求到了 or 有數據要發送到客戶端,主線程就將該socket
連接 插入請求隊列中。 - 所有的工作線程都睡眠在請求隊列上,當有任務到來時(就緒的
socket
連接被插入請求隊列中,這說明半同步/半反應堆模式采用的事件處理模式是Reactor
模式),所有空閑的工作線程通過競爭(比如申請互斥鎖)獲取任務的接管權。
事件處理模式的選擇
- 采用
Reactor
模式意味著 工作線程 要負責 讀寫工作:既要 從socket
上讀取客戶請求 ,還要 往socket
寫入服務器應答 。這也是名稱中 半反應堆 的含義。 - 當然,半同步/半反應堆模式也可以使用模擬的
Proactor
事件處理模式,即由主線程來完成數據的讀寫:- 此時,主線程會將應用程序數據、任務類型等信息封裝為一個任務對象,然后將任務對象(或者是指向該任務對象的一個指針)插入請求隊列。
- 工作線程從請求隊列中取得任務對象之后,即可直接處理客戶請求,無須執行讀寫操作了。
缺點
- 主線程和工作線程共享請求隊列。 主線程往請求隊列中添加任務,或者工作線程從請求隊列中取出任務,都需要對請求隊列加鎖保護,從而白白耗費CPU時間。
- 每個工作線程在同一時間只能處理一個客戶請求。 如果客戶數量較多,而工作線程較少,則請求隊列中將堆積很多任務對象,客戶端的響應速度越來越慢。如果增加工作線程,則又會耗費大量CPU時間。
總而言之,耗費CPU時間。
改進:高效的半同步/半異步模式
針對上面提到的第二個缺點,可以讓每個工作線程都能同時處理多個客戶連接:
工作流程:
- 主線程 只管理 監聽
socket
,連接socket
由 工作線程 來負責:當有新的連接socket
到來時,主線程就接受之并將其派發給某個工作線程,此后該新socket
上的任何IO操作都由被選中的工作線程來處理,直到客戶關閉連接。 - 主線程向工作線程派發
socket
的最簡單的方式,是往它和工作線程之間的管道里寫數據。工作線程檢測到管道上有數據可讀時,就分析是否是一個新的客戶連接請求到來。如果是,則把該新socket
上的讀寫事件注冊到自己的epoll
內核事件表中。(注冊這件事本來是主線程在做)
PS: 事實上,每個線程(主線程和工作線程)都維持自己的事件循環,它們各自獨立地監聽不同的事件,每個線程都工作在異步模式,所以它并非嚴格意義上的半同步/半異步模式。
領導者/追隨者模式
領導者/追隨者模式是:多個工作線程輪流獲得事件源集合,輪流監聽、分發并處理事件的一種模式。
- 領導者: 在任意時間點中,程序都只會有一個領導者線程,它負責進行I/O事件的監聽。
- 追隨者: 而其他的線程則為追隨者線程,他們休眠在線程池中,等待成為新的領導者。
- 工作流程: 如果當前的領導者檢測到I/O事件,首先要從線程池中推選出新的領導者線程等待新的I/O事件的到來,然后舊的領導者處理I/O事件,以此實現并發。
用通俗點的方法來講就像是一群在營地中輪流放哨的哨兵,每次都會有一個人在值班,而其他人去休息。當值班者發現有什么特殊情況的時候就會去讓領班叫醒一個哨兵來繼續放哨,然后自己去探查情況。如果探查情況完后沒人值班,則自己繼續盯梢,否則就去休息。
組件 :句柄集、線程集、事件處理器
領導者/追隨者模式包含的組件有:句柄集(HandleSet
)、線程集(ThreadSet
)、事件處理器(EventHandler
),之間的關系如圖所示:
句柄集
- 句柄(
Handle
) 用于表示I/O資源,在Linux
下通常就是一個文件描述符。 - 句柄集 其實就是句柄的監控管理集合,通過調用
wait_for_event
方法來監聽這些句柄上的I/O事件,并將其中的就緒事件通知給領導者線程,而領導者線程則調用綁定到Handle
上的事件處理器來處理事件。綁定是通過調用句柄集中的register_handle
方法實現的。
線程集
線程集是所有工作線程(包括領導者和追隨者)的管理者。它負責各線程之間的同步,以及新領導者線程的推選。
線程集中的線程在任意時間都必然處于下面三種狀態之一:
- 領導者(Leader): 線程處于領導者身份,負責監聽句柄集上的I/O事件
- 事件處理中(Processing): 線程正在處理事件。領導者檢測到I/O事件后,轉移到
Processing
狀態進行事件的處理,并且調用promote_new_leader
推選新的領導者。如果不想讓出領導者的地位,也可以指定其他的追隨者來處理事件(Event Handoff
)。當處于Processing
狀態的線程處理完事件之后,如果當前線程集中沒有領導者,他就會成為新的領導者,否則就直接變為追隨者。 - 追隨者(Follower): 線程此時處于追隨者身份,此時處于休眠狀態,通過調用線程集的
join
等待被推選為新的領導者,也可能被當前的領導者指定處理新的任務。
狀態轉移圖:
PS
領導者推選新的領導者 和 追隨者等待成為新的領導者 這兩個操作都將修改線程集,因此線程集提供一個成員 Synchronizer
來同步這兩個操作,以避免竟態條件。
事件處理器
- 事件處理器通常包含一個或者多個回調函數
handle_event
,用于處理事件對應的業務邏輯。 - 事件處理器在使用前首先需要被綁定到某個句柄之上,每當該句柄上有事件發生的時候,領導者就執行與之綁定的事件處理器中的回調函數。
- 具體的事件處理器需要重新實現基類的
handle_event
方法,以處理特定任務。
工作流程
PS
- 由于領導者線程自己監聽I/O事件并且處理客戶請求,所以在本模式中不需要在線程之間傳遞任何額外的數據,也不需要像半同步/半反應堆模式那樣在線程之間同步對請求隊列的訪問。(CPU耗時低)
- 但是也有一個明顯的缺點就是只能支持一個事件源集合,因此無法像高效的半同步/半異步模式那樣讓每個工作線程獨立地管理多個客戶連接。