異步網絡模型

異步網絡模型

異步網絡模型在服務開發中應用非常廣泛,相關資料和開源庫也非常多。項目中,使用現成的輪子提高了開發效率,除了能使用輪子,還是有必要了解一下輪子的內部構造。

這篇文章從最基礎的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模型簡化流程示意圖
  1. 註冊I/O就緒事件處理器;
  2. 事件分離器等待I/O就緒事件;
  3. I/O事件觸發,激活事件分離器,分離器調度對應的事件處理器;
  4. 事件處理器完成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模型簡化工作流程示意圖
  1. 發起I/O異步操作,註冊I/O完成事件處理器;
  2. 事件分離器等待I/O操作完成事件;
  3. 內核並行執行實際的I/O操作,並將結果數據存入用戶自定義緩 衝區;
  4. 內核完成I/O操作,通知事件分離器,事件分離器調度對應的事件處理器;
  5. 事件處理器處理用戶自定義緩衝區中的數據。

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 的執行流程:

  1. 主線程往系統I/O複用中註冊文件描述符fd上的讀就緒事件;
  2. 主線程調用調用系統I/O複用接口等待文件描述符fd上有數據可讀;
  3. 當fd上有數據可讀時,通知主線程。主線程循環讀取fd上的數據,直到沒有更多數據可讀,然後將讀取到的數據封裝成一個請求對象並插入請求隊列。
  4. 睡眠在請求隊列上的某個工作線程被喚醒,它獲得請求對象並處理客戶請求,然後向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事件,同步線程處理請求對象,簡單的來說:

  1. 異步線程監聽到事件後,將其封裝爲請求對象插入到請求隊列中;
  2. 請求隊列有新的請求對象,通知同步線程獲取請求對象;
  3. 同步線程處理請求對象,實現業務邏輯。

半同步/半反應堆模式

考慮將兩種事件處理模型,即Reactor和Proactor,與幾種I/O模型結合在一起,那麼半同步/半異步模式就演變爲半同步/半反應堆模式。先看看使用Reactor的方式,如圖14 所示。

                    圖14 半同步/半反應堆模式示意圖

其工作流程爲:

  1. 異步線程監聽所有fd上的I/O事件,若監聽socket接可讀,接受新的連接;並監聽該連接上的讀寫事件;
  2. 若連接socket上有讀寫事件發生,異步線程將該連接socket插入請求隊列中;
  3. 同步線程被喚醒,並接管連接socket,從socket上讀取請求和發送應答;

若將Reactor替換爲Proactor,那麼其工作流程爲:

  1. 異步線程完成I/O操作,並I/O操作的結果封裝爲任務對象,插入請求隊列中;
  2. 請求隊列通知同步線程處理任務;
  3. 同步線程執行任務處理邏輯。

一種高效的演變模式

半同步/半反應堆模式有明顯的缺點:

  1. 異步線程和同步線程共享隊列,需要保護,存在資源競爭;
  2. 工作線程同一時間只能處理一個任務,任務處理量很大或者任務處理存在一定的阻塞時,任務隊列將會堆積,任務的時效性也等不到保證;不能簡單地考慮增加工作線程來處理該問題,線程數達到一定的程度,工作線程的切換也將白白消耗大量的CPU資源。

下面介紹一種改進的方式,如圖15 所示,每個工作線程都有自己的事件循環,能同時獨立處理多個用戶連接。

                圖 15 半同步/半反應堆模式的演變模式

其工作流程爲:

  1. 主線程實現連接監聽,只處理網絡I/O連接事件;
  2. 新的連接socket分發至工作線程中,這個socket上的I/O事件都由該工作線程處理,工作線程都可以處理多個socket 的I/O事件;
  3. 工作線程獨立維護自己的事件循環,監聽不同連接socket的I/O事件。

Follower/Leader 模式

Follower/Leader是多個工作線程輪流進行事件監聽、事件分發、處理事件的模式。

