淺析Swoole server

2021-03-11 10:00:39

一. 基礎知識

1.1 Swoole

Swoole是面向生產環境的php非同步網路通訊引擎, php開發人員可以利用Swoole開發出高效能的server服務。Swoole的server部分, 內容很多, 也涉及很多的知識點, 本文僅對其server進行簡單的概述, 具體的實現細節在後續的文章中再進行詳細介紹。

推薦(免費):

1.2 網路程式設計

1. 網路通訊是指在一臺(或者多臺)機器上啟動一個(或者多個)程序, 監聽一個(或者多個)埠, 按照某種協定(可以是標準協定http, dns; 也可以是自行定義的協定)與使用者端交換資訊。

2. 目前的網路程式設計多是在tcp, udp或者更上層的協定之上進行程式設計。Swoole的server部分是基於tcp以及udp協定的。

3. 利用udp進行程式設計較為簡單, 本文主要介紹tcp協定之上的網路程式設計

4. TCP網路程式設計主要涉及4種事件

連線建立: 主要是指使用者端發起連線(connect)以及伺服器端接受連線(accept)

訊息到達: 伺服器端接受到使用者端傳送的資料,該事件是TCP網路程式設計最重要的事件,伺服器端對於該類事件進行處理時, 可以採用阻塞式或者非阻塞式,除此之外, 伺服器端還需要考慮分包, 應用層緩衝區等問題

訊息傳送成功: 傳送成功是指應用層將資料成功傳送到核心的通訊端傳送緩衝區中,並不是指使用者端成功接受資料。對於低流量的服務而言,資料通常一次性即可傳送完,並不需要關心此類事件。如果一次性不能將全部資料傳送到核心緩衝區,則需要關心訊息是否成功傳送(阻塞式程式設計在系統呼叫(write, writev, send等)返回後即是傳送成功, 非阻塞式程式設計則需要考慮實際寫入的資料是否與預期一致)

連線斷開: 需要考慮使用者端斷開連線(read返回0)以及伺服器端斷開連線(close, shutdown)

5. tcp建立連線的過程如下圖

● 圖中, ACK、SYN表示標誌位, seq、ack為tcp包的序號以及確認序號

6. tcp斷開連線的過程如下圖

● 上圖考慮的是使用者端主動斷開連線的情況, 伺服器端主動斷開連線也類似

● 圖中, FIN、ACK表示標誌位, seq、ack為tcp包的序號以及確認序號

1.3 程序間通訊

1. 程序之間的通訊有無名管道(pipe), 有名管道(fifo), 訊號(signal), 號誌(semaphore), 通訊端(socket), 共用記憶體(shared memory)等方式

2. Swoole中採用unix域通訊端(通訊端的一種)用於多程序之間的通訊(指Swoole內部程序之間)

1.4 socketpair

1. socketpair用於建立一個通訊端對, 類似於pipe, 不同的是pipe是單向通訊, 雙向通訊需要建立兩次, socketpair呼叫一次即可實現雙向通訊, 除此之外, 由於使用的是通訊端, 還可以定義資料交換的方式

2. socketpair系統呼叫

  • 呼叫成功後sv[0], sv[1]分別儲存一個檔案描述符
  • 向sv[0]中寫入, 可以從sv[1]中讀取
  • 向sv[1]中寫入, 可以從sv[0]中讀取
  • 程序呼叫socketpair後, fork子程序, 子程序會預設繼承sv[0], sv[1]這兩個檔案描述符, 進而可以實現父子程序間通訊。例如, 父程序向sv[0]中寫入, 子程序從sv[1]中讀取; 子程序向sv[1]中寫入, 父程序從sv[0]中讀取

1.5 守護行程(daemon)

1. 守護行程是一種特殊的後臺程序, 它脫離於終端, 用於週期性的執行某種任務

