張雨萌
(上海浦東發展銀行總行信息科技部,上海 200000)
本文首先重新定義一個名詞——服務。服務特指由網絡實現的應用系統間的API 調用與響應過程,其中API調用和響應不局限于特定形式。
一個服務存在兩個重要角色:服務調用方和服務提供方。本文將服務中發起API 調用的應用系統稱之為服務調用方,將提供API 調用并作出響應的應用系統稱之為服務提供方。
在網絡架構中,存在著客戶端、服務器之分,與之對應存在著客戶端應用系統、服務器應用系統。需要強調的是,服務器應用系統一定是服務提供方,客戶端應用系統一定是服務調用方。
在服務A 中,如果服務器應用系統在收到服務調用方的API 調用請求后,需要與其他服務提供方新建另一個服務B,并根據新建服務B 的響應結果完成服務A。該服務器應用系統同時具有服務提供方和服務調用方兩個角色,服務A 被稱為依賴型服務。如果服務器應用系統在收到服務調用方的API 調用請求后,不需要新建服務,依靠本系統即可完成服務的響應,則該服務器應用系統僅作為服務提供方角色,本文將該服務稱之為獨立型服務。
因承載服務類型的不同,本文將獨立型服務中的服務器應用系統稱為完全服務器應用系統。將依賴型服務中的服務器應用系統稱為服務器—客戶端應用系統。
服務器—客戶端應用系統的本質依然是服務器應用系統,其與完全服務器應用系統主要差異表現在服務中所處位置不同、提供服務的基本過程不同。
完全服務器應用系統和服務器—客戶端應用系統在服務中所處位置如圖1 所示,完全服務器應用系統在服務中是I/O 請求數據的終點和I/O 響應數據的起點,而服務器—客戶端應用系統只是I/O 數據流轉的中間環節。

Fig.1 Position of total-server application system and server-client application system in the service圖1 完全服務器應用系統和服務器—客戶端應用系統在服務中的位置
完全服務器應用系統在服務中的基本處理流程如圖2所示,服務器—客戶端應用系統在服務中處理基本過程如圖3 所示。通過對比發現,服務器—客戶端應用系統與完全服務器應用系統的最大差異在于其將業務處理過程分成兩個階段,并在業務處理過程中新建另一個服務,其業務處理結果依賴于新建服務的響應數據。

Fig.2 Basic process of total-server application system in the service圖2 完全服務器應用系統在服務中的基本處理流程

Fig.3 Basic process of server-client application system in the service圖3 服務器—客戶端應用系統在服務中的基本處理流程
服務器應用系統的線程模型研究較多。1999 年,為了優化網絡編程的程序設計,Schmid[1]在應用架構設計層面提出了Reactor 模型,并結合Unix 網絡編程技術[2]給出模型實現;針對突破單機性能局限提升應用系統的服務承載能力,Dan[3]提出了c10k 問題,并首次提出包括NIO 在內的解決方案;Doug[4]在總結應用程序線程模型基礎上,結合Java NIO 技術進一步細化了Reactor 模型,提出單線程Reactor模型、線程池Reactor 模型以及多Reactor 模型。以上學者的核心思想是提高應用系統的服務承載能力,解耦I/O 事件和進程/線程資源、復用進程/線程資源以及合理的程序模型設計。在上述研究基礎上,各類基于Java NIO[5-7]以及基于Reactor 模型的應用設計[8-21]得到廣泛應用。
受限于當時的業務場景,關于服務器應用系統的線程模型設計多關注于解耦在服務提供方角色下的I/O 事件和進程/線程資源,并未考慮在服務調用方角色下I/O 事件和進程/線程資源的解耦方式,即相關行業實踐和研究更適配于完全服務器應用系統而無法適配服務器—客戶端應用系統。
為適配服務器—客戶端應用系統,本文提出拓展的多Reactor 模型,在多Reactor 模型基礎上增加異步回調處理機制,同時解耦服務調用方和服務提供方兩個角色上的I/O事件與線程資源。
經典線程模型如圖4 所示。服務器應用系統在收到客戶端應用系統的API 調用請求后,為每個請求分配一個handler 線程,完成socket 對象的建立和業務處理,在handler 線程完成業務處理后,通過已建立的socket 對象向客戶端應用系統作出響應。

