從0到1優雅的實現PHP多進程管理

轉載自:https://segmentfault.com/a/1190000012234031

業務場景

在我們實際的業務場景中(PHP技術棧),我們可能需要定時或者近乎實時的執行一些業務邏輯,簡單的我們可以使用unix系統自帶的crontab實現定時任務,但是對於一些實時性要求比較高的業務就不適用了,所以我們就需要一個常駐內存的任務管理工具,爲了保證實時性,一方面我們讓它一直執行任務(適當的睡眠,保證cpu不被100%佔用),另一方面我們實現多進程保證併發的執行任務。

目的

綜上所述,我的目標就是:實現基於php-cli模式實現的master-worker多進程管理工具。其次,“我有這樣一個目標,我是怎樣一步步去分析、規劃和實現的”,這是本文的宗旨。

備註:下文中,父進程統稱爲master,子進程統稱爲worker。

分析

我們把這一個大目標拆成多個小目標去逐個實現,如下:

  • 多進程

    • 目的:一個master fork多個worker
    • 現象:所有worker的ppid父進程ID爲當前master的pid
  • master控制worker

    • 目的:master通知worker,worker接收來自master的消息
  • master接收信號

    • 目的:master接收並自定義處理來自終端的信號

多進程

PHP fork進程的方法 pcntl_fork, 這個大家應該有所瞭解,如果不知道的簡單google/bing一下應該很容易找到這個函數。接着FTM, 我們看看pcntl_fork這個函數的使用方式大致如下:

$pid = pcntl_fork(); // pcntl_fork 的返回值是一個int值
                     // 如果$pid=-1 fork進程失敗
                     // 如果$pid=0 當前的上下文環境爲worker
                     // 如果$pid>0 當前的上下文環境爲master,這個pid就是fork的worker的pid

接着看代碼:

$pid = pcntl_fork();    
switch ($pid) {
  case -1:
    // fatal error 致命錯誤 所有進程crash掉
    break;

  case 0:
    // worker context
    exit; // 這裏exit掉,避免worker繼續執行下面的代碼而造成一些問題
    break;

  default:
    // master context
    pcntl_wait($status); // pcntl_wait會阻塞,例如直到一個子進程exit
    // 或者 pcntl_waitpid($pid, $status, WNOHANG); // WNOHANG:即使沒有子進程exit,也會立即返回
    break;
}

我們看到master有調用pcntl_wait或者pcntl_waitpid函數,爲什麼呢?首先我們在這裏得提到兩個概念,如下:

  • 孤兒進程:父進程掛了,子進程被pid=1的init進程接管(wait/waitpid),直到子進程自身生命週期結束被系統回收資源和父進程採取相關的回收操作
  • 殭屍進程:子進程exit退出,父進程沒有通過wait/waitpid獲取子進程狀態,子進程佔用的進程號等描述資源符還存在,產生危害:例如進程號是有限的,無法釋放進程號導致未來可能無進程號可用

所以,pcntl_wait或者pcntl_waitpid的目的就是防止worker成爲殭屍進程(zombie process)。

除此之外我們還需要把我們的master掛起和worker掛起,我使用的的是while循環,然後usleep(200000)防止CPU被100%佔用。

最後我們通過下圖(1-1)來簡單的總結和描述這個多進程實現的過程:

master控制worker

上面實現了多進程和多進程的常駐內存,那master如何去管理worker呢?答案:多進程通信。話不多說google/bing一下,以下我列舉幾種方式:

  • 命名管道: 感興趣
  • 隊列: 個人感覺和業務中使用redis做消息隊列思路應該一致
  • 共享內存: 違背“不要通過共享內存來通信,要通過通信來實現共享”原則
  • 信號: 承載信息量少
  • 套接字: 不熟悉

所以我選擇了“命名管道”的方式。我設計的通信流程大致如下:

  • step 1: 創建worker管道
  • step 2: master寫消息到worker管道
  • step 3: worker讀消息從worker管道