2. 行程群組

  • 每個程序都屬於一個行程群組
  • 每個行程群組都有一個行程群組號, 也就是該組組長的程序號(PID)
  • 一個程序只能為自己或者其子程序設定行程群組號

3. 對談

  • 一個對談可以包含多個行程群組, 這些行程群組中最多隻能有一個前臺行程群組(也可以沒有), 其餘為後臺行程群組
  • 一個對談最多隻能有一個控制終端
  • 使用者通過終端登入或者網路登入, 會建立一個新的對談
  • 程序呼叫系統呼叫setsid可以建立一個新的對談, 呼叫setsid的程序不能是某個行程群組的組長。setsid呼叫完成後, 該程序成為這個對談的首程序(領頭程序), 同時變成一個新的行程群組的組長, 如果該程序之前有控制終端, 該程序與終端的聯絡也被斷開

4. 建立守護行程的方式

  • fork子程序後, 父程序退出, 子程序執行setsid, 子程序即可成為守護行程。這種方式下, 子程序是對談的領頭程序, 可以重新開啟終端, 此時可以再次fork, fork產生的子程序無法再開啟終端(只有對談的領頭程序才能開啟終端)。第二次fork並不是必須的, 只是為了防止子程序再次開啟終端
  • linux提供了daemon函數(該函數並不是系統呼叫, 為庫函數)用於建立守護行程

1.6 Swoole tcp server範例

  • 上述程式碼在cli模式下執行時, 經過詞法分析, 語法分析生成opcode, 進而交由zend虛擬機器器執行
  • zend虛擬機器器在執行到$serv->start()時, 啟動Swoole server
  • 上述程式碼中設定的事件回撥是在worker程序中執行, 後文會詳細介紹Swoole server模型

二. Swoole server


2.1 base模式

1. 說明

  • base模式採用多程序模型, 這種模型與nginx一致, 每個程序只有一個執行緒, 主程序負責管理工作程序, 工作程序負責監聽埠, 接受連線, 處理請求以及關閉連線
  • 多個程序同時監聽埠, 會有驚群問題, linux 3.9之前版本的核心, Swoole沒有解決驚群問題
  • linux 核心3.9及其後續版本提供了新的通訊端引數SO_REUSEPORT, 該引數允許多個程序繫結到同一個埠, 核心在接受到新的連線請求時, 會喚醒其中一個進行處理, 核心層面也會做負載均衡, 可以解決上述的驚群問題, Swoole也已經加入了這個引數
  • base模式下, reactor_number引數並沒有實際作用
  • 如果worker程序數設定為1, 則不會fork出worker程序, 主程序直接處理請求, 這種模式適合偵錯

2. 啟動過程

  • php程式碼執行到$serv->start()時,主程序進入int swServer_start(swServer *serv)函數, 該函數負責啟動server
  • 在函數swServer_start中會呼叫swReactorProcess_start, 這個函數會fork出多個worker程序
  • 主程序和worker程序各自進入自己的事件迴圈, 處理各類事件

2.2 process模式

1. 說明

  • 這種模式為多程序多執行緒, 有主程序, manager程序, worker程序, task_worker程序
  • 主程序下有多個執行緒, 主執行緒負責接受連線, 之後交給react執行緒處理請求。 react執行緒負責接收封包, 並將資料轉發給worker程序進行處理, 之後處理worker程序返回的資料
  • manager程序, 該程序為單執行緒, 主要負責管理worker程序, 類似於nginx中的主程序, 當worker程序異常退出時, manager程序負責重新fork出一個worker程序
  • worker程序, 該程序為單執行緒, 負責具體處理請求
  • task_worker程序, 用於處理比較耗時的任務, 預設不開啟
  • worker程序與主程序中的react執行緒使用域通訊端進行通訊, worker程序之間不進行通訊

