異步網絡模型
異步網絡模型在服務開發中應用非常廣泛,相關資料和開源庫也非常多。項目中,使用現成的輪子提高了開發效率,除了能使用輪子,還是有必要了解一下輪子的內部構造。
這篇文章從最基礎的5種I/O模型切入,到I/O事件處理模型,再到併發模式,最後以Swoole開源庫來做具體分析,逐步深入。文中提到的模型都是一些通用的模型,在《linux高性能服務器編程》中也都有涉及。文章不涉及模型的實現細節,最重要的是去理解各個模型的工作模式以及其優缺點。
文中涉及接口調用的部分,都是指Linux系統的接口調用。 共分爲5部分:
I/O模型
從基礎的系統調用方法出發,給大家從頭回顧一下最基本的I/O模型,雖然簡單,但是不可或缺的基礎;
事件處理模型
這部分在同步I/O、異步I/O的基礎上分別介紹Reactor模型以及Proactor模型,着重兩種模型的構成以及事件處理流程。Reactor模型是我們常見的;不同平臺對異步I/O系統接口的支持力度不同,這部分還介紹了一種使用同步I/O來模擬Proactor模型的方法。
併發模式
就是多線程、多進程的編程的模式。介紹了兩種較爲高效的併發模型,半同步/半異步(包括其演變模式)、Follower/Leader模式。
Swoole異步網絡模型分析
這部分是結合已介紹的事件處理模型、併發模式對Swoole的異步模型進行分析; 從分析的過程來看,看似複雜的網絡模型,可以拆分爲簡單的模型單元,只不過我們需要權衡利弊,選取合適業務需求的模型單元進行組合。 我們團隊基於Swoole 1.8.5版本,做了很多修改,部分模塊做了重構,計劃在17年6月底將修改後版本開源出去,敬請期待。
改善性能的方法
最後一部分是在引入話題,介紹的是幾種常用的方法。性能優化是沒有終點的,希望大家能貢獻一些想法和具體方法。
I/O模型
POSIX 規範中定義了同步I/O 和異步I/O的術語,
同步I/O : 需要進程去真正的去操作I/O;
異步I/O:內核在I/O操作完成後再通知應用進程操作結果。
在《UNIX網絡編程》中介紹了5中I/O模型:阻塞I/O、非阻塞I/O、I/O複用、SIGIO 、異步I/O;本節對這5種I/O模型進行說明和對比。
I/O阻塞
通常把阻塞的文件描述符(file descriptor,fd)稱之爲阻塞I/O。默認條件下,創建的socket fd是阻塞的,針對阻塞I/O調用系統接口,可能因爲等待的事件沒有到達而被系統掛起,直到等待的事件觸發調用接口才返回,例如,tcp socket的connect調用會阻塞至第三次握手成功(不考慮socket 出錯或系統中斷),如圖1所示。另外socket 的系統API ,如,accept、send、recv等都可能被阻塞。
圖1 I/O 阻塞模型示意圖
另外補充一點,網絡編程中,通常把可能永遠阻塞的系統API調用 稱爲慢系統調用,典型的如 accept、recv、select等。慢系統調用在阻塞期間可能被信號中斷而返回錯誤,相應的errno 被設置爲EINTR,我們需要處理這種錯誤,解決辦法有:
1. 重啓系統調用
直接上示例代碼吧,以accept爲例,被中斷後重啓accept 。有個例外,若connect 系統調用在阻塞時被中斷,是不能直接重啓的(與內核socket 的狀態有關),有興趣的同學可以深入研究一下connect 的內核實現。使用I/O複用等待連接完成,能避免connect不能重啓的問題。
int client_fd = -1;
struct sockaddr_in client_addr;
socklen_t child_addrlen;
while (1) {
call_accept:
client_fd = accept(server_fd,NULL,NULL);
if (client_fd < 0) {
if (EINTR == errno) {
goto call_accept;
} else {
sw_sysError("accept fail");
break;
}
}
}
2. 信號處理
利用信號處理,可以選擇忽略信號,或者在安裝信號時設置SA_RESTART屬性。設置屬性SA_RESTART,信號處理函數返回後,被安裝信號中斷的系統調用將自動恢復,示例代碼如下。需要知道的是,設置SA_RESTART屬性方法並不完全適用,對某些系統調用可能無效,這裏只是提供一種解決問題的思路,示例代碼如下:
int client_fd = -1;
struct sigaction action,old_action;
action.sa_handler = sig_handler;
sigemptyset(&action.sa_mask);
action.sa_flags = 0;
action.sa_flags |= SA_RESTART;
/// 若信號已經被忽略,則不設置
sigaction(SIGALRM, NULL, &old_action);
if (old_action.sa_handler != SIG_IGN) {
sigaction(SIGALRM, &action, NULL);
}
while (1) {
client_fd = accept(server_fd,NULL,NULL);
if (client_fd < 0) {
sw_sysError("accept fail");
break;
}
}
I/O非阻塞
把非阻塞的文件描述符稱爲非阻塞I/O。可以通過設置SOCK_NONBLOCK標記創建非阻塞的socket fd,或者使用fcntl將fd設置爲非阻塞。
對非阻塞fd調用系統接口時,不需要等待事件發生而立即返回,事件沒有發生,接口返回-1,此時需要通過errno的值來區分是否出錯,有過網絡編程的經驗的應該都瞭解這點。不同的接口,立即返回時的errno值不盡相同,如,recv、send、accept errno通常被設置爲EAGIN 或者EWOULDBLOCK,connect 則爲EINPRO- GRESS 。
以recv操作非阻塞套接字爲例,如圖2所示。
圖2 非阻塞I/O模型示意圖
當我們需要讀取,在有數據可讀的事件觸發時,再調用recv,避免應用層不斷去輪詢檢查是否可讀,提高程序的處理效率。通常非阻塞I/O與I/O事件處理機制結合使用。
I/O複用
最常用的I/O事件通知機制就是I/O複用(I/O multiplexing)。Linux 環境中使用select/poll/epoll 實現I/O複用,I/O複用接口本身是阻塞的,在應用程序中通過I/O複用接口向內核註冊fd所關注的事件,當關注事件觸發時,通過I/O複用接口的返回值通知到應用程序,如圖3所示,以recv爲例。I/O複用接口可以同時監聽多個I/O事件以提高事件處理效率。
圖 3 I/O複用模型示意圖
關於select/poll/epoll的對比,可以參考[],epoll使用比較多,但是在併發的模式下,需要關注驚羣的影響。
SIGIO
除了I/O複用方式通知I/O事件,還可以通過SIGIO信號來通知I/O事件,如圖4所示。兩者不同的是,在等待數據達到期間,I/O複用是會阻塞應用程序,而SIGIO方式是不會阻塞應用程序的。
圖 4 信號驅動I/O模型示意圖
異步I/O
POSIX規範定義了一組異步操作I/O的接口,不用關心fd 是阻塞還是非阻塞,異步I/O是由內核接管應用層對fd的I/O操作。異步I/O嚮應用層通知I/O操作完成的事件,這與前面介紹的I/O 複用模型、SIGIO模型通知事件就緒的方式明顯不同。以aio_read 實現異步讀取IO數據爲例,如圖5所示,在等待I/O操作完成期間,不會阻塞應用程序。
圖 5 異步I/O 模型示意圖
I/O模型對比
前面介紹的5中I/O中,I/O 阻塞、I/O非阻塞、I/O複用、SIGIO 都會在不同程度上阻塞應用程序,而只有異步I/O模型在整個操作期間都不會阻塞應用程序。
如圖6所示,列出了5種I/O模型的比較
圖6 五種I/O 模型比較示意圖
事件處理模型
網絡設計模式中,如何處理各種I/O事件是其非常重要的一部分,Reactor 和Proactor兩種事件處理模型應運而生。上章節提到將I/O分爲同步I/O 和 異步I/O,可以使用同步I/O實現Reactor模型,使用異步I/O實現Proactor模型。
本章節將介紹Reactor和Proactor兩種模型,最後將介紹一種使用同步I/O模擬Proactor事件處理模型。
Reactor事件處理模型
Reactor模型是同步I/O事件處理的一種常見模型,關於Reactor模型結構的資料非常多,一個典型的Reactor模型類圖結構如圖7所示,
圖 7 Reactor 模型類結構圖
Reactor的核心思想:將關注的I/O事件註冊到多路複用器上,一旦有I/O事件觸發,將事件分發到事件處理器中,執行就緒I/O事件對應的處理函數中。模型中有三個重要的組件:
- 多路複用器:由操作系統提供接口,Linux提供的I/O複用接口有select、poll、epoll;
- 事件分離器:將多路複用器返回的就緒事件分發到事件處理器中;
- 事件處理器:處理就緒事件處理函數。
圖7所示,Reactor 類結構中包含有如下角色。
- Handle:標示文件描述符;
- Event Demultiplexer:執行多路事件分解操作,對操作系統內核實現I/O複用接口的封裝;用於阻塞等待發生在句柄集合上的一個或多個事件(如select/poll/epoll);
- Event Handler:事件處理接口;
- Event Handler A(B):實現應用程序所提供的特定事件處理邏輯;
- Reactor:反應器,定義一個接口,實現以下功能:
a)供應用程序註冊和刪除關注的事件句柄;
b)運行事件處理循環;
c)等待的就緒事件觸發,分發事件到之前註冊的回調函數上處理.
接下來介紹Reactor的工作流程,如圖8所示,爲Reactor模型工作的簡化流程。
圖8 Reactor模型簡化流程示意圖
- 註冊I/O就緒事件處理器;
- 事件分離器等待I/O就緒事件;
- I/O事件觸發,激活事件分離器,分離器調度對應的事件處理器;
- 事件處理器完成I/O操作,處理數據.
網絡設計中,Reactor使用非常廣,在開源社區有很許多非常成熟的、跨平臺的、Reactor模型的網絡庫,比較典型如libevent。
Proactor事件處理模型
與Reactor不同的是,Proactor使用異步I/O系統接口將I/O操作託管給操作系統,Proactor模型中分發處理異步I/O完成事件,並調用相應的事件處理接口來處理業務邏輯。Proactor類結構如圖9所示。
圖9 Proactor模型類結構圖
圖9所示,Proactor類結構中包含有如下角色:
- Handle: 用來標識socket連接或是打開文件;
- Async Operation Processor:異步操作處理器;負責執行異步操作,一般由操作系統內核實現;
- Async Operation:異步操作;
- Completion Event Queue:完成事件隊列;異步操作完成的結果放到隊列中等待後續使用;
- Proactor:主動器;爲應用程序進程提供事件循環;從完成事件隊列中取出異步操作的結果,分發調用相應的後續處理邏輯;
- Completion Handler:完成事件接口;一般是由回調函數組成的接口;
- Completion Handler A(B):完成事件處理邏輯;實現接口定義特定的應用處理邏輯。
Proactor模型的簡化的工作流程,如圖10所示。
圖10 Proactor模型簡化工作流程示意圖
- 發起I/O異步操作,註冊I/O完成事件處理器;
- 事件分離器等待I/O操作完成事件;
- 內核並行執行實際的I/O操作,並將結果數據存入用戶自定義緩 衝區;
- 內核完成I/O操作,通知事件分離器,事件分離器調度對應的事件處理器;
- 事件處理器處理用戶自定義緩衝區中的數據。
Proactor利用異步I/O並行能力,可給應用程序帶來更高的效率,但是同時也增加了編程的複雜度。windows對異步I/O提供了非常好的支持,常用Proactor的模型實現服務器;而Linux對異步I/O操作(aio接口)的支持並不是特別理想,而且不能直接處理accept,因此Linux平臺上還是以Reactor模型爲主。
Boost asio採用的是Proactor模型,但是Linux上,採用I/O複用的方式來模擬Proactor,另啓用線程來完成讀寫操作和調度。
同步I/O模擬Proactor
下面一種使用同步I/O模擬Proactor的方案,原理是:
主線程執行數據讀寫操作,讀寫操作完成後,主線程向工作線程通知I/O操作“完成事件”;
工作流程如圖 11所示。
圖11 同步I/O模擬Proactor模型
簡單的描述一下圖11 的執行流程:
- 主線程往系統I/O複用中註冊文件描述符fd上的讀就緒事件;
- 主線程調用調用系統I/O複用接口等待文件描述符fd上有數據可讀;
- 當fd上有數據可讀時,通知主線程。主線程循環讀取fd上的數據,直到沒有更多數據可讀,然後將讀取到的數據封裝成一個請求對象並插入請求隊列。
- 睡眠在請求隊列上的某個工作線程被喚醒,它獲得請求對象並處理客戶請求,然後向I/O複用中註冊fd上的寫就緒事件。主線程進入事件等待循環,等待fd可寫。
併發模式
在I/O密集型的程序,採用併發方式可以提高CPU的使用率,可採用多進程和多線程兩種方式實現併發。當前有高效的兩種併發模式,半同步/半異步模式、Follower/Leader模式。
半同步/半異步模式
首先區分一個概念,併發模式中的“同步”、“異步”與 I/O模型中的“同步”、“異步”是兩個不同的概念:
併發模式中,“同步”指程序按照代碼順序執行,“異步”指程序依賴事件驅動,如圖12 所示併發模式的“同步”執行和“異步”執行的讀操作;
I/O模型中,“同步”、“異步”用來區分I/O操作的方式,是主動通過I/O操作拿到結果,還是由內核異步的返回操作結果。
圖12(a) 同步讀操作示意圖
圖12(b) 異步讀操作示意圖
本節從最簡單的半同步/半異步模式的工作流程出發,並結合事件處理模型介紹兩種演變的模式。
半同步/半異步工作流程
半同步/半異步模式的工作流程如圖13 所示。
圖13 半同步/半異步模式的工作流程示意圖
其中異步線程處理I/O事件,同步線程處理請求對象,簡單的來說:
- 異步線程監聽到事件後,將其封裝爲請求對象插入到請求隊列中;
- 請求隊列有新的請求對象,通知同步線程獲取請求對象;
- 同步線程處理請求對象,實現業務邏輯。
半同步/半反應堆模式
考慮將兩種事件處理模型,即Reactor和Proactor,與幾種I/O模型結合在一起,那麼半同步/半異步模式就演變爲半同步/半反應堆模式。先看看使用Reactor的方式,如圖14 所示。
圖14 半同步/半反應堆模式示意圖
其工作流程爲:
- 異步線程監聽所有fd上的I/O事件,若監聽socket接可讀,接受新的連接;並監聽該連接上的讀寫事件;
- 若連接socket上有讀寫事件發生,異步線程將該連接socket插入請求隊列中;
- 同步線程被喚醒,並接管連接socket,從socket上讀取請求和發送應答;
若將Reactor替換爲Proactor,那麼其工作流程爲:
- 異步線程完成I/O操作,並I/O操作的結果封裝爲任務對象,插入請求隊列中;
- 請求隊列通知同步線程處理任務;
- 同步線程執行任務處理邏輯。
一種高效的演變模式
半同步/半反應堆模式有明顯的缺點:
- 異步線程和同步線程共享隊列,需要保護,存在資源競爭;
- 工作線程同一時間只能處理一個任務,任務處理量很大或者任務處理存在一定的阻塞時,任務隊列將會堆積,任務的時效性也等不到保證;不能簡單地考慮增加工作線程來處理該問題,線程數達到一定的程度,工作線程的切換也將白白消耗大量的CPU資源。
下面介紹一種改進的方式,如圖15 所示,每個工作線程都有自己的事件循環,能同時獨立處理多個用戶連接。
圖 15 半同步/半反應堆模式的演變模式
其工作流程爲:
- 主線程實現連接監聽,只處理網絡I/O連接事件;
- 新的連接socket分發至工作線程中,這個socket上的I/O事件都由該工作線程處理,工作線程都可以處理多個socket 的I/O事件;
- 工作線程獨立維護自己的事件循環,監聽不同連接socket的I/O事件。
Follower/Leader 模式
Follower/Leader是多個工作線程輪流進行事件監聽、事件分發、處理事件的模式。
在Follower/Leader模式工作的任何一個時間點,只有一個工作線程處理成爲Leader ,負責I/O事件監聽,而其他線程都是Follower,並等待成爲Leader。
Follower/Leader模式的工作流概述如下:
- 當前Leader Thread1監聽到就緒事件後,從Follower 線程集中推選出 Thread 2成爲新的Leader;
- 新的Leader Thread2 繼續事件I/O監聽;
- Thread1繼續處理I/O就緒事件,執行完後加入到Follower 線程集中,等待成爲Leader。
從上描述,Leader/Follower模式的工作線程存在三種狀態,工作線程同一時間只能處於一種狀態,這三種狀態爲:
-
Leader:線程處於領導者狀態,負責監聽I/O事件;
-
Processing:線程處理就緒I/O事件;
-
Follower:等待成爲新的領導者或者可能被當前Leader指定處理就緒事件。
Leader監聽到I/O就緒事件後,有兩種處理方式:
- 推選出新的Leader後,並轉移到Processing處理該I/O就緒事件;
- 指定其他Follower 線程處理該I/O就緒事件,此時保持Leader狀態不變;
如圖16所示爲上面描述的三種狀態的轉移關係。
圖16 Follower/Leader模式狀態轉移示意圖
如圖16所示,處於Processing狀態的線程處理完I/O事件後,若當前不存在Leader,就自動提升爲Leader,否則轉變Follower。
從以上描述中可知,Follower/Leader模式中不需要在線程間傳遞數據,線程間也不存在共享資源。但很明顯Follower/Leader 僅支持一個事件處理源集,無法做到圖15所示的每個工作線程獨立監聽I/O事件。
Swoole 網絡模型分析
Swoole爲PHP提供I/O擴展功能,支持異步I/O、同步I/O、併發通信,並且爲PHP多進程模式提供了併發數據結構和IPC通信機制;Swoole 既可以充當網絡I/O服務器,也支持I/O客戶端,較大程度爲用戶簡化了網絡I/O、多進程/多線程併發編程的工作。
Swoole作爲server時,支持3種運行模式,分別是多進程模式、多線程模式、多進程+多線程模式;多進程+多線程模式是其中最爲複雜的方式,其他兩種方式可以認爲是其特例。
本節結合之前介紹幾種事件處理模型、併發模式來分析Swoole server的多進程+多線程模型,如圖17。
圖17 swoole server多進程+多線程模型結構示意圖
圖17所示,整體上可以分爲Master Process、Manger Process、Work Process Pool三部分。這三部分的主要功能:
- Master Process:監聽服務端口,接收用戶連接,收發連接數據,依靠reactor模型驅動;
- Manager Process:Master Process的子進程,負責fork WorkProcess,並監控Work Process的運行狀態;
- Work Process Pool:工作進程池,與PHP業務層交互,將客戶端數據或者事件(如連接關閉)回調給業務層,並將業務層的響應數據或者操作(如主動關閉連接)交給Master Process處理;工作進程依靠reactor模型驅動。
Manager Process 監控Work Process進程,本節不做進一步講解,主要關注Master和Work。
Master Process
Master Process 內部包括主線程(Main Thread)和工作線程池(Work Thread Pool),這兩部分主要功能分別是:
主線程: 監聽服務端口,接收網絡連接,將成功建立的連接分發到線程池中;依賴reactor模型驅動;
工作線程池: 獨立管理連接,收發網絡數據;依賴Reactor事件處理驅動。
顧一下前面介紹的半同步/半異步併發模式,很明顯,主進程的工作方式就是圖15所示的方式。
Work Process
如上所描述,Work Process是Master Process和PHP層之間的媒介:
- Work Process接收來自Master Process的數據,包括網絡數據和連接事件,回調至PHP業務層;
- 將來自PHP層的數據和連接控制信息發送給Master Process進程,Master Process來處理。
Work Process同樣是依賴Reactor事件模型驅動,其工作方式一個典型的Reactor模式。
Work Process作爲Master Process和PHP層之間的媒介,將數據收發操作和數據處理分離開來,即使PHP層因消息處理將Work進程阻塞一段時間,也不會對其他連接有影響。
從整體層面來看,Master Process實現對連接socket上數據的I/O操作,這個過程對於Work Process是異步的,結合圖11 所描述的同步I/O模擬Proactor模式,兩種方式如出一轍,只不過這裏使用的是多進程。
進程間通信
Work Process是Master Process和PHP層之間的媒介,那麼需要看看Work Process 與Master Process之間的通信方式,並在Swoole server 的多進程+多線程模型進程中,整個過程還是有些複雜,下面說明一下該流程,如圖18所示。
圖18 swoole server 多進程多線程通信示意圖
具體流程爲:
- Master 進程主線程接收客戶端連接,連接建立成功後,分發至工作線程,工作線程通過Unix Socket通知Work進程連接信息;
- Work 進程將連接信息回調至PHP業務層;
- Maser 進程中的工作線程接收客戶端請求消息,並通過Unix Socket方式發送到Work進程;
- Work 進程將請求消息回調至PHP業務層;
- PHP業務層構造回覆消息,通過Work進程發送,Work進程將回復消息拷貝至共享內存中,並通過Unix Socket通知發送至Master進程的工作線程有數據需要發送;
- 工作線程從共享內存中取出需發送的數據,併發送至客戶端;
- 客戶端斷開連接,工作線程將連接斷開的事件通過UnixSocket發送至Work進程;
- Work進程將連接斷開事件回調至PHP業務層.
需要注意在步驟5中,Work進程通知Master進程有數據需要發送,不是將數據直接發送給Master進程,而是將數據地址(在共享內存中)發送給Master進程。
改善性能的方法
性能對於服務器而言是非常敏感和重要的,當前,硬件的發展雖然不是服務器性能的瓶頸,作爲軟件開發人員還是應該考慮在軟件層面來上改善服務性能。好的網絡模塊,除了穩定性,還有非常多的細節、技巧處理來提升服務性能,感興趣的同學可以深入瞭解Ngnix源碼的細節,以及陳碩的《Linux多線程服務器編程》。
數據複製
如果應用程序不關心數據的內容,就沒有必要將數據拷貝到應用緩衝區,可以藉助內核接口直接將數據拷貝到內核緩衝區處理,如在提供文件下載服務時,不需要將文件內容先讀到應用緩衝區,在調用send接口發送出去,可以直接使用sendfile (零拷貝)接口直接發送出去。
應用程序的工作模塊之間也應該避免數據拷貝,如:
- 當兩個工作進程之間需要傳遞數據,可以考慮使用共享內存的方式實現數據共享;
- 在流媒體的應用中,對幀數據的非必要拷貝會對程序性能的影響,特備是在嵌入式環境中影響非常明顯。通常採用的辦法是,給每幀數據分配內存(下面統稱爲buffer),當需要使用該buffer時,會增加該buffer的引用計數,buffer的引用計數爲0時纔會釋放對應的內存。這種方式適合在進程內數據無拷貝傳遞,並且不會給釋放buffer帶來困擾。
資源池
在服務運行期間,需要使用系統調用爲用戶分配資源,通常系統資源的分配都是比較耗時的,如動態創建進程/線程。可以考慮在服務啓動時預先分配資源,即創建資源池,當需要資源,從資源池中獲取即可,若資源池不夠用時,再動態的分配,使用完成後交還到資源池中。這實際上是用空間換取時間,在服務運行期間可以節省非必要的資源創建過程。需要注意的是,使用資源池還需要根據業務和硬件環境對資源池的大小進行限制。
資源池是一個抽象的概念,常見的包括進程池、線程池、 內存池、連接池;這些資源池的相關資料非常多,這裏就不一一介紹了。
鎖/上下文切換
1.關於鎖
對共享資源的操作是併發程序中經常被提起的一個話題,都知道在業務邏輯上無法保證同步操作共享資源時,需要對共享資源加鎖保護,但是鎖不僅不能處理任何業務邏輯,而且還存在一定的系統開銷。並且對鎖的不恰當使用,可能成爲服務期性能的瓶頸。
針對鎖的使用有如下建議:
- 如果能夠在設計層面避免共享資源競爭,就可以避免鎖,如圖15描述的模式;
- 若無法避免對共享資源的競爭,優先考慮使用無鎖隊列的方式實現共享資源;
- 使用鎖時,優先考慮使用讀寫鎖;此外,鎖的範圍也要考慮,儘量較少鎖的顆粒度,避免其他線程無謂的等待。
2.上下文切換
併發程序需要考慮上下文切換的問題,內核調度線程(進程)執行是存在系統開銷的,若線程(進程)調度佔用CPU的時間比重過大,那處理業務邏輯佔用的CPU時間就會不足。在項目中,線程(進程)數量越多,上下文切換會很頻繁,因此是不建議爲每個用戶連接創建一個線程,如圖15所示的併發模式,一個線程可同時處理多個用戶連接,是比較合理的解決方案。
多核的機器上,併發程序的不同線程可以運行在不同的CPU上,只要線程數量不大於CPU數目,上下文切換不會有什麼問題,在實際的併發網絡模塊中,線程(進程)的個數也是根據CPU數目來確定的。在多核機器上,可以設置CPU親和性,將進程/線程與CPU綁定,提高CPU cache的命中率,建好內存訪問損耗。
有限狀態機器
有限狀態機是一種高效的邏輯處理方式,在網絡協議處理中應用非常廣泛,最典型的是內核協議棧中TCP狀態轉移。有限狀態機中每種類型對應執行邏輯單元的狀態,對邏輯事務的處理非常有效。 有限狀態機包括兩種,一種是每個狀態都是相互獨立的,狀態間不存在轉移;另一種就是狀態間存在轉移。有限狀態機比較容易理解,下面給出兩種有限狀態機的示例代碼。
不存在狀態轉移
typedef enum _tag_state_enum{
A_STATE,
B_STATE,
C_STATE,
D_STATE
}state_enum;
void STATE_MACHINE_HANDLER(state_enum cur_state) {
switch (cur_state){
case A_STATE:
process_A_STATE();
break;
case B_STATE:
process_B_STATE();
break;
case C_STATE:
process_C_STATE();
break;
default:
break;
}
return ;
}
存在狀態轉移
void TRANS_STATE_MACHINE_HANDLER(state_enum cur_state) {
while (C_STATE != cur_state) {
switch (cur_state) {
case A_STATE:
process_A_STATE();
cur_state = B_STATE;
break;
case B_STATE:
process_B_STATE();
cur_state = C_STATE;
break;
case C_STATE:
process_C_STATE();
cur_state = D_STATE;
break;
default:
return ;
}
}
return ;
}
時間輪
經常會面臨一些業務定時超時的需求,用例子來說明吧。
功能需求:服務器需要維護來自大量客戶端的TCP連接(假設單機服務器需要支持的最大TCP連接數在10W級別),如果某連接上60s內沒有數據到達,就認爲相應的客戶端下線。
先介紹一下兩種容易想到的解決方案,
方案a 輪詢掃描
處理過程爲:
- 維護一個map<client_id, last_update_time > 記錄客戶端最近一次的請求時間;
- 當client_id對應連接有數據到達時,更新last_update_time;
- 啓動一個定時器,輪詢掃描map 中client_id 對應的last_update_time,若超過 60s,則認爲對應的客戶端下線。
輪詢掃描,只啓動一個定時器,但輪詢效率低,特別是服務器維護的連接數很大時,部分連接超時事件得不到及時處理。
方案b 多定時器觸發
處理過程爲:
- 維護一個map<client_id, last_update_time > 記錄客戶端最近一次的請求時間;
- 當某client_id 對應連接有數據到達時,更新last_update_time,同時爲client_id啓用一個定時器,60s後觸發;
- 當client_id對應的定時器觸發後,查看map中client_id對應的last_update_time是否超過60s,若超時則認爲對應客戶端下線。
多定時器觸發,每次請求都要啓動一個定時器,可以想象,消息請求非常頻繁是,定時器的數量將會很龐大,消耗大量的系統資源。
方案c 時間輪方案
下面介紹一下利用時間輪的方式實現的一種高效、能批量的處理方案,先說一下需要的數據結構:
- 創建0~60的數據,構成環形隊列time_wheel,current_index維護環形隊列的當前遊標,如圖19所示;
- 數組元素是slot 結構,slot是一個set<client_id>,構成任務集;
- 維護一個map<client_id,index>,記錄client_id 落在哪個slot上。
圖19 時間輪環形隊列示意圖
執行過程爲:
- 啓用一個定時器,運行間隔1s,更新current_index,指向環形隊列下一個元素,0->1->2->3...->58->59->60...0;
- 連接上數據到達時,從map中獲取client_id所在的slot,在slot的set中刪除該client_id;
- 將client_id加入到current_index - 1鎖標記的slot中;
- 更新map中client_id 爲current_id-1 。
與a、b兩種方案相比,方案c具有如下優勢:
- 只需要一個定時器,運行間隔1s,CPU消耗非常少;
- current_index 所標記的slot中的set不爲空時,set中的所有client_id對應的客戶端均認爲下線,即批量超時。
上面描述的時間輪處理方式會存在1s以內的誤差,若考慮實時性,可以提高定時器的運行間隔,另外該方案可以根據實際業務需求擴展到應用中。我們對Swoole的修改中,包括對定時器進行了重構,其中超時定時器採用的就是如上所描述的時間輪方案,並且精度可控。