Fig.4 Classic thread model圖4 經典線程模型
經典線程模型存在如下缺點:①經典線程模型下,服務器應用系統承載服務的能力有限,其并發服務數受限于可用線程數;②在大量handler 線程阻塞的情況下,服務器應用系統會出現線程頻繁切換、CPU 使用率增高等問題,導致系統處理能力下降。
為了優化經典線程模型,提出以I/O 多路復用技術為基礎的Reactor 模型并應用于服務器系統中。
單線程Reactor 模型首次將服務器應用系統在服務提供方角色下的I/O 事件與線程資源進行解耦,該模型如圖5所示。

Fig.5 Basic Reactor model圖5 單線程Reactor 模型
單線程Reactor 模型主要包括Reactor Thread、Acceptor以及負責業務處理的Handler 線程。
1.2.1 單線程Reactor 模型模塊介紹
Reactor Thread 保存了服務器創建網絡連接的必要信息以及一個I/O 多路復用器,在創建ServerSocket 對象后將ServerSocket 對象注冊到I/O 多路復用器,其不斷監聽socket對象的可讀(READABLE)、可寫(WRITABLE)事件。Reactor Thread 包括一個Dispatcher 組件,在有可讀(READABLE)的I/O 數據時,Dispatcher 組件將創建一個負責業務處理的Handler 線程。
Acceptor 通過Reactor Thread 中的I/O 多路復用器獲取可建立連接(ACCEPTABLE)狀態的I/O 請求,并創建一個已連接狀態(ACCEPTED)的socket 對象,將該socket 對象注冊到Reactor Thread 中的I/O 多路復用器。
Handler 線程用于具體業務處理,其由Dispatcher 組件創建,并在業務處理過程完成后銷毀。
1.2.2 單線程Reactor 模型不足
從架構層面看,一個Reactor Thread 在面對高負載、多并發應用場景時,單線程Reactor 模型存在如下問題:
(1)性能問題。Reactor Thread 承擔職責太多,一個Reactor Thread 同時處理上百萬個通訊鏈路,性能上無法支撐;另外,由于業務處理線程無法重用,其每次創建的成本必然在高并發時對系統運行造成巨大壓力。
(2)可靠性問題。一旦Reactor Thread 出現故障,會導致整個系統通信模塊不可用,不能接收和處理I/O 數據,造成節點故障。
雖然單線程Reactor 模型首次把服務器應用系統在服務提供方角色下的I/O 事件與線程資源進行了解耦,但由于業務處理線程無法重用,其每次創建的成本必然在高并發時對系統運行造成巨大壓力。為改善單線程Reactor 模型業務處理線程無法重用的問題,線程池Reactor 模型應運而生。線程池Reactor 模型如圖6 所示。
相較于單線程Reactor 模型,線程池Reactor 模型去除Dispatcher 組件,使用了線程池進行業務處理,此舉可以避免業務處理線程不斷創建、銷毀。但線程池Reactor 模型仍然沒有解決單線程Reactor 模型中Reactor Thread 同時處理上百萬個通訊鏈路時的性能及可靠性問題。

Fig.6 Thread pool Reactor model圖6 線程池Reactor 模型
作為目前主流的線程模型,多Reactor 模型成功解決了線程池Reactor 模型的性能及可靠性問題,其核心思想是將ServerSocket 對象的創建同socket 對象可讀(READABLE)、可寫(WRITABLE)事件的監聽解耦,并將監聽socket 對象可讀(READABLE)、可寫(WRITABLE)事件的工作放到多個線程、多個I/O 多路復用器中。
多Reactor 模型結構如圖7 所示,主要包括Main Reactor、Acceptor、Sub Reactor、Handler Thread Pool 四部分。

