首頁>技術>

開發通訊系統的程式設計師對OS系統提供的Socket引數配置、系統呼叫等介面,普遍都有不勝其煩的感覺,作為通訊伺服器(TCP、UDP、HTTP等)還需要考慮支撐大併發、大吞吐能力所需的程序/執行緒框架、阻塞/非阻塞、同步/非同步等架構或模型。眾所周知,基於非阻塞、非同步事件驅動通訊模型是被認為效率最好的架構,當前應用開發中使用比較普遍的是 Libevent 事件驅動開源系統。

最近發現一款開源專案 ePump 框架,是一種適合懶人的高效能應用系統開發框架,在諸多方面優於Libevent 系統,其開原始碼使用寬鬆的MIT協議,在GitHub上可下載,參見 https://github.com/kehengzhong/epump

ePump 採用多執行緒、事件驅動架構來監聽管理檔案描述符FD的可讀Readable、可寫Writable,以及定時器超時Timeout等狀態變化,進而產生對應的事件,將事件派發到各個工作執行緒中以回撥函式方式進行處理。

ePump將大量繁瑣的建立網路檔案描述符、bind、listen、connect、accept等介面封裝成幾個標準的API,如想建立一個TCP伺服器,就呼叫eptcp_mlisten 即可,想連線遠端伺服器呼叫 eptcp_connect 即可,充當TCP伺服器時收到遠端TCP連線請求,ePump 框架呼叫 eptcp_mlisten 中傳入的回撥函式,回撥函式內呼叫 eptcp_accept 即可接受新連線。

使用ePump框架的程式設計師只需關注那幾個回撥函式的實現即可,如使用ePump開發一個TCP伺服器時,只需實現: (1)遠端連線請求到來時的回撥函式;(2)連線上有資料可讀的回撥函式;(3)程式執行過程中,可動態新增監聽連線可寫的回撥函式;(4)可隨意啟動任何定時器,並處理超時的回撥函式。

基於以上回調函式,就可以實現一個支撐大併發、充分利用硬體多CPU並行運算能力、作業系統多執行緒排程等功能的、高效能的伺服器系統,大量複雜的多執行緒排程、事件驅動機制、非阻塞非同步處理、負載均衡、驚群效應處理等都封裝在ePump框架中。

一. ePump是什麼?

ePump是一個基於I/O事件通知、非阻塞通訊、多路複用、多執行緒等機制開發的事件驅動模型的 C 語言應用開發框架,利用該框架可以很容易地開發出高效能、大併發連線的伺服器程式。

ePump是事件泵(Event Pump)的英文簡稱,顧名思義,意思是對各種網路讀寫事件、定時器事件等進行迴圈處理的泵,這些底層事件包括檔案描述符FD的讀就緒(Read Readiness)、寫就緒(Write Readiness)、連線成功(Connected)、定時器超時(Timeout)等等。

ePump負責管理和監控處於非阻塞模式的檔案描述符FD和定時器,根據其狀態變化產生相應的事件,並派發到相應的工作執行緒或ePump執行緒的事件佇列中,這些執行緒透過呼叫該事件關聯的回撥函式(Callback)來處理事件。

應用程式呼叫ePump框架提供的介面函式來預先建立、開啟各種網路通訊Socket檔案描述符FD,或啟動定時器等,並將其新增或繫結到ePump執行緒的監控佇列中,對這些FD和定時器的狀態監控是採用作業系統提供的I/O事件通知設施,如epoll、select、poll、kqueue、completion port等。

二. ePump解決什麼?

許多伺服器程式需要處理來自客戶側發起的大併發TCP連線請求、UDP請求,如Web伺服器、Online伺服器、訊息系統等。早期實現的通訊伺服器類系統中,一個連線請求通常是由一個獨立的程序或執行緒來接受並處理通訊細節,如早先的Apache Web伺服器;或者是利用OS提供的I/O非同步事件通知、多路複用機制實現單程序下處理多個非阻塞併發連線請求,如SQUID系統。

這些系統採用的框架,要不在等待網路等I/O裝置的資料到來之前阻塞自己,要不採用單程序多路複用模型,它們對CPU的利用效率多少存在一定的侷限,而ePump框架是一種充分高效利用CPU處理能力的事件驅動模型框架。

ePump框架是一個多執行緒(未來增加多程序)事件驅動模型框架,基於檔案描述符的非同步就緒通知(Readiness Notification)機制,無需為等待"在路上"的資料而阻塞等待工作執行緒或工作程序。