在Follower/Leader模式工作的任何一個時間點,只有一個工作線程處理成爲Leader ,負責I/O事件監聽,而其他線程都是Follower,並等待成爲Leader。

Follower/Leader模式的工作流概述如下:

  1. 當前Leader Thread1監聽到就緒事件後,從Follower 線程集中推選出 Thread 2成爲新的Leader;
  2. 新的Leader Thread2 繼續事件I/O監聽;
  3. Thread1繼續處理I/O就緒事件,執行完後加入到Follower 線程集中,等待成爲Leader。

從上描述,Leader/Follower模式的工作線程存在三種狀態,工作線程同一時間只能處於一種狀態,這三種狀態爲:

  • Leader:線程處於領導者狀態,負責監聽I/O事件;

  • Processing:線程處理就緒I/O事件;

  • Follower:等待成爲新的領導者或者可能被當前Leader指定處理就緒事件。

Leader監聽到I/O就緒事件後,有兩種處理方式:

  1. 推選出新的Leader後,並轉移到Processing處理該I/O就緒事件;
  2. 指定其他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三部分。這三部分的主要功能:

  1. Master Process:監聽服務端口,接收用戶連接,收發連接數據,依靠reactor模型驅動;
  2. Manager Process:Master Process的子進程,負責fork WorkProcess,並監控Work Process的運行狀態;
  3. 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層之間的媒介:

  1. Work Process接收來自Master Process的數據,包括網絡數據和連接事件,回調至PHP業務層;
  2. 將來自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 多進程多線程通信示意圖

具體流程爲:

  1. Master 進程主線程接收客戶端連接,連接建立成功後,分發至工作線程,工作線程通過Unix Socket通知Work進程連接信息;
  2. Work 進程將連接信息回調至PHP業務層;
  3. Maser 進程中的工作線程接收客戶端請求消息,並通過Unix Socket方式發送到Work進程;
  4. Work 進程將請求消息回調至PHP業務層;
  5. PHP業務層構造回覆消息,通過Work進程發送,Work進程將回復消息拷貝至共享內存中,並通過Unix Socket通知發送至Master進程的工作線程有數據需要發送;
  6. 工作線程從共享內存中取出需發送的數據,併發送至客戶端;
  7. 客戶端斷開連接,工作線程將連接斷開的事件通過UnixSocket發送至Work進程;
  8. Work進程將連接斷開事件回調至PHP業務層.

需要注意在步驟5中,Work進程通知Master進程有數據需要發送,不是將數據直接發送給Master進程,而是將數據地址(在共享內存中)發送給Master進程。

改善性能的方法

性能對於服務器而言是非常敏感和重要的,當前,硬件的發展雖然不是服務器性能的瓶頸,作爲軟件開發人員還是應該考慮在軟件層面來上改善服務性能。好的網絡模塊,除了穩定性,還有非常多的細節、技巧處理來提升服務性能,感興趣的同學可以深入瞭解Ngnix源碼的細節,以及陳碩的《Linux多線程服務器編程》。

數據複製

如果應用程序不關心數據的內容,就沒有必要將數據拷貝到應用緩衝區,可以藉助內核接口直接將數據拷貝到內核緩衝區處理,如在提供文件下載服務時,不需要將文件內容先讀到應用緩衝區,在調用send接口發送出去,可以直接使用sendfile (零拷貝)接口直接發送出去。

應用程序的工作模塊之間也應該避免數據拷貝,如:

  1. 當兩個工作進程之間需要傳遞數據,可以考慮使用共享內存的方式實現數據共享;
  2. 在流媒體的應用中,對幀數據的非必要拷貝會對程序性能的影響,特備是在嵌入式環境中影響非常明顯。通常採用的辦法是,給每幀數據分配內存(下面統稱爲buffer),當需要使用該buffer時,會增加該buffer的引用計數,buffer的引用計數爲0時纔會釋放對應的內存。這種方式適合在進程內數據無拷貝傳遞,並且不會給釋放buffer帶來困擾。

資源池