2. 啟動過程

  • Swoole server啟動入口: swServer_start函數

  • 如果設定了daemon模式, 在必要的引數檢查完後, 先將自己變為守護行程再fork manager程序, 進而建立reactor執行緒
  • 主程序先fork出manager程序, manager程序負責fork出worker程序以及task_worker程序。worker程序之後進入int swWorker_loop
    (swServer *serv, int worker_id), 也就是進入自己的事件迴圈, task_worker也是一樣, 進入自己的事件迴圈

主程序pthread_create出react執行緒, 主執行緒和react執行緒各自進入自己的事件迴圈, reactor執行緒執行static int swRea-torThread_loop (swThreadParam *param), 等待處理事件

3. 結構圖

  • Swoole process模式結構如下圖所示,

上圖並沒有考慮task_worker程序, 在預設情況下, task_worker程序數為0

三. 請求處理流程(process模式)


3.1 reactor執行緒與worker程序之間的通訊

1. Swoole master程序與worker程序之間的通訊如下圖所示

  • Swoole使用SOCK_DGRAM, 而不是SOCK_STREAM, 這裡是因為每個reactor執行緒負責處理多個請求, reactor接收到請求後會將資訊轉發給worker程序, 由worker程序負責處理,如果使用SOCK_STREAM, worker程序無法對tcp進行分包, 進而處理請求
  • swFactoryProcess_start函數中會根據worker程序數建立對應個數的通訊端對, 用於reactor執行緒與worker程序通訊(詳見swPipeUnsock_create函數)

2. 假設reactor執行緒有2個, worker程序有3個, 則reactor與worker之間的通訊如下圖所示

  • 每個reactor執行緒負責監聽幾個worker程序, 每個worker程序只有一個reactor執行緒監聽(reactor_num <= worker_num)。Swoole預設使用worker_process_id % reactor_num對worker程序進行分配, 交給對應的reactor執行緒進行監聽
  • reactor執行緒收到某個worker程序的資料後會進行處理, 值得注意的是, 這個reactor執行緒可能並不是傳送請求的那個reactor執行緒。

3. reactor執行緒與worker程序通訊的封包

3.2 請求處理

1. master程序中的主執行緒負責監聽埠(listen), 接受連線(accept, 產生一個fd), 接受連線後將請求分配給reactor執行緒, 預設通過fd % reactor_number進行分配, 之後通過epoll_ctl將fd加入到對應reactor執行緒中, 剛加入時監聽寫事件, 因為新接受連線建立的通訊端寫緩衝區為空, 故而一定可寫, 會被立刻觸發, 進而reactor執行緒進行一些初始化操作

  • 存在多個執行緒同時操作一個epollfd(通過系統呼叫epoll_create建立)的情況
  • 多個執行緒同時呼叫epoll_ctl是執行緒安全的(對應一個epollfd), 一個執行緒正在執行, 其他執行緒會被阻塞(因為需要同時操作epoll底層的紅黑樹)
  • 多個執行緒同時呼叫epoll_wait也是執行緒安全的, 但是一個事件可能會被多個執行緒同時接收到, 實際中不建議多個執行緒同時epoll_wait一個epollfd。Swoole中也是不存在這種情況的, Swoole中每個reactor執行緒都有自己的epollfd
  • 一個執行緒呼叫epoll_wait, 一個執行緒呼叫epoll_ctl, 根據man手冊, 如果epoll_ctl新加入的fd已經準備好, 會使得執行epoll_wait的執行緒變成非阻塞狀態(可以通過man epoll_wait檢視相關內容)

2. reactor執行緒中fd的寫事件被觸發, reactor執行緒負責處理, 發現是首次加入, 沒有資料可寫, 則會開啟讀事件監聽, 準備接受使用者端傳送的資料