該框架為每個檔案描述符建立iodev_t物件,為定時驅動的應用程式建立定時器iotimer_t物件,利用作業系統提供的I/O事件通知設施如epoll、select等,將建立或開啟的檔案描述符FD設定為非阻塞模式,並新增到系統的監控管理列表中,對其狀態變化進行非同步回撥通知。

對這兩類物件的監控管理和事件通知派發是由ePump執行緒池來實現,對事件的回撥處理是由Worker工作執行緒池或ePump執行緒池來完成。為了充分利用伺服器硬體的效能,工作處理執行緒的個數一般跟CPU Core核數量一致。

大量複雜的底層處理細節都被封裝成一些簡單易用的API介面函式,透過這些API函式,開發者可以快速開發出支撐大併發的高效能伺服器程式。

三. ePump框架工作原理

ePump框架是作者在其2003年開發的eProbe框架的基礎上發展而來,是Event Pump的縮寫,顧名思義這是一個事件驅動架構。

對於不同的I/O事件通知、非阻塞通訊、多路複用機制,包括epoll、select、kqueue、completion port i/o等,其基本工作原理包括:

將FD增加到監聽列表中將FD從監聽列表中刪除設定阻塞監聽的時間阻塞等待監聽列表中FD set,等候R/W事件發生輪詢FD set列表檢測各FD是否產生R/W事件,執行該事件對應的回撥函式檢查Timeout,執行Timeout事件對應的回撥函式3.1 ePump基礎資料結構

根據以上工作原理,我們設計ePump框架的幾個基礎資料結構:

3.1.1 裝置(iodev_t)

針對每個FD,用資料結構為iodev_t來管理,將檔案描述符FD當作iodev_t裝置,針對裝置來管理讀寫狀態、FD型別、要處理的讀寫事件、回撥函式和回撥引數、四元組地址等等. 我們把TCP監聽socket、TCP連線socket(主動連線的、被動接收的)、UDP監聽socket、UDP客戶socket、Unix Socket、ICMP Raw Socket、UDP Raw Socket等等,都透過iodev_t裝置來管理。

所有的iodev_t裝置都會產生事件,ePump系統對iodev_t裝置產生的事件進行處理,即透過事件驅動多執行緒來呼叫回撥函式。

3.1.2 定時器(iotimer_t)

類似iodev_t裝置,能產生驅動事件的還有iotimer_t定時器, 設定一個時間並啟動定時器後,系統將從當前時刻起到指定時間到達時,產生Timeout事件。

iotimer_t定時器有一次性的和週期性的,iotimer_t定時器資料結構管理定時器id、回撥函式和回撥引數、定時的時間等。

在Unix類OS系統,一個程序只能設定一個時鐘定時器,由系統提供的介面來設定,常用的有alarm()和setitimer()。對於通訊系統中大量存在各種定時器需求,同時考慮跨平臺性等,系統提供的定時器介面一般都不能滿足需求。我們在ePump系統中設計了iotimer_t資料結構,可提供毫秒級精度、同時大併發數量的定時器功能實現。

ePump架構中把定時器當做一個重要的基礎設施,與檔案描述符裝置一樣被ePump執行緒監聽和管理。

3.1.3 事件(ioevent_t)

ioevent_t事件是ePump的信使,管理事件型別、產生事件的物件、事件的回撥函式和引數。

iodev_t裝置基於各種硬體裝置的R/W狀態變動,觸發ioevent_t事件的產生,而iotimer_t定時器根據設定的定時時間,當指定時間超時,就觸發超時Timeout事件。

此外,應用程式可以註冊使用者鉤子(Hook)事件,註冊的使用者鉤子(Hook)事件需要繫結Callback回撥函式和回撥引數,最主要的是定義使用者事件觸發條件。

各種條件下產生的這些事件,都會被派送到工作執行緒的事件佇列,驅動工作執行緒來進行事件處理,或者啟用相應的回撥函式來處理事件。

3.2 ePump多執行緒架構

ePump架構是由多執行緒來構成的,按照工作流程,這些執行緒分成兩類,一類是ePump執行緒,另一類是worker執行緒。ePump執行緒職能主要是負責監聽檔案描述的R/W讀寫狀態和定時器佇列,建立讀寫事件和定時器事件,並將ioevent_t事件派發到各個worker執行緒的事件佇列中。worker執行緒職能是監聽事件佇列,執行事件佇列中各個事件關聯的回撥函式。

