您好,登錄后才能下訂單哦!
今天就跟大家聊聊有關(guān)PHP中的多進程消費隊列是什么,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。
引言
最近開發(fā)一個小功能,用到了隊列mcq,啟動一個進程消費隊列數(shù)據(jù),后邊發(fā)現(xiàn)一個進程處理不過來了,又加了一個進程,過了段時間又處理不過來了......
這種方式每次都要修改crontab,如果進程掛掉了,不會及時的啟動,要等到下次crontab執(zhí)行的時候才會啟動。關(guān)閉(重啟)進程的時候用的是kill,這可能會丟失正在處理的數(shù)據(jù),比如下面這個例子,我們假設(shè)sleep過程就是處理邏輯,這里為了明顯看出效果,將處理時間放大到10s:
<?php $i = 1; while (1) { echo "開始第[{$i}]次循環(huán)\n"; sleep(10); echo "結(jié)束第[{$i}]次循環(huán)\n"; $i++; }
當(dāng)我們運行腳本之后,等到循環(huán)開始之后,給進程發(fā)送 kill {$pid}
,默認發(fā)送的是編號為15的SIGTERM
信號。假設(shè)$i
是從隊列拿到的,拿到2的時候,正在處理,我們給程序發(fā)送了kill信號,和隊列數(shù)據(jù)丟失一樣,問題比較大,因此我要想辦法解決這些問題。
開始第[1]次循環(huán) 結(jié)束第[1]次循環(huán) 開始第[2]次循環(huán) [1] 28372 terminated php t.php
nginx進程模型
這時候我想到了nginx,nginx作為高性能服務(wù)器的中流砥柱,為成千上萬的企業(yè)和個人服務(wù),他的進程模型比較經(jīng)典,如下所示:
管理員通過master進程和nginx進行交互,從/path/to/nginx.pid
讀取nginx master進程的pid,發(fā)送信號給master進程,master根據(jù)不同的信號做出不同的處理,然后反饋信息給管理員。worker是master進程fork出來的,master負責(zé)管理worker,不會去處理業(yè)務(wù),worker才是具體業(yè)務(wù)的處理者,master可以控制worker的退出、啟動,當(dāng)worker意外退出,master會收到子進程退出的消息,也會重新啟動新的worker進程補充上來,不讓業(yè)務(wù)處理受影響。nginx還可以平滑退出,不丟失任何一個正在處理的數(shù)據(jù),更新配置時nginx可以做到不影響線上服務(wù)來加載新的配置,這在請求量很大的時候特別有用。
進程設(shè)計
看了nginx的進模型,我們完全可以開發(fā)一個類似的類庫來滿足處理mcq數(shù)據(jù)的需求,做到單文件控制所有進程、可以平滑退出、可以查看子進程狀態(tài)。不需要太復(fù)雜,因為我們處理隊列數(shù)據(jù)接收一定的延遲,做到nginx那樣不間斷服務(wù)比較麻煩,費時費力,意義不是很大。設(shè)計的進程模型跟nginx類似,更像是nginx的簡化版本。
進程信號量設(shè)計
信號量是進程間通訊的一種方式,比較簡單,單功能也比較弱,只能發(fā)送信號給進程,進程根據(jù)信號做出不同的處理。
master進程啟動的時候保存pid到文件/path/to/daeminze.pid
,管理員通過信號和master進程通訊,master進程安裝3種信號,碰到不同的信號,做出不同的處理,如下所示:
SIGINT => 平滑退出,處理完正在處理的數(shù)據(jù)再退出 SIGTERM => 暴力退出,無論進程是否正在處理數(shù)據(jù)直接退出 SIGUSR1 => 查看進程狀態(tài),查看進程占用內(nèi)存,運行時間等信息
master進程通過信號和worker進程通訊,worker進程安裝了2個信號,如下所示:
SIGINT => 平滑退出 SIGUSR1 => 查看worker進程自身狀態(tài)
為什么worker進程只安裝2個信號呢,少了個SIGTERM
,因為master進程收到信號SIGTERM
之后,向worker進程發(fā)送SIGKILL
信號,默認強制關(guān)閉進程即可。
worker進程是通過master進程fork出來的,這樣master進程可以通過pcntl_wait
來等待子進程退出事件,當(dāng)有子進程退出的時候返回子進程pid,做處理并啟動新的進程補充上來。
master進程也通過pcntl_wait
來等待接收信號,當(dāng)有信號到達的時候,會返回-1
,這個地方還有些坑,在下文中會詳細講。
PHP中有2種信號觸發(fā)的方式,第一種方式是declare(ticks = 1);
,這種效率不高,Zend每執(zhí)行一次低級語句,都會去檢查進程中是否有未處理的信號,現(xiàn)在已經(jīng)很少使用了,PHP 5.3.0
及之前的版本可能會用到這個。
第二種是通過pcntl_signal_dispatch
來調(diào)用未處理的信號,PHP 5.4.0
及之后的版本適用,可以巧妙的將該函數(shù)放在循環(huán)中,性能上基本沒什么損失,現(xiàn)在推薦適用。
PHP安裝修信號量
PHP通過pcntl_signal
安裝信號,函數(shù)聲明如下所示:
bool pcntl_signal ( int $signo , [callback $handler [, bool $restart_syscalls = true ] )
第三個參數(shù)restart_syscalls
不太好理解,找了很多資料,也沒太查明白,經(jīng)過試驗發(fā)現(xiàn),這個參數(shù)對pcntl_wait
函數(shù)接收信號有影響,當(dāng)設(shè)置為缺省值true
的時候,發(fā)送信號,進程用pcntl_wait
收不到,必須設(shè)置為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(); }
運行之后,我們對父進程發(fā)送kill -SIGINT {$pid}
信號,發(fā)現(xiàn)pcntl_wait沒有反應(yīng),等到有子進程退出的時候,發(fā)送過的SIGINT
會一個個執(zhí)行,比如下面結(jié)果:
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)
這是運行腳本之后馬上給父進程發(fā)送了四次SIGINT
信號,等到一個子進程推出的時候,所有信號都會觸發(fā)。
但當(dāng)把安裝信號的第三個參數(shù)設(shè)置為false
:
pcntl_signal(SIGINT, function($signo) { echo "Ctrl + C\n"; }, false);
這時候給父進程發(fā)送SIGINT
信號,pcntl_wait
會馬上返回-1
,信號對應(yīng)的事件也會觸發(fā)。
所以第三個參數(shù)大概意思就是,是否重新注冊此信號,如果為false只注冊一次,觸發(fā)之后就返回,pcntl_wait
就能收到消息,如果為true,會重復(fù)注冊,不會返回,pcntl_wait
收不到消息。
信號量和系統(tǒng)調(diào)用
信號量會打斷系統(tǒng)調(diào)用,讓系統(tǒng)調(diào)用立刻返回,比如sleep
,當(dāng)進程正在sleep的時候,收到信號,sleep會馬上返回剩余sleep秒數(shù),比如:
<?php pcntl_signal(SIGINT, function($signo) { echo "Ctrl + C\n"; }, false); while (true) { pcntl_signal_dispatch(); echo "123\n"; $limit = sleep(2); echo "limit sleep [{$limit}] s\n"; }
運行之后,按Ctrl + C
,結(jié)果如下所示:
123 ^Climit sleep [1] s Ctrl + C 123 limit sleep [0] s 123 ^Climit sleep [1] s Ctrl + C 123 ^Climit sleep [2] s
daemon(守護)進程
這種進程一般設(shè)計為daemon進程,不受終端控制,不與終端交互,長時間運行在后臺,而對于一個進程,我們可以通過下面幾個步驟把他升級為一個標(biāo)準(zhǔn)的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、設(shè)置子進程為會話組長,進程組長。
3、再次fork,父進程退出,子進程繼續(xù)運行。
4、恢復(fù)文件掩碼為0
。
5、切換當(dāng)前目錄到根目錄/
。
第2步是為第1步做準(zhǔn)備,設(shè)置進程為會話組長,必要條件是進程非進程組長,因此做第一次fork,進程組長(父進程)退出,子進程通過posix_setsid()
設(shè)置為會話組長,同時也為進程組長。
第3步是為了不讓進程重新控制終端,因為一個進程控制一個終端的必要條件是會話組長(pid=sid)。
第4步是為了恢復(fù)默認的文件掩碼,避免之前做的操作對文件掩碼做了設(shè)置,帶來不必要的麻煩。關(guān)于文件掩碼, linux中,文件掩碼在創(chuàng)建文件、文件夾的時候會用到,文件的默認權(quán)限為666,文件夾為777,創(chuàng)建文件(夾)的時候會用默認值減去掩碼的值作為創(chuàng)建文件(夾)的最終值,比如掩碼022
下創(chuàng)建文件666 - 222 = 644
,創(chuàng)建文件夾777 - 022 = 755
:
掩碼 | 新建文件權(quán)限 | 新建文件夾權(quán)限 |
umask(0) | 666 (-rw-rw-rw-) | 777 (drwxrwxrwx) |
umask(022) | 644 (-rw-r--r--) | 755 (drwxr-xr-x) |
第5步是切換了當(dāng)前目錄到根目錄/
,網(wǎng)上說避免起始運行他的目錄不能被正確卸載,這個不是太了解。
對應(yīng)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 |
另外,會話、進程組、進程的關(guān)系如下圖所示,這張圖有助于更好的理解。
至此,你也可以輕松地造出一個daemon進程了。
命令設(shè)計
我準(zhǔn)備給這個類庫設(shè)計6個命令,如下所示:
start 啟動命令
restart 強制重啟
stop 平滑停止
reload 平滑重啟
quit 強制停止
status 查看進程狀態(tài)
啟動命令
啟動命令就是默認的流程,按照默認流程走就是啟動命令,啟動命令會檢測pid文件中是否已經(jīng)有pid,pid對應(yīng)的進程是否健康,是否需要重新啟動。
強制停止命令
管理員通過入口文件結(jié)合pid給master進程發(fā)送SIGTERM
信號,master進程給所有子進程發(fā)送SIGKILL
信號,等待所有worker進程退出后,master進程也退出。
強制重啟命令
強制停止命令
+ 啟動命令
平滑停止命令
平滑停止命令,管理員給master進程發(fā)送SIGINT
信號,master進程給所有子進程發(fā)送SIGINT
,worker進程將自身狀態(tài)標(biāo)記為stoping
,當(dāng)worker進程下次循環(huán)的時候會根據(jù)stoping
決定停止,不在接收新的數(shù)據(jù),等所有worker進程退出之后,master進程也退出。
平滑重啟命令
平滑停止命令
+ 啟動命令
查看進程狀態(tài)
查看進程狀態(tài)這個借鑒了workerman的思路,管理員給master進程發(fā)送SIGUSR1
信號,告訴主進程,我要看所有進程的信息,master進程,master進程將自身的進程信息寫入配置好的文件路徑A中,然后發(fā)送SIGUSR1
,告訴worker進程把自己的信息也寫入文件A中,由于這個過程是異步的,不知道worker進程啥時候?qū)懲?,所以master進程在此處等待,等所有worker進程都寫入文件之后,格式化所有的信息輸出,最后輸出的內(nèi)容如下所示:
/dir /usr/local/bin/php DaemonMcn.php status Daemon [DaemonMcn] 信息: -------------------------------- master進程狀態(tài) -------------------------------- pid 占用內(nèi)存 處理次數(shù) 開始時間 運行時間 16343 0.75M -- 2018-05-15 09:42:45 0 天 0 時 3 分 12 slaver -------------------------------- slaver進程狀態(tài) -------------------------------- 任務(wù)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 分 -------------------------------------------------------------------------------- 任務(wù)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進程輸出一行信息,統(tǒng)計文件的行數(shù),達到worker進程的行數(shù)之后表示所有worker進程都將信息寫入完畢,否則,每個1s檢測一次。
其他設(shè)計
另外還加了兩個比較實用的功能,一個是worker進程運行時間限制,一個是worker進程循環(huán)處理次數(shù)限制,防止長時間循環(huán)進程出現(xiàn)內(nèi)存溢出等意外情況。時間默認是1小時,運行次數(shù)默認是10w次。
除此之外,也可以支持多任務(wù),每個任務(wù)幾個進程獨立開,統(tǒng)一由master進程管理。
代碼已經(jīng)放到github中,有興趣的可以試試,不支持windows哦,有什么錯誤還望指出來。
看完上述內(nèi)容,你們對PHP中的多進程消費隊列是什么有進一步的了解嗎?如果還想了解更多知識或者相關(guān)內(nèi)容,請關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。