1. 程式人生 > 程式設計 >詳解PHP多程序消費佇列

詳解PHP多程序消費佇列

引言

最近開發一個小程式設計客棧功能,用到了佇列mcq,啟動一個程序消費佇列資料,後邊發現一個程序處理不過來了,又加了一個程序,過了段時間又處理不過來了......

這種方式每次都要修改crontab,如果程序掛掉了,不會及時的啟動,要等到下次crontab執行的時候才會啟動。關閉(重啟)程序的時候用的是kill,這可能會丟失正在處理的資料,比如下面這個例子,我們假設sleep過程就是處理邏輯,這裡為了明顯看出效果,將處理時間放大到10s:

<?php
$i = 1;
while (1) {
    echo "開始第[{$i}]次迴圈\n";
    sleep(10);
    echo "結束第[{$i}]次迴圈\n";
    $i++;
}

當我們執行指令碼之後,等到迴圈開始之後,給程序傳送kill {$pid},預設傳送的是編號為15的SIGTERM訊號。假設$i是從佇列拿到的,拿到2的時候,正在處理,我們給程式傳送了kill訊號,和佇列資料丟失一樣,問題比較大,因此我要想辦法解決這些問題。

開始第[1]次迴圈

結束第[1]次迴圈

開始第[2]次迴圈

[1]    28372 terminated  php t.php

nginx程序模型

這時候我想到了nginx,nginx作為高效能伺服器的中流砥柱,為成千上萬的企業和個人服務,他的程序模型比較經典,如下所示:

詳解PHP多程序消費佇列

管理員通過master程序和nginx進行互動,從/path/to/nginx.pid讀取nginx master程序的pid,傳送訊號給master程序,master根據不同的訊號做出不同的處理,然後反饋資訊給管理員。worker是master程序fork出來的,master負責管理worker,不會去處理業務,worker才是具體業務的處理者,master可以控制worker的退出、啟動,當worker意外退出,master會收到子程序退出的訊息,也會重新啟動新的worker程序補充上來,不讓業務處理受影響。nginx還可以平滑退出,不丟失任何一個正在處理的資料,更新配置時nginx可以做到不影響線上服務來載入新的配置,這在請求量很大的時候特別有用。

程序設計

看了nginx的進模型,我們完全可以開發一個類似的類庫來滿足處理mcq資料的需求,做到單檔案控制所有程序、可以平滑退出、可以檢視子程序狀態。不需要太複雜,因為我們處理佇列資料接收一定的延遲,做到nginx那樣不間斷服務比較麻煩,費時費力,意義不是很大。設計的程序模型跟nginx類似,更像是nginx的簡化版本。

詳解PHP多程序消費佇列

程序訊號量設計

訊號量是程序間通訊的一種方式,比較簡單,單功能也比較弱,只能傳送訊號給程序,程序根據訊號做出不同的處理。

master程序啟動的時候儲存pid到檔案/path/to/daeminze.pid,管理員通過訊號和master程序通訊,master程序安裝3種訊號,碰到不同的訊號,做出不同的處理,如下所示:

SIGINT => 平滑退出,處理完正在處理的資料再退出

SIGTERM => 暴力退出,無論程序是否正在處理資料直接退出

SIGUSR1 => 檢視程序狀態,檢視程序佔用記憶體,執行時間等資訊

master程序通過訊號和worker程序通訊,worker程序安裝了2個訊號,如下所示:

SIGINT => 平滑退出

SIGUSR1 => 檢視worker程序自身狀態

為什麼worker程序只安裝2個訊號呢,少了個SIGTERM,因為master程序收到訊號SIGTERM之後,向worker程序傳送SIGKILL訊號,預設強制關閉程序即可。

worker程序是通過master程序fork出來的,這樣master程序可以通過pcntl_wait來等待子程序退出事件,當有子程序退出的時候返回子程序pid,做處理並啟動新的程序補充上來。

master程序也通過pcntl_wait來等待接收訊號,當有訊號到達的時候,會返回-1,這個地方還有些坑,在下文中會詳細講。

PHP中有2種訊號觸發的方式,第一種方式是deGDMLtahzEcclare(ticks = 1);,這種效率不高,Zend每執行一次低階語句,都會去檢查程序中是否有未處理的訊號,現在已經很少使用了,PHP 5.3.0及之前的版本可能會用到這個。