每個ePump執行緒採用I/O事件非同步通知、非阻塞通訊、多路複用等機制和模型,利用select/poll/epoll等系統呼叫,當被監聽的檔案描述符處於I/O讀寫就緒(I/O Readiness)時,ePump就會建立針對這些檔案描述符的R/W讀寫事件,將這些R/W讀寫事件包裝成ePump框架中標準的ioevent_t事件,並將其派發到各個worker工作執行緒的FIFO事件佇列中。這些Event Queue FIFO事件佇列是執行緒事件驅動模型的核心,每個ePump執行緒和每個worker執行緒都有一個這樣的FIFO事件佇列。此外,ePump執行緒還要維持並處理定時器佇列,當定時器超時時,建立定時器超時ioevent_t事件,派發到相應的worker工作執行緒的事件佇列中。

worker執行緒的主要職能就是阻塞等待該執行緒繫結的事件佇列,當有事件到達時,透過喚醒機制,喚醒處於掛起狀態的worker執行緒,被喚醒的工作執行緒將從其FIFO事件佇列中,逐個地、迴圈地取走並處理事件佇列的ioevent_t事件。

ioevent_t事件的處理流程基於回撥函式註冊機制,應用層在建立或開啟檔案描述符FD,或啟動定時器時,將該FD對應的iodev_t裝置和定時器例項,註冊並繫結回撥函式。這樣,ePump框架中iodev_t裝置、iotimer_t定時器等在建立ioevent_t事件時,都會將其註冊繫結的回撥函式和回撥引數,設定到ioevent_t事件中。各worker工作執行緒從事件佇列中獲取到ioevent_t事件後,執行其設定的回撥函式即可。

ePump執行緒除了監聽檔案描述符FD對應的iodev_t裝置、管理iotimer_t定時器佇列、建立ioevent_t事件、派發ioevent_t事件到各個事件佇列外,也可以繫結一個FIFO事件佇列,並以呼叫事件回撥函式的方式處理事件佇列的事件。

為了保證工作效率,ePump架構的執行緒總數,即包括ePump執行緒和worker工作執行緒,最好為CPU的Core Processor數量,這樣能確保完全並行處理。

四. ePump框架工作模型

先定義清楚什麼是快業務和慢業務。快業務是指接收到客戶端的請求後,其業務處理過程相對簡單快速,沒有長時間阻塞和等待的業務處理流程;相反,慢業務則是指在處理客戶端的請求時,需要較長時間的阻塞和等待,如存在資料庫慢查詢、慢插入的業務流程等。

ePump框架結構非常靈活,基於業務情況,可分成兩類工作模型:

4.1 快業務模型 -- 沒有worker執行緒,只有ePump執行緒ePump執行緒既負責iodev_t和iotimer_t的監聽、ioevent_t事件的建立和分發,同時還可以充當工作執行緒的職能,處理其FIFO事件佇列中的ioevent_t事件。類似這個工作模型的應用系統是Nginx Web伺服器。這個模型最大的缺點是:一旦透過呼叫回撥函式處理事件期間,出現慢業務情況,即長時間等待或阻塞等,譬如讀寫資料庫時,長時間阻塞等待查詢結果等,就會導致後續其他的iodev_t裝置中的檔案描述FD的I/O就緒(Readiness)狀態,及iotimer_t定時器超時狀態,不能被及時有效地處理。一個事件的處理延遲,會導致大量其他iodev_t裝置的狀態變化、或定時器的超時等得不到及時快速的處理,從而產生總體處理上的延遲、阻塞、甚至沒有響應或者崩潰。針對這類慢業務,採用類似於Apache Web伺服器那種獨佔式程序/執行緒架構模型比較適合,但總體來說,這種獨佔式程序/執行緒模型,對多核CPU並行處理能力的利用效率非常低下,併發數量較低。該模型最大的好處是:對多核CPU平行計算和處理的利用效率可達到極致,適合處理那種需要快速響應型的通訊或業務系統。4.2 複合業務模型 -- 少數ePump執行緒,大多數worker執行緒ePump執行緒只負責iodev_t和iotimer_t的監聽、ioevent_t事件的建立和分發,不負責處理事件。worker執行緒負責處理所有產生的ioevent_t事件,呼叫這些事件的回撥函式,從而處理應用層業務流程。worker執行緒執行上層應用註冊的回撥函式時,執行過程的阻塞並不會癱瘓其他iodev_t裝置或定時器等的事件,能確保其他裝置或定時器事件能透過其他worker工作執行緒進行及時有效的處理。這種模型的好處是可以一定程度很好地解決了慢業務類應用的需求,同時非常高效地利用多核CPU的平行計算處理能力。使用ePump框架的複合業務模型時,執行緒總數建議為CPU的Core Processor的數量,其中ePump執行緒數量為CPU Core總數的10-20%,worker執行緒數量為CPU Core總數的80-90%。譬如CPU為32核的伺服器,執行ePump架構開發的程式時,ePump執行緒數配置為3-6個,worker工作執行緒數配置為26-29個。五. ePump框架中的檔案描述符FD