在服務運行期間,需要使用系統調用爲用戶分配資源,通常系統資源的分配都是比較耗時的,如動態創建進程/線程。可以考慮在服務啓動時預先分配資源,即創建資源池,當需要資源,從資源池中獲取即可,若資源池不夠用時,再動態的分配,使用完成後交還到資源池中。這實際上是用空間換取時間,在服務運行期間可以節省非必要的資源創建過程。需要注意的是,使用資源池還需要根據業務和硬件環境對資源池的大小進行限制。

資源池是一個抽象的概念,常見的包括進程池、線程池、 內存池、連接池;這些資源池的相關資料非常多,這裏就不一一介紹了。

鎖/上下文切換

1.關於鎖 
對共享資源的操作是併發程序中經常被提起的一個話題,都知道在業務邏輯上無法保證同步操作共享資源時,需要對共享資源加鎖保護,但是鎖不僅不能處理任何業務邏輯,而且還存在一定的系統開銷。並且對鎖的不恰當使用,可能成爲服務期性能的瓶頸。

針對鎖的使用有如下建議:

  1. 如果能夠在設計層面避免共享資源競爭,就可以避免鎖,如圖15描述的模式;
  2. 若無法避免對共享資源的競爭,優先考慮使用無鎖隊列的方式實現共享資源;
  3. 使用鎖時,優先考慮使用讀寫鎖;此外,鎖的範圍也要考慮,儘量較少鎖的顆粒度,避免其他線程無謂的等待。

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 輪詢掃描

處理過程爲:

  1. 維護一個map<client_id, last_update_time > 記錄客戶端最近一次的請求時間;
  2. 當client_id對應連接有數據到達時,更新last_update_time;
  3. 啓動一個定時器,輪詢掃描map 中client_id 對應的last_update_time,若超過 60s,則認爲對應的客戶端下線。

輪詢掃描,只啓動一個定時器,但輪詢效率低,特別是服務器維護的連接數很大時,部分連接超時事件得不到及時處理。

方案b 多定時器觸發

處理過程爲:

  1. 維護一個map<client_id, last_update_time > 記錄客戶端最近一次的請求時間;
  2. 當某client_id 對應連接有數據到達時,更新last_update_time,同時爲client_id啓用一個定時器,60s後觸發;
  3. 當client_id對應的定時器觸發後,查看map中client_id對應的last_update_time是否超過60s,若超時則認爲對應客戶端下線。

多定時器觸發,每次請求都要啓動一個定時器,可以想象,消息請求非常頻繁是,定時器的數量將會很龐大,消耗大量的系統資源。

方案c 時間輪方案

下面介紹一下利用時間輪的方式實現的一種高效、能批量的處理方案,先說一下需要的數據結構:

  1. 創建0~60的數據,構成環形隊列time_wheel,current_index維護環形隊列的當前遊標,如圖19所示;
  2. 數組元素是slot 結構,slot是一個set<client_id>,構成任務集;
  3. 維護一個map<client_id,index>,記錄client_id 落在哪個slot上。

                     圖19 時間輪環形隊列示意圖

執行過程爲:

  1. 啓用一個定時器,運行間隔1s,更新current_index,指向環形隊列下一個元素,0->1->2->3...->58->59->60...0;
  2. 連接上數據到達時,從map中獲取client_id所在的slot,在slot的set中刪除該client_id;
  3. 將client_id加入到current_index - 1鎖標記的slot中;
  4. 更新map中client_id 爲current_id-1 。

與a、b兩種方案相比,方案c具有如下優勢

  1. 只需要一個定時器,運行間隔1s,CPU消耗非常少;
  2. current_index 所標記的slot中的set不爲空時,set中的所有client_id對應的客戶端均認爲下線,即批量超時。

上面描述的時間輪處理方式會存在1s以內的誤差,若考慮實時性,可以提高定時器的運行間隔,另外該方案可以根據實際業務需求擴展到應用中。我們對Swoole的修改中,包括對定時器進行了重構,其中超時定時器採用的就是如上所描述的時間輪方案,並且精度可控。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章