接着還是逐個擊破,當然話不多說還是google/bing一下。posix_mkfifo創建命名管道、fopen打開文件(管道以文件形式存在)、fread讀取管道、fclose關閉管道就呼嘯而出,哈哈,這樣我們就能很容易的實現我們上面的思路的了。接着說說我在這裏遇到的問題:fopen阻塞了,導致業務代碼無法循環執行,一想不對啊,平常fopen普通文件不存在阻塞行爲,這時候二話不說FTM搜fopen,crtl+f頁面搜“block”,重點來了:

fopen() will block if the file to be opened is a fifo. This is true whether it's opened in "r" or "w" mode. (See man 7 fifo: this is the correct, default behaviour; although Linux supports non-blocking fopen() of a fifo, PHP doesn't).

翻譯下,大概意思就是“當使用fopen的r或者w模式打開一個fifo的文件,就會一直阻塞;儘管linux支持非阻塞的打開fifo,但是php不支持。”,得不到解決方案,不支持,感覺要放棄,一想這種場景應該不會不支持吧,再去看看posix_mkfifo,結果喜出望外:

<?php
  $fh=fopen($fifo, "r+"); // ensures at least one writer (us) so will be non-blocking
  stream_set_blocking($fh, false); // prevent fread / fwrite blocking
?>

The "r+" allows fopen to return immediately regardless of external  writer channel.

結論使用“r+”,同時我們又知道了使用stream_set_blocking防止緊接着的fread阻塞。接着我們用下圖(1-2)來簡單的總結和描述這個master-worker通信的方式。

master接收信號

最後我們需要解決的問題就是master怎麼接受來自client的信號,google/bing結論:

master接收信號 -> pcntl_signal註冊對應信號的handler方法 -> pcntl_signal_dispatch() 派發信號到handler

如下圖(1-3)所示,

其他

接着我們只要實現不同信號下master&worker的策略,例如worker的重啓等。這裏需要注意的就是,當master接受到重啓的信號後,worker不要立即exit,而是等到worker的業務邏輯執行完成了之後exit。具體的方式就是:

master接收reload信號 -> master把reload信號寫worker管道 -> worker讀取到reload信號 -> worker添加重啓標誌位 -> worker執行完業務邏輯後且檢測到重啓的標誌位後exit

建模

上面梳理完我們的實現方式後,接着我們就開始碼代碼了。碼代碼之前進行簡單的建模,如下:

進程管理類Manager

- attributes
  + master: master對象
  + workers: worker進程對象池
  + waitSignalProcessPool: 等待信號的worker池
  + startNum: 啓動進程數量
  + userPasswd: linux用戶密碼
  + pipeDir: 管道存放路徑
  + signalSupport: 支持的信號
  + hangupLoopMicrotime: 掛起間隔睡眠時間
- method
  + welcome: 歡迎於
  + configure: 初始化配置
  + fork: forkworker方法
  + execFork: 執行forkworker方法
  + defineSigHandler: 定義信號handler
  + registerSigHandler: 註冊信號handler
  + hangup: 掛起主進程

進程抽象類Process

- attributes
  + type: 進程類型 master/worker
  + pid: 進程ID
  + pipeName: 管道名稱 
  + pipeMode: 管道模式
  + pipeDir: 管道存放路徑
  + pipeNamePrefix: 管道名稱前綴
  + pipePath: 管道生成路徑
  + readPipeType: 讀取管道數據的字節數
  + workerExitFlag: 進程退出標誌位
  + signal: 當前接受到的信號
  + hangupLoopMicrotime: 掛起間隔睡眠時間
- method
  + hangup: 掛起進程(抽象方法)
  + pipeMake: 創建管道
  + pipeWrite: 寫管道
  + pipeRead: 讀管道
  + clearPipe: 清理管道文件
  + stop: 進程exit

master實體類MasterProcess

- attributes
  + 
- method
  + hangup: 掛起進程

worker實體類MasterProcess

- attributes
  + 
- method
  + dispatchSig: 定義worker信號處理方式

最後我們需要做的就是優雅的填充我們的代碼了。

最後

項目地址 https://github.com/TIGERB/naruto


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