在Unix、Linux作業系統中,將一切與I/O讀寫相關的物理裝置或虛擬裝置都看作是檔案,包括普通檔案,目錄,字元裝置檔案(如鍵盤、滑鼠),塊裝置檔案(如硬碟、光碟機),網路套接字Socket等,均抽象成檔案。檔案描述符是作業系統核心kernel管理被開啟的檔案結構而分配的索引,是一個整型數值。核心為每個程序維護一個檔案描述表,針對該表的索引即檔案描述符fd從0開始,0為標準輸入,1為標準輸出,2為標準錯誤輸出。在程序中開啟的每個檔案,都會分配一個檔案描述符fd,來對應到該程序的檔案描述表某個索引項中,透過fd來讀寫和訪問檔案。

預設地,一個程序開啟的檔案描述符總數是有限制的,Linux系統,這種限制包括兩個方面,使用者級限制和核心級限制。核心級限制是受限於硬體資源和作業系統處理的I/O能力,而制定的一個所有使用者程序總計能開啟的最大檔案描述符總數,可以用shell命令:

sysctl -a | grep file

cat /proc/sys/fs/file-max

檢視核心級限制。由於系統核心同時開啟的檔案總數有限制,對每個使用者和程序相應地限制開啟的檔案最大數量,這個是使用者級的限制,這個數量預設一般是1024,即預設情況下,程序能開啟的檔案描述符總數是1024。

ePump系統在初始化時,把開啟的檔案描述符總數作為初始化輸入引數,透過系統呼叫setrlimit來修改,以提高包括網路socket在內的檔案描述符總數,從而提升系統最大I/O併發處理能力。

ePump框架對檔案描述符進行了封裝,採用iodev_t資料結構來管理每一個檔案描述符,將檔案描述符、型別、回撥函式、四元組地址、讀寫狀態、關聯執行緒等資訊統一封裝管理,ePump執行緒負責對iodev_t裝置的I/O讀寫狀態進行監聽,一旦收到I/O讀寫就緒通知(Readiness Notification)就建立ioevent_t事件,不同的I/O讀寫狀態,就會建立不同的事件,透過對這些事件註冊不同的回撥函式,來實現事件驅動模型的處理閉環。

針對檔案描述符的各種不同的I/O讀寫狀態,ePump架構中定義了多種檔案描述符型別:

#define FDT_LISTEN                          0x01#define FDT_CONNECTED                0x02#define FDT_ACCEPTED                    0x04#define FDT_UDPSRV                        0x08#define FDT_UDPCLI                         0x10#define FDT_USOCK_LISTEN             0x20#define FDT_USOCK_CONNECTED   0x40#define FDT_USOCK_ACCEPTED       0x80#define FDT_RAWSOCK                    0x100#define FDT_FILEDEV                        0x200#define FDT_TIMER                           0x10000#define FDT_USERCMD                    0x20000#define FDT_LINGER_CLOSE             0x40000#define FDT_STDIN                          0x100000#define FDT_STDOUT                       0x200000

基於檔案描述符構建的iodev_t裝置是ePump框架最基礎的物理設施,本質上說,ePump就是一個管理檔案描述符的系統。檔案描述符產生的事件就像血液一樣驅動運轉整個ePump框架。

六. ePump框架的回撥(Call Back)機制

根據業務邏輯,軟體模組一般採用分層模型,不同的模組之間一般透過函式介面來相互呼叫,但在分層邏輯中下層模組通常作為基礎能力設施,譬如進行運算、I/O讀寫等功能,提供函式呼叫介面給上層模組,上層模組透過下層模組的介面函式來使用其運算、讀寫等功能。作為底層支撐模組,下層模組如何呼叫上層模組的函式功能呢?這就是回撥(CallBack)機制。