第二種是通過pcntl_signal_dispatch來呼叫未處理的訊號,PHP 5.4.0及之後的版本適用,可以巧妙的將該函式放在迴圈中,效能上基本沒什麼損失,現在推薦適用。

PHP安裝修訊號量

PHP通過pcntl_signal安裝訊號,函式宣告如下所示:

bool pcntl_signal ( int $signo,[callback $handler [,bool $restart_syscalls = true ] )

第三個引數restart_syscalls不太好理解,找了很多資料,也沒太查明白,經過試驗發現,這個引數對pcntl_wait函式接收訊號有影響,當設定為預設值true的時候,http://www.cppcns.com傳送訊號,程序用pcntl_wait收不到,必須設定為false才可以,看看下面這個例子:

<?php
$i = 0;
while ($i<5) {
    $pid = pcntl_fork();
    $random = rand(10,50);
    if ($pid == 0) {
        sleep($random);
        exit();
    }
    echo "child {$pid} sleep {$random}\n";
    $i++;
}

pcntl_signal(SIGINT,function($signo) {
     echo "Ctrl + C\n";
});

while (1) {
    $pid = pcntl_wait($status);
    var_dump($pid);
    pcntl_signal_dispatch();
}

執行之後,我們對父程序傳送kill -SIGINT {$pid}訊號,發現pcntl_wait沒有反應,等到有子程序退出的時候,傳送過的SIGINT會一個個執行,比如下面結果:

child 29643 sleep 48

child 29644 sleep 24

child 29645 sleep 37

child 29646 sleep 20

child 29647 sleep 31

int(29643)

Ctrl + C

Ctrl + C

Ctrl + C

Ctrl + C

int(29646)

這是執行指令碼之後馬上給父程序傳送了四次SIGINT訊號,等到一個子程序推出的時候,所有訊號都會觸發。

但當把安裝訊號的第三個引數設定為false:

pcntl_signal(SIGINT,function($signo) {
     echo "Ctrl + C\n";
},false);

這時候給父程序傳送SIGINT訊號,pcntl_wait會馬上返回-1,訊號對應的事件也會觸發。

所以第三個引數大概意思就是,是否重新註冊此訊號,如果為false只註冊一次,觸發之後就返回,pcntl_wait就能收到訊息,如果為true,會重複註冊,不會返回,pcntl_wait收不到訊息。

訊號量和系統呼叫

訊號量會打斷系統呼叫,讓系統呼叫立刻返回,比如sleep,當程序正在sleep的時候,收到訊號,sleep會馬上返回剩餘sleep秒數,比如:

<?php
pcntl_signal(SIGINT,false);

while (true) {
	pcntl_signal_dispatch();
    echo "123\n";
    $limit = sleep(2);
	echo "limit sleep [{$limit}] s\n";
}

執行之後,按Ctrl + C,結果如下所示:

123

^Climit sleep [1] s

GDMLtahzEc

Ctrl + C

123

limit sleep [0] s

123

^Climit sleep [1] s

Ctrl + C

123

^Climit sleep [2] s

daemon(守護)程序

這種程序一般設計為daemon程序,不受終端控制,不與終端互動,長時間執行在後臺,而對於一個程序,我們可以通過下面幾個步驟把他升級為一個標準的daemon程序:

protected function daemonize()
{
    $pid = pcntl_fork();
    if (-1 == $pid) {
        throw new Exception("fork程序失敗");
    } elseif ($pid != 0) {
        exit(0);
    }
    if (-1 == posix_setsid()) {
        throw new Exception("新建立session會話失敗");
    }

    $pid = pcntl_fork();
    if (-1 == $pid) {
        throw new Exception("fork程序失敗");
    } else if($pid != 0) {
        exit(0);
    }

    umask(0);
    chdir("/");
}

攏共分五步:

1.fork子程序,父程序退出。

2.設定子程序為會話組長,程序組長。

3.再次fork,父程序退出,子程序繼續執行。

4.恢復檔案掩碼為0。

5.切換當前目錄到根目錄/。

第2步是為第1步做準備,設定程序為會話組長,必要條件是程序非程序組長,因此做第一次fork,程序組長(父程序)退出,子程序通過posix_setsid()設定為會話組長,同時也為程序組長。

第3步是為了不讓程序重新控制終端,因為一個程序控制一個終端的必要條件是會話組長(pidGDMLtahzEc=sid)。

第4步是為了恢復預設的檔案掩碼,避免之前做的操作對檔案掩碼做了設定,帶來不必要的麻煩。關於檔案掩碼, linux中,檔案掩碼在建立檔案、資料夾的時候會用到,檔案的預設許可權為666,資料夾為777,建立檔案(夾)的時候會用預設值減去掩碼的值作為建立檔案(夾)的最終值,比如掩碼022下建立檔案666 - 222 = 644,建立資料夾777 - 022 = 755:

掩碼 新建檔案許可權 新建資料夾許可權
umask(0) 666 (-rw-rw-rw-) 777 (drwxrwxrwx)
umask(022) 644 (-rw-r--r--) 755 (drwxr-xr-x)

第5步是切換了當前目錄到根目錄/,網上說避免起始執行他的目錄不能被正確解除安裝,這個不是太瞭解。

對應5步,每一步的各種id變化資訊:

操作後 pid ppid pgid sid
開始 17723 31381 17723 31381
第一次fork 17723 1 17723 31381
posix_setsid() 17740 1 17740 17740
第二次fork 17840 1 17740 17740

另外,會話、程序組、程序的關係如下圖所示,這張圖有助於更好的理解。

詳解PHP多程序消費佇列

至此,你也可以輕鬆地造出一個daemon程序了。

命令設計

我準備給這個類庫設計6個命令,如下所示:

1.start 啟動命令

2.restart 強制重啟

3.stop 平滑停止

4.reload 平滑重啟

5.quit 強制停止

6.status 檢視程序狀態

啟動命令

啟動命令就是預設的流程,按照預設流程走就是啟動命令,啟動命令會檢測pid檔案中是否已經有pid,pid對應的程序是否健康,是否需要重新啟動。

強制停止命令

管理員通過入口檔案結合pid給master程序傳送SIGTERM訊號,master程序給所有子程序傳送SIGKILL訊號,等待所有worker程序退出後,master程序也退出。

強制重啟命令

強制停止命令+啟動命令

平滑停止命令

平滑停止命令,管理員給master程序傳送SIGINT訊號,master程序給所有子程序傳送SIGINT,worker程序將自身狀態標記為stoping,當worker程序下次迴圈的時候會根據stoping決定停止,不在接收新的資料,等所有worker程序退出之後,master程序也退出。

平滑重啟命令

平滑停止命令+啟動命令

檢視程序狀態

檢視程序狀態這個借鑑了workerman的思路,管理員給master程序傳送SIGUSR1訊號,告訴主程序,我要看所有程序的資訊,master程序,master程序將自身的程序資訊寫入配置好的檔案路徑A中,然後傳送SIGUSR1,告訴worker程序把自己的資訊也寫入檔案A中,由於這個過程是非同步的,不知道worker程序啥時候寫完,所以master程序在此處等待,等所有worker程序都寫入檔案之後,格式化所有的資訊輸出,最後輸出的內容如下所示:

➜/dir /usr/local/bin/php DaemonMcn.php status

Daemon [DaemonMcn] 資訊:

-------------------------------- master程序狀態 --------------------------------

pid       佔用記憶體       處理次數       開始時間                 執行時間

16343     0.75M          --             2018-05-15 09:42:45      0 天 0 時 3 分

12 slaver

-------------------------------- slaver程序狀態 --------------------------------

任務task-mcq:

16345     0.75M          236            2018-05-15 09:42:45      0 天 0 時 3 分

16346     0.75M          236            2018-05-15 09:42:45      0 天 0 時 3 分

--------------------------------------------------------------------------------

任務test-mcq:

16348     0.75M          49             2018-05-15 09:42:45      0 天 0 時 3 分

16350     0.75M          49             2018-05-15 09:42:45      0 天 0 時 3 分

16358     0.75M          49             2018-05-15 09:42:45      0 天 0 時 3 分

16449     0.75M          1              2018-05-15 09:46:40      0 天 0 時 0 分

--------------------------------------------------------------------------------

等待worker程序將程序資訊寫入檔案的時候,這個地方用了個比較trick的方法,每個worker程序輸出一行資訊,統計檔案的行數,達到worker程序的行數之後表示所有worker程序都將資訊寫入完畢,否則,每個1s檢測一次。

以上就是詳解PHP多程序消費佇列的詳細內容,更多關於PHP多程序消費佇列的資料請關注我們其它相關文章!