3. reactor執行緒讀取到使用者的請求資料, 一個請求的資料接收完後, 將資料轉發給worker程序, 預設是通過fd % worker_number進行分配

  • reactor傳送給worker程序的封包, 會包含一個頭部, 頭部中記錄了reactor的資訊
  • 如果傳送的資料過大, 則需要將資料進行分片, 限於篇幅, 資料分片, 後續再進行詳細講述
  • 可能存在多個reactor執行緒同時向同一個worker程序傳送資料的情況, 故而Swoole採用SOCK_DGRAM模式與worker程序進行通訊, 通過每個封包的包頭, worker程序可以區分出是由哪個reactor執行緒傳送的資料, 也可以知道是哪個請求

4. worker程序收到reactor傳送的封包後, 進行處理, 處理完成後, 將請求結果傳送給主程序

  • worker程序傳送給主程序的封包, 也會包含一個頭部, 當reactor執行緒收到封包後, 能夠知道對應的reactor執行緒, 請求的fd等資訊

5. 主程序收到worker程序傳送的封包, 這個會觸發某個reactor執行緒進行處理

  • 這個reactor執行緒並不一定是之前傳送請求給worker程序的那個reactor執行緒
  • 主程序的每個reactor執行緒都負責監聽worker程序傳送的封包, 每個worker傳送的封包只會由一個reactor執行緒進行監聽, 故而只會觸發一個reactor執行緒

6. reactor執行緒處理worker程序傳送的請求處理結果, 如果是直接傳送資料給使用者端, 則可以直接傳送, 如果需要改變這個這個連線的監聽狀態(例如close), 則需要先找到監聽這個連線的reactor執行緒, 進而改變這個連線的監聽狀態(通過呼叫epoll_ctl)

  • reactor處理執行緒與reactor監聽執行緒可能並不是同一個執行緒
  • reactor監聽執行緒負責監聽使用者端傳送的資料, 進而轉發給worker程序
  • reactor處理執行緒負責監聽worker程序傳送給主程序的資料, 進而將資料傳送給使用者端

四. gdb偵錯

4.1 process模式啟動

4.2 base模式啟動

五. 總結及思考

1. 本文主要介紹了Swoole server的兩種模式: base模式、process模式, 詳細講解了兩種模式的網路程式設計模型, 並重點介紹了process模式下, 程序間通訊的方式、請求的處理流程等

2. process模式下, 為什麼不直接在主程序中建立多個執行緒, 由執行緒直接進行處理請求(可以避免程序間通訊的開銷), 而是建立出manager程序, 再由manager程序建立出worker程序, 由worker程序處理請求?

  • 個人覺得可能是php對多執行緒的支援不是很友好, phper大都也只是進行單執行緒程式設計
  • ZendVM 提供的 TSRM 雖然也是支援多執行緒環境,但實際上這是一個 按執行緒隔離記憶體的方案, 多執行緒並沒有意義

3. process模式下, 主程序中的每個reactor執行緒都可以同時處理多個請求, 多個請求是並行處理的, 我們從2個維度看

  • 從主程序的角度看, 主程序同時處理多個請求, 當一個請求包全部接收完後, 轉發給worker程序進行處理
  • 從某個worker程序的角度看, 這個worker程序收到的請求是序列的, 預設情況下, worker程序也是序列處理請求, 如果單個請求阻塞(Swoole的worker程序會回撥phper寫的事件處理常式, 該函數可能阻塞), 後續的請求也無法處理, 這個就是排頭阻塞問題, 這種情況下可以使用Swoole的協程, 通過協程的排程, 單個請求阻塞時, worker程序可以繼續處理其他請求

4. 使用Swoole建立tcp server時, 由於tcp是位元組流的協定, 需要分包, 而Swoole在不清楚使用者端與伺服器端通訊協定的情況下, 無法進行分包, process模式下, reactor交給worker程序的資料也只能是位元組流的, 需要使用者自行處理。當然, 一般情況也不需要自行構建協定, 使用tcp server, Swoole已經支援Http, Https等協定

以上就是淺析Swoole server的詳細內容,更多請關注TW511.COM其它相關文章!