ePump框架作為底層基礎設施,給不同的業務系統提供功能支撐,業務系統的流程實現紛繁複雜,透過回撥(Callback)機制,將實現上層業務系統的函式指標註冊到ePump框架的檔案描述符裝置或定時器中,當ePump監聽到裝置和定時器的I/O讀寫狀態、定時器超時狀態發生變化時,透過事件驅動模型,執行上層系統註冊到發生狀態變化的裝置和定時器的回撥函式,從而運用ePump底層多執行緒CPU並行處理運算處理能力來解決複雜的業務流程的目的。

ePump的回撥(CallBack)機制封裝在ePump對上層模組提供的介面函式中,在ePump的介面函式中,一般包含有需要傳入的函式指標,這個函式指標指向的是上層業務函式,它就是ePump的回撥函式,回撥函式的原型定義如下:

typedef int IOHandler (void * vmgmt, void * pobj, int event, int fdtype);

第一個引數由上層模組ePump介面函式的引數傳入,第二引數pobj、第三個引數event、第四個引數fdtype,是ePump回撥返回時傳遞的引數。其中

pobj 是ePump產生I/O讀寫就緒ready時的iodev_t裝置物件或者iotimer_t定時器物件event 是事件型別fdtype 是檔案描述符型別

ePump中管理的iodev_t裝置物件和iotime_t定時器物件,在狀態發生變化時,ePump會產生相應的事件,這些事件型別如下:

/* event types include getting connected, connection accepted, readable,     * writable, timeout. the working threads will be driven by these events */    #define IOE_CONNECTED        1    #define IOE_CONNFAIL         2    #define IOE_ACCEPT           3    #define IOE_READ             4    #define IOE_WRITE            5    #define IOE_INVALID_DEV      6    #define IOE_TIMEOUT          100    #define IOE_USER_DEFINED     10000

ePump對上層提供的基本介面函式如下:

void * eptcp_listen  (void * vpcore, char * localip, int port, void * para, int * retval,                      IOHandler * cb, void * cbpara, int bindtype);void * eptcp_mlisten (void * vpcore, char * localip, int port, void * para,                      IOHandler * cb, void * cbpara);void * eptcp_accept  (void * vpcore, void * vld, void * para, int * retval,                      IOHandler * cb, void * cbpara, int bindtype);void * eptcp_connect (void * vpcore, struct in_addr ip, int port, char * localip, int localport,                      void * para, int * retval, IOHandler * cb, void * cbpara); void * epudp_listen (void * pcore, char * lip, int port, void * para, int * ret, IOHandler * cb, void * cbp);void * epudp_client (void * pcore, char * lip, int port, void * para, int * ret, IOHandler * cb, void * cbp); void * epusock_connect (void * pcore, char * sock, void * para, int * ret, IOHandler * cb, void * cbp);void * epusock_listen  (void * pcore, char * sock, void * para, int * ret, IOHandler * cb, void * cbp);void * epusock_accept  (void * pcore, void * vld, void * para, int * ret, IOHandler * cb, void * cbp); void * epfile_bind_fd    (void * pcore, int fd, void * para, IOHandler * cb, void * cbp);void * epfile_bind_stdin (void * pcore, void * para, IOHandler * cb, void * cbp); void * iotimer_start (void * pcore, int ms, int cmdid, void * para, IOHandler * cb, void * cbp);int    iotimer_stop  (void * viot);

ePump框架提供的功能介面函式涵蓋了TCP、UDP、Unix Socket等通訊設施所產生的檔案描述符事件監聽,和定時器事件的監聽。對於除了TCP、UDP、Unix Socket之外的檔案描述符,可以使用epfile_bind_fd介面來建立並繫結檔案描述符裝置,這樣可以擴充套件到任意檔案描述符FD都可以加入到ePump架構中進行管理和事件驅動。

七. ePump框架的排程(Scheduling)機制

排程(scheduling)是按照一定的機制和演算法對相關資源進行分配的過程,ePump框架的資源主要是iodev_t裝置、iodev_t定時器、ioevent_t事件、ePump執行緒、worker工作執行緒,排程機制也是圍繞這些資源的分派來設計。

7.1 iodev_t裝置繫結ePump執行緒