Fig.7 Multiple Reactor model圖7 多Reactor 模型
1.4.1 多Reactor 模型模塊介紹
(1)Main Reactor。保存服務器創建網絡連接的必要信息并創建ServerSocket 對象,將ServerSocket 對象注冊到其與Acceptor 共享的一個I/O 多路復用器中,注冊監聽的I/O事件為可建立連接(ACCEPTABLE)狀態的I/O 請求,一般由單線程完成。
(2)Acceptor。用于監聽服務調用方的網絡連接請求,通過其與Main Reactor 共享的I/O 多路復用器獲取Server-Socket 對象,可建立連接(ACCEPTABLE)狀態的I/O 請求,創建一個已連接狀態(ACCEPTED)的socket 對象,并將該socket 對象注冊到Sub Reactor 中的某個I/O 多路復用器中。
(3)Sub Reactor。不斷監聽socket 對象的可讀(READABLE)、可寫(WRITABLE)事件,讀取可讀狀態下socket 對象的I/O 數據,并調用Handler Thread Pool 中的線程進行業務處理;一旦有可寫狀態的I/O 數據就將I/O 數據返回給客戶端應用系統。采用線程池方式構建,線程池中的每個線程保存一個I/O 多路復用器。
(4)Handler Thread Pool。負責具體的業務處理,輸入為Sub Reactor 讀取的socket 對象I/O 數據,輸出為Sub Reactor 響 應 的I/O 數 據。
1.4.2 無法適配問題
多Reactor 模型通過對模塊的合理規劃,解耦了Server-Socket 對象的創建,以及socket 對象可讀(READABLE)、可寫(WRITABLE)事件的監聽,有效提升了服務器應用系統的承載能力和可靠性。
但由于在多Reactor 模型中,Handler Thread Pool 線程只用于業務處理,未考慮到依賴型服務的業務處理過程,這意味著使用多Reactor 模型的服務器—客戶端應用系統在作為服務調用方時,在其他服務提供方的響應時間內業務處理線程是阻塞狀態,無法承載其他服務。而如果阻塞線程過多,新服務請求的響應速度或已積壓服務請求的處理速度將大幅降低,造成服務大量超時,所以多Reactor 模型有著天然無法適配服務器—客戶端應用系統的問題。從這個角度看,多Reactor 模型只是一個適配完全服務器應用系統的線程模型。
為解耦服務器—客戶端應用系統在服務調用方和服務提供方兩個角色上的I/O 事件與線程資源,本文在多Reactor 模型基礎上提出了一種拓展多Reactor 模型,旨在適配服務器—客戶端應用系統。
拓展多Reactor 模型如圖8 所示。其中,Main Reactor 的作用與多Reactor 模型中的Main Reactor 一致,保存服務器創建網絡連接的必要信息并創建ServerSocket 對象,將ServerSocket 對象注冊到其與Acceptor 共享的一個I/O 多路復用器中。注冊監聽的I/O 事件為可建立連接(ACCEPTABLE)狀態的I/O 請求,由單線程完成。

Fig.8 Extended multi-Reactor mode圖8 拓展多Reactor 模型
Acceptor 用于監聽服務調用方的網絡連接請求,通過其與Main Reactor 共享的I/O 多路復用器獲取ServerSocket對象,建立連接(ACCEPTABLE)狀態的I/O 請求,創建一個已連接狀態(ACCEPTED)的socket 對象,并將該socket 對象注冊到1st Sub Reactor 中的某個I/O 多路復用器中。
1st Sub Reactor 不斷監聽socket 對象的I/O 可讀(READABLE)、可寫(WRITABLE)事件,讀取可讀狀態下socket 對象的I/O 數據,同時調用1st Thread Pool 中的線程進行前半程業務處理,一旦有可寫狀態的I/O 數據就將I/O 數據返回給客戶端應用系統。采用線程池構建,線程池中的每個線程保存一個I/O 多路復用器。
1st Thread Pool 負責執行前半程業務處理過程,通過2nd Sub Reactor 與其他服務提供方建立連接并發送I/O 請求數據。
2nd Sub Reactor 是實現服務調用方角色上解耦I/O 事件與線程資源的核心模塊,主要承載服務器—客戶端應用系統新建服務的工作,在其內部同樣保持一個或多個I/O多路復用器,在獲取到其他服務提供方的I/O 響應數據后,通過2nd Thread Pool 中的線程回調后半程的業務處理過程。
2nd Thread Pool 負責執行后半程的業務處理過程,輸入為2nd Sub Reactor 獲取到的I/O 響應數據。在完成后半程業務處理后,2nd Thread Pool 中的線程通過修改1st Sub Reactor 的I/O 多路復用器中的socket 監聽事件為可寫(WRITABLE),1st Sub Reactor 作為服務提供方角色下的響應數據返回給客戶端應用系統。
拓展多Reactor 模型中線程資源、I/O 事件的時序圖如圖9 所示,該模型在服務調用方和服務提供方兩個角色上均實現了I/O 事件與線程資源的解耦。
本文對拓展多Reactor 模型下的服務器—客戶端應用系統和多Reactor 模型下的服務器—客戶端應用系統分別進行HTTP 協議的性能測試,并比較了服務器—客戶端應用系統在兩個模型下的性能測試結果。
性能測試按照圖1 所示的鏈路開展,其中客戶端應用程序使用Apache Jmeter 性能測試工具,在15min 內,通過向服務器—客戶端應用系統不斷發送HTTP 請求以獲取服務器—客戶端應用系統的最高TPS 和交易失敗率,其獲取響應數據的超時時間為20s。完全服務器使用多Reactor 模型,業務處理過程不涉及I/O 操作,其服務響應時間設置為10s,系統可用線程數設置為150。

Fig.9 Life cycle of thread resources and I/O events in the extended multi-reactor model圖9 拓展多Reactor 模型中線程資源、I/O 事件的生命周期
服務器—客戶端應用系統在拓展多Reactor 模型和多Reactor 模型下都使用Apache Tomcat 作為Web 容器,通過開啟Http11NioProtocol 協議實現多Reactor 模型。在此基礎上,拓展多Reactor 模型下的服務器—客戶端應用系統借助Servlet 3.1 中的異步處理技術以及HttpAsyncClient 工具進行構建。
服務器—客戶端應用系統在拓展多Reactor 模型和多Reactor 模型下使用工具如表1 所示。

Table 1 Comparison of tools used in server-client application system under extended multi-reactor model and multiple reactor model表1 服務器—客戶端應用系統在拓展多Reactor 模型和多Reactor 模型下使用工具對比
性能測試結果如表2 所示。
通過性能測試結果可以發現,在多Reactor 模型下,服務器—客戶端應用系統最高的TPS 一直未超過50,且在200 和500 并發用戶數時,分別出現16.78%和96.26 失敗率,究其原因是由于服務器—客戶端應用系統的業務處理線程大量阻塞,沒有足夠的線程資源處理已經積壓的請求。在拓展多Reactor 模型下,服務器—客戶端應用系統最高的TPS 可以突破50,在500 并發用戶數時,最高TPS 可以到達200,且失敗率一直保持為0%。

Table 2 Performance test results of server-client application system under extended multi-reactor model and multiple reactor model表2 服務器—客戶端應用系統在拓展多Reactor 模型和多Reactor 模型下的性能測試結果
實驗證明,拓展多Reactor 模型在正確率、TPS 兩個方面均優于多Reactor 模型。
本文首次從場景上將服務器應用系統細分為完全服務器應用系統和服務器—客戶端應用系統,并分析了兩種應用系統的差異之處,為系統設計提供了一種新的視角;本文還針對性地提出了拓展多Reactor 模型的線程設計,為服務器—客戶端應用系統進一步提升服務承載能力提供了一種新的可行模型。
本文仍有不足之處,如模型的構建基于Java 語言,在Apache Tomcat 以及HttpAsyncClient 工具的支持下完成,而生產實際中存在多種語言和工具,如何構建一個輕量級的Java 工具庫,以及基于其他語言構建工具庫是下一步研究的方向。