透過各種應用介面建立iodev_t裝置後,需要選擇一個ePump執行緒來執行該裝置的監聽和就緒通知(Readiness Notification),並將當前iodev_t裝置和選擇的ePump執行緒建立繫結關係,有繫結的ePump執行緒來監聽和產生各種R/W事件。如何分配ePump執行緒需要取決於iodev_t的裝置型別和繫結型別。

7.1.1 Listen服務埠類的iodev_t裝置

需要所有ePump執行緒都繫結該iodev_t裝置,或對於支援SO_REUSEPORT Socket選項的作業系統,需要為每一個ePump執行緒在同一個主機、同一個Listen埠上建立多個iodev_t Listen裝置,並繫結到該ePump執行緒中。這樣做的目的是確保當有客戶端網路連線請求時,所有ePump執行緒都能均衡地平分負載。當然,對於Linux核心版本低於3.9.x的系統,存在驚群效應,如何處理請參見第8.3.2節。

7.1.2 非Listen的iodev_t裝置指定ePump執行緒根據呼叫引數指定的ePump執行緒來建立繫結關係。根據ePump執行緒的最低負載ePump的負載主要是該執行緒繫結的iodev_t裝置數量、iotimer_t定時器數量、該執行緒最近單位時間內產生的ioevent_t數量等指標來衡量,選擇最低負載的ePump執行緒,可以讓負載均衡地分攤到各個ePump執行緒中,從提升系統工作效率。7.2 iotimer_t定時器

應用程式啟動iotimer_t定時器時,ePump框架一般根據ePump執行緒的當前負載,選擇負載最低的ePump執行緒來繫結,由繫結的ePump執行緒來管理和監控,並負責產生超時事件。

繫結ePump執行緒一般是將iotimer_t定時器物件新增到該ePump執行緒的管理定時器列表的紅黑樹結構中,如果當前ePump執行緒處於阻塞掛起狀態,透過啟用機制喚醒當前ePump執行緒,並基於定時器樹型結構中離當前時刻最短時長來重新啟動系統呼叫。

7.3 ioevent_t事件

除了使用者事件外,基本所有ioevent_t事件都由ePump執行緒產生,當然也由ePump執行緒根據相關機制和演算法來排程,將其派發到worker工作執行緒或ePump執行緒的FIFO事件佇列中,進行事件回撥函式的呼叫處理。ioevent_t事件的壽命週期較短,即被建立、被分派到事件佇列、被執行緒執行其回撥函式、執行完畢,其例項物件會被回收而結束壽命。

ioevent_t事件一般都綁定了某個iodev_t裝置或iotimer_t定時器,當前ioevent_t事件派發排程到哪一個worker執行緒,直接決定ePump框架系統執行的效率。

同一個iodev_t裝置連續產生的基本相同的ioevent_t事件則會被排程機制拋棄。

7.4 ePump執行緒

ePump執行緒是ePump框架的核心設施,負責對iodev_t裝置列表和iotimer_t定時器列表進行管理,透過epoll_wait或select等系統呼叫,阻塞等待裝置R/W就緒通知或定時器超時,併產生ioevent_t事件,負責對這些事件進行排程派發。

早期的eProbe框架中,只有一個全域性的FIFO事件佇列,產生的所有事件都新增到事件佇列尾部,然後所有空閒的worker工作執行緒都來爭搶FIFO佇列事件並進行處理。這個排程模型簡單,能均衡地分配工作任務,一旦某個事件處理過程中堵塞時,並不影響其他事件的處理。但由於FIFO佇列的共享鎖、廣播式喚醒所有工作執行緒的驚群效應、同一個裝置的連續產生的多個事件派發到不同執行緒處理等等因素,嚴重降低CPU處理效率,甚至會產生不可預知的故障等問題。

改進版的ePump框架是在每個worker工作執行緒和ePump執行緒中,配置一個獨立的FIFO事件佇列,這些執行緒也只從自己的FIFO佇列中獲取事件並處理事件。ePump執行緒將產生的每一個事件排程分發到這些執行緒的FIFO事件佇列中,並呼叫啟用機制,喚醒當前處於掛起狀態的執行緒。當然如果存在worker工作執行緒,則排程機制將優先把ioevent_t事件分配給工作執行緒。

ePump執行緒排程派發ioevent_t事件的演算法流程如下:

事件排程的基礎演算法是低負載優先演算法,即選擇當前負載最低的worker工作執行緒,並將事件派發到該執行緒的事件佇列中。對於同一個iodev_t裝置產生的後續所有ioevent_t事件,都會以pipeline方式排程到同一個worker執行緒中。對於同一個iodev_t裝置連續產生的同一型別的ioevent_t事件,如果還在同一個worker工作執行緒的FIFO事件佇列中,尚未被取走執行,那麼後續的這樣同裝置同類型事件就會被拋棄。由哪個worker執行緒啟動的iotimer_t定時器,其超時事件最終仍然由該worker工作執行緒處理。如果ePump框架中沒有啟動worker工作執行緒,則選擇當前負載最低的ePump執行緒,並將事件派發到該執行緒的事件佇列。

對於大規模即時訊息通訊系統,單臺伺服器可能會同時維持30萬甚至更大規模的TCP併發連線,每個連線隨時會產生讀寫事件進行資料收發處理操作。ePump框架的多個ePump執行緒可以均衡分散式地分擔30萬個iodev_t裝置,這些裝置產生的事件,也很快地均衡排程到各個worker工作執行緒中,沒有共享鎖造成的衝突,相同裝置產生的事件都以pipeline方式在同一個執行緒處理,規避了多執行緒爭搶裝置資源的衝突訪問問題,也迴避了一個執行緒關閉釋放了iodev_t裝置資源、另外一個執行緒還在使用該資源的異常故障問題。

7.5 worker工作執行緒

ePump框架中,worker工作執行緒是處理ioevent_t事件的主要載體,基本流程是迴圈地提取FIFO事件佇列中的事件,執行該事件中的回撥函式,處理完後釋放該ioevent_t事件物件,繼續讀取下一個ioevent_t事件進行處理,直到處理完全部事件後,透過非同步通知的條件變數進行阻塞等待新事件的到來。

worker工作執行緒的實時負載是ePump排程演算法的主要變數,負載的計算依賴於如下幾個因子:

當前工作執行緒事件佇列中的排隊等候的ioevent_t事件數量,佔所有執行緒事件佇列中的ioevent_t事件總數的百分比,其權重為600;當前工作執行緒在單位時間內佔用CPU進行處理的時間比例,其權重為300;當前工作執行緒累計處理事件數量,佔所有事件總數的比例,其權重為100;

根據以上因子的百分數值和權重比例,實時計算得出的值即為worker工作執行緒的負載。

ePump執行緒的事件排程派發機制主要依賴於工作執行緒的負載,即低負載優先演算法。運用這種演算法的最終結果是多個工作執行緒終將平衡地承擔系統中的所有處理任務。

八. ePump框架中驚群效應的處理機制8.1 驚群效應(Thundering Herd Problem)

驚群效應是指多程序(多執行緒)在同時阻塞等待同一個事件的時候(休眠狀態),如果等待的這個事件發生,那麼他就會喚醒等待的所有程序(或者執行緒),但是最終卻只能有一個程序(執行緒)獲得這個時間的“控制權”,對該事件進行處理,而其他程序(執行緒)獲取“控制權”失敗,只能重新進入休眠狀態,這種現象和效能浪費就叫做驚群效應。

8.2 驚群效應消耗什麼?

作業系統核心對使用者程序(執行緒)頻繁地做無效的排程、上下文切換等任務,會使系統性能大打折扣。上下文切換(context switch)過高會導致 CPU 頻繁地在暫存器和執行佇列之間奔波,更多的時間花在了程序(執行緒)切換,而不是在真正工作的程序(執行緒)上面。直接的消耗包括 CPU 暫存器要儲存和載入(例如程式計數器)、系統排程器的程式碼需要執行。間接的消耗在於多核 cache 之間的共享資料。

8.3 ePump框架中存在的驚群問題

不像libevent框架沒有設計程序或執行緒,只定義了介面呼叫,將程序和執行緒的使用交給了應用程式來處理。ePump框架採用了多執行緒(未來版本將支援多程序)來產生和處理各種事件。使用多程序或多執行緒的系統,由於爭搶共同資源,多少都會存在程序或執行緒的驚群問題。

8.3.1 worker執行緒組不存在驚群問題

ePump框架中,為每個worker工作執行緒單獨設計了接收和處理事件的FIFO佇列,單個worker工作執行緒在沒有事件處理時,阻塞掛起並等候FIFO佇列的條件變數核心物件上,直至有新事件新增到FIFO佇列後,被條件變數核心物件喚醒。

worker執行緒組沒有共享一個大FIFO事件佇列,這樣新新增的事件並不會喚醒所有處於休眠的worker工作執行緒,而是直接由ePump執行緒像郵遞員一樣,將信件送達家庭郵箱中,也即是新增加的事件會由ePump執行緒排程派發到某個worker工作執行緒的FIFO事件佇列中,並直接喚醒該執行緒,worker工作執行緒組中的其他執行緒就不會接受到喚醒指令。

這種方式徹底規避了worker執行緒組的驚群效應,提升了系統排程效率和CPU的利用率。

8.3.2 ePump執行緒組的驚群問題

ePump框架中的ePump執行緒都阻塞掛起在I/O事件通知的系統呼叫上,如select、poll、epoll_wait等,等候檔案描述符的R/W就緒狀態,或等待定時時間超時,處於阻塞掛起狀態的ePump執行緒,被喚醒的條件只有兩類:

一是檔案描述符可讀(readable)或可寫(writable)二是設定的timeout時間到期了

如果有一個iodev_t裝置的檔案描述符被所有的ePump執行緒都監聽(monitor)了,當這個裝置有R/W Readiness讀寫就緒時,所有的ePump執行緒就會被喚醒,所有被喚醒的執行緒將去搶奪該檔案描述符的處理權,當然最終也只有一個執行緒能取得處理該裝置R/W事件的許可權,這樣就造成了ePump執行緒的驚群效應。

ePump框架中確實存在一種iodev_t裝置型別,就是監聽某個服務埠的Listen裝置,如用TCP Listen或UDP Listen監聽某個服務埠時,建立的iodev_t裝置就是需要繫結所有的ePump執行緒,讓所有ePump執行緒對該裝置進行R/W狀態監控處理。這樣處理的目的是將不同終端使用者對該埠服務的請求能夠均衡地分配到不同的ePump執行緒中,如果不做均衡處理,將會導致某一個ePump執行緒非常繁忙,而其他ePump執行緒則過分清閒的狀態。

所有ePump執行緒都繫結監聽埠服務的iodev_t裝置,有兩種情形需要分別處理。

1. 作業系統核心支援SO_REUSEPORT Socket選項情況支援SO_REUSEPORT Socket選項的作業系統,如核心版本高於3.9.x的Linux系統,可以建立多個Socket繫結到同一個IP地址的同一個埠上,並將該Socket分別用不同的程序或執行緒來監聽客戶端的連線請求。當客戶端的TCP三路握手成功後,核心就會均衡地將當前連線請求交給某一個執行緒來accept,從核心層面解決了多個執行緒在該Socket檔案描述符R/W狀態為連線就緒時,爭搶處理權的競爭問題。ePump框架中,如果判斷作業系統支援SO_REUSEPORT Socket選項,呼叫tcp_mlisten或udp_mlisten等介面針對同一個監聽埠,為每一個ePump執行緒單獨建立一個Listen iodev_t裝置,並建立繫結關係。這樣每個ePump執行緒都會監聽這一個埠,接收客戶端請求,並處理這些請求。基於以上處理過程,當客戶端發起連線請求後,監聽該埠服務的ePump執行緒組中只有一個執行緒才會被啟用,其他ePump執行緒並不會接收到R/W Readiness Notification事件通知。

2. 作業系統核心不支援SO_REUSEPORT選項情況

ePump框架中,如果作業系統核心不支援SO_REUSEPORT Socket選項,監聽某個服務埠時,系統只需要建立一個監聽Socket的iodev_t裝置,並將該Listen iodev_t裝置繫結到所有的ePump執行緒中;iodev_t裝置中內建一個共享鎖,當有客戶端請求到來時,所有ePump執行緒都會收到核心發起的R/W Readiness Notification就緒通知,所有ePump執行緒都會被喚醒,所有執行緒都爭奪處理該客戶請求,採用共享鎖確保只有一個ePump執行緒能夠獲得該客戶請求的處理。這種情況就是典型的驚群效應。8.3.3 規避或弱化ePump框架驚群問題的措施儘量使用支援SO_REUSEPORT選項的OS版本。支援SO_REUSEPORT選項的作業系統,會徹底解決ePump執行緒組的驚群問題。儘量使用ePump框架的複合業務模型,即ePump執行緒數量較少,worker工作執行緒數量較多,當監聽埠有讀寫請求時,ePump執行緒數量越少,驚群問題的負面效果也就越低,當然這需要在處理使用者併發請求之間尋找平衡。

14
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 6位數字支付密碼自動完成元件