前文再續(xù),就書(shū)接上一回,隨著與Server、TCP、Protocol的邂逅,Swoole終于迎來(lái)了自己的故事,今天,我們來(lái)聊聊Swoole的進(jìn)程模型。
前邊幾篇東西雖然標(biāo)題是Swoole,其主要講的是操作系統(tǒng)、計(jì)算機(jī)網(wǎng)絡(luò)方面的知識(shí),包括一點(diǎn)點(diǎn)筆者自己的私貨,今天終于放假了,咱可以討論一下公的了=。=
并發(fā)之始
之前我們已經(jīng)初步討論的一個(gè)WebServer是怎樣工作的,但之前的例子中,我們看到的服務(wù)都是一個(gè)客戶端與一個(gè)服務(wù)端一問(wèn)一答的場(chǎng)景,但事實(shí)上,絕大部分時(shí)候我們預(yù)期的服務(wù)并不是只向一個(gè)客戶端提供服務(wù),所以,作為一個(gè)成熟的Server,并發(fā)\并行問(wèn)題是必須解決的。
其實(shí),“并發(fā)”和“并行”兩個(gè)概念在計(jì)算機(jī)中是相關(guān)但不同的,有興趣的童鞋可以自己搜索一下,筆者今天僅討論并發(fā)咯。
而軟件開(kāi)發(fā)中,最常見(jiàn)的并發(fā)問(wèn)題解決方案,莫過(guò)于多線程/多進(jìn)程兩種模式了。
微軟的體系中,除了線程,還有“纖程”;而最近非常火爆的“協(xié)程”,則又是另一個(gè)解決方案了。
在《計(jì)算機(jī)組成原理》中我們都學(xué)過(guò),并發(fā)中最迫切需要解決的問(wèn)題之一,就是數(shù)據(jù)的可靠性問(wèn)題,而不同的并發(fā)模型,其并發(fā)數(shù)據(jù)可靠性的機(jī)制往往各有特點(diǎn),因此,在使用Swoole Server\Client的過(guò)程中,其并發(fā)解決方案的模型是必須要了解的,否則使用上很容易出現(xiàn)不符合預(yù)期的結(jié)果。
## 簡(jiǎn)單說(shuō),就是防止臟讀臟寫(xiě)
Swoole目前總共有三種運(yùn)行模式,其中Base模式基本沒(méi)有生產(chǎn)應(yīng)用價(jià)值;協(xié)程模式暫時(shí)還處于預(yù)覽階段;因此,筆者在此想和大家討論的,就是Swoole的多進(jìn)程模式,也是官方目前最推薦用于生產(chǎn)環(huán)境的模式。
事實(shí)上,Swoole曾經(jīng)還有多線程模式,但由于Zend在多線程模式本身的缺陷,在1.6版本后,多線程模式已經(jīng)被關(guān)閉。
進(jìn)程模型
首先,我們還是來(lái)簡(jiǎn)單回顧一下Swoole Server的構(gòu)造函數(shù),之前我們已經(jīng)解決了Host、Port、Protocol的問(wèn)題,這期我們來(lái)看最后一個(gè)參數(shù)的:
$server=new\swoole_server("127.0.0.1",8088,SWOOLE_PROCESS,SWOOLE_SOCK_TCP);
第三個(gè)參數(shù)mode中我們填入的PROCESS,即表示當(dāng)前Server是運(yùn)行于多進(jìn)程模式的。
其他mode的可選參數(shù)可以參考手冊(cè)
然后,我們簡(jiǎn)單實(shí)現(xiàn)一個(gè)沒(méi)有任何內(nèi)容的Server:
$server=new\swoole_server("127.0.0.1",8088,SWOOLE_PROCESS,SWOOLE_SOCK_TCP);
$server->on('connect',function($serv,$fd){ });
$server->on('receive',function($serv,$fd,$from_id,$data){ });
$server->on('close',function($serv,$fd){ });
$server->start();
在啟動(dòng)服務(wù)之后,我們繼續(xù)在Shell中輸入以下命令:
>php swoole_server_demo.php
>pstree -ap|grep swoole_server_demo
|-php,2829 swoole_server_demo.php
||-php,2831 swoole_server_demo.php
||`-php,2836 swoole_server_demo.php
pstree命令可以查看進(jìn)程的樹(shù)模型
從系統(tǒng)的輸出中,我們可以很容看出server其實(shí)有3個(gè)進(jìn)程,進(jìn)程的pid分別是2829、2831、2836,其中2829是2831的父進(jìn)程,而2831又是2836的父進(jìn)程。
所以,其實(shí)我們雖然看起來(lái)只是啟動(dòng)了一個(gè)Server,其實(shí)最后產(chǎn)生的是三個(gè)進(jìn)程。
這三個(gè)進(jìn)程中,所有進(jìn)程的根進(jìn)程,也就是例子中的2829進(jìn)程,就是所謂的Master進(jìn)程;而2831進(jìn)程,則是Manager進(jìn)程;最后的2836進(jìn)程,是Worker進(jìn)程。
基于此,我們簡(jiǎn)單梳理一下,當(dāng)執(zhí)行的start方法之后,發(fā)生了什么:
守護(hù)進(jìn)程模式下,當(dāng)前進(jìn)程fork出Master進(jìn)程,然后退出,Master進(jìn)程觸發(fā)OnMasterStart事件。
Master進(jìn)程啟動(dòng)成功之后,fork出Manager進(jìn)程,并觸發(fā)OnManagerStart事件。
Manager進(jìn)程啟動(dòng)成功時(shí)候,fork出Worker進(jìn)程,并觸發(fā)OnWorkerStart事件。
非守護(hù)進(jìn)程模式下,則當(dāng)前進(jìn)程直接作為Master進(jìn)程工作。
所以,一個(gè)最基礎(chǔ)的Swoole Server,至少需要有3個(gè)進(jìn)程,分別是Master進(jìn)程、Manager進(jìn)程和Worker進(jìn)程。
不要看到進(jìn)程多就覺(jué)得麻煩咯,其實(shí)全賴它們各司其職,才有Swoole重新定義PHP的壯舉。
事實(shí)上,一個(gè)多進(jìn)程模式下的Swoole Server中,有且只有一個(gè)Master進(jìn)程;有且只有一個(gè)Manager進(jìn)程;卻可以有n個(gè)Worker進(jìn)程。
那么這幾個(gè)進(jìn)程之間是怎么協(xié)同工作的呢?我們先暫時(shí)考慮只有一個(gè)Worker的情況。
那么,我們又可以拉出之前寫(xiě)的最簡(jiǎn)單Server,來(lái)看看這個(gè)過(guò)程中,三種進(jìn)程之間是怎么協(xié)作的。
Client主動(dòng)Connect的時(shí)候,Client實(shí)際上是與Master進(jìn)程中的某個(gè)Reactor線程發(fā)生了連接。
當(dāng)TCP的三次握手成功了以后,由這個(gè)Reactor線程將連接成功的消息告訴Manager進(jìn)程,再由Manager進(jìn)程轉(zhuǎn)交給Worker進(jìn)程。
在這個(gè)Worker進(jìn)程中觸發(fā)了OnConnect的方法。
當(dāng)Client向Server發(fā)送了一個(gè)數(shù)據(jù)包的時(shí)候,首先收到數(shù)據(jù)包的是Reactor線程,同時(shí)Reactor線程會(huì)完成組包,再將組好的包交給Manager進(jìn)程,由Manager進(jìn)程轉(zhuǎn)交給Worker。
此時(shí)Worker進(jìn)程觸發(fā)OnReceive事件。
如果在Worker進(jìn)程中做了什么處理,然后再用Send方法將數(shù)據(jù)發(fā)回給客戶端時(shí),數(shù)據(jù)則會(huì)沿著這個(gè)路徑逆流而上。
同樣的故事,隨著認(rèn)識(shí)的加深,會(huì)發(fā)現(xiàn)不一樣的精彩
首先,Master進(jìn)程是一個(gè)多線程進(jìn)程,其中有一組非常重要的線程,叫做Reactor線程(組),每當(dāng)一個(gè)客戶端連接上服務(wù)器的時(shí)候,都會(huì)由Master進(jìn)程從已有的Reactor線程中,根據(jù)一定規(guī)則挑選一個(gè),專門(mén)負(fù)責(zé)向這個(gè)客戶端提供維持鏈接、處理網(wǎng)絡(luò)IO與收發(fā)數(shù)據(jù)等服務(wù)。
以前我們提到的分包拆包等功能也是在這里完成的哦。
而Manager進(jìn)程,某種意義上可以看做一個(gè)代理層,它本身并不直接處理業(yè)務(wù),其主要工作是將Master進(jìn)程中收到的數(shù)據(jù)轉(zhuǎn)交給Worker進(jìn)程,或者將Worker進(jìn)程中希望發(fā)給客戶端的數(shù)據(jù)轉(zhuǎn)交給Master進(jìn)程進(jìn)行發(fā)送。
另外,Manager進(jìn)程還負(fù)責(zé)監(jiān)控Worker進(jìn)程,如果Worker進(jìn)程因?yàn)槟承┮馔鈷炝耍琈anager進(jìn)程會(huì)重新拉起新的Worker進(jìn)程,有點(diǎn)像Supervisor的工作
而這個(gè)特性,也是最終實(shí)現(xiàn)熱重載的核心機(jī)制。
最后就是Worker進(jìn)程了,顧名思義,Worker進(jìn)程其實(shí)就是處理各種業(yè)務(wù)工作的進(jìn)程,Manager將數(shù)據(jù)包轉(zhuǎn)交給Worker進(jìn)程,然后Worker進(jìn)程進(jìn)行具體的處理,并根據(jù)實(shí)際情況將結(jié)果反饋給客戶端。
如果要打個(gè)比方的話,Master進(jìn)程就像業(yè)務(wù)窗口的,Reactor就是前臺(tái)接待員,用戶很多的時(shí)候,后邊的用戶就需要排隊(duì)等待服務(wù);Reactor負(fù)責(zé)與客戶直接溝通,對(duì)客戶的請(qǐng)求進(jìn)行初步的整理(傳輸層級(jí)別的整理——組包);然后,Manager進(jìn)程就是類似項(xiàng)目經(jīng)理的角色,要負(fù)責(zé)將業(yè)務(wù)分配給合適的Worker(例如空閑的Worker);而Worker進(jìn)程就是工人,負(fù)責(zé)實(shí)現(xiàn)具體的業(yè)務(wù)。
實(shí)際上,一對(duì)多投遞這種模式總是在并發(fā)的程序設(shè)計(jì)非常常見(jiàn):1個(gè)Master進(jìn)程投遞n個(gè)Reactor線程;1個(gè)Manager進(jìn)程投遞n個(gè)Worker進(jìn)程。
現(xiàn)在,我們來(lái)看看一個(gè)簡(jiǎn)單的多進(jìn)程Swoole Server的幾個(gè)基本配置:
$server->set([
"daemonize"=>true,
"reactor_num"=>2,
"worker_num"=>4,]
);
$server->start();
reactor_num:表示Master進(jìn)程中,Reactor線程總共開(kāi)多少個(gè),注意,這個(gè)可不是越多越好,因?yàn)橛?jì)算機(jī)的CPU是有限的,所以一般設(shè)置為與CPU核心數(shù)量相同,或者兩倍即可。
worker_num:表示啟動(dòng)多少個(gè)Worker進(jìn)程,同樣,Worker進(jìn)程數(shù)量不是越多越好,仍然設(shè)置為與CPU核心數(shù)量相同,或者兩倍即可。
讀書(shū)萬(wàn)卷不若自己親手寫(xiě)一行,試驗(yàn)一下這個(gè)配置下,Server啟動(dòng)后,pstree的結(jié)構(gòu)。
進(jìn)程模型與數(shù)據(jù)共享
在以前的討論中,我們最常接觸到的回調(diào)方法如下:
OnConnect
OnReceive
OnClose
如上一節(jié)所說(shuō),這三個(gè)回調(diào)其實(shí)都是在Worker進(jìn)程中發(fā)生的,而了解了進(jìn)程模型以后,我們可以認(rèn)識(shí)一下更多的回調(diào)方法了:
// 以下回調(diào)發(fā)生在Master進(jìn)程
$server->on("start",function(\swoole_server$server){
echo"On master start.";
});
$server->on('shutdown',function(\swoole_server$server){
echo"On master shutdown.";
});
// 以下回調(diào)發(fā)生在Manager進(jìn)程
$server->on('ManagerStart',function(\swoole_server$server){
echo"On manager start.";
});
$server->on('ManagerStop',function(\swoole_server$server){
echo"On manager stop.";
});
// 以下回調(diào)也發(fā)生在Worker進(jìn)程
$server->on('WorkerStart',function(\swoole_server$server,$worker_id){
echo"Worker start";
});
$server->on('WorkerStop',function(\swoole_server$server,$worker_id){
echo"Worker stop";
});
$server->on('WorkerError',function(\swoole_server$server,$worker_id,$worker_pid,$exit_code){
echo"Worker error";
});
OK,現(xiàn)在我們更新一下我們的測(cè)試代碼,以展示不同進(jìn)程之間,數(shù)據(jù)共享的特點(diǎn)和關(guān)系:
$server=new\swoole_server("127.0.0.1",8088,SWOOLE_PROCESS,SWOOLE_SOCK_TCP);
$server->on('connect',function($serv,$fd){ });
$server->on('receive',function($serv,$fd,$from_id,$data){ });
$server->on('close',function($serv,$fd){ });
// 在交互進(jìn)程中放入一個(gè)數(shù)據(jù)。
$server->BaseProcess="I'm base process."http:// 為了便于閱讀,以下回調(diào)方法按照被起調(diào)的順序組織
// 1. 首先啟動(dòng)Master進(jìn)程
$server->on("start",function(\swoole_server$server){
echo"On master start.".PHP_EOL;
// 先打印在交互進(jìn)程寫(xiě)入的數(shù)據(jù)
echo"server->BaseProcess =".$server->BaseProcess.PHP_EOL;
// 修改交互進(jìn)程中寫(xiě)入的數(shù)據(jù)
$server->BaseProcess="I'm changed by master.";
// 在Master進(jìn)程中寫(xiě)入一些數(shù)據(jù),以傳遞給Manager進(jìn)程。
$server->MasterToManager="Hello manager, I'm master.";
});
// 2. Master進(jìn)程拉起Manager進(jìn)程
$server->on('ManagerStart',function(\swoole_server$server){
echo"On manager start.".PHP_EOL;
// 打印,然后修改交互進(jìn)程中寫(xiě)入的數(shù)據(jù)
echo"server->BaseProcess =".$server->BaseProcess.PHP_EOL;
$server->BaseProcess="I'm changed by manager.";
// 打印,然后修改在Master進(jìn)程中寫(xiě)入的數(shù)據(jù)
echo"server->MasterToManager =".$server->MasterToManager.PHP_EOL;
$server->MasterToManager="This value has changed in manager.";
// 寫(xiě)入傳遞給Worker進(jìn)程的數(shù)據(jù)
$server->ManagerToWorker="Hello worker, I'm manager.";
});
// 3. Manager進(jìn)程拉起Worker進(jìn)程
$server->on('WorkerStart',function(\swoole_server$server,$worker_id){
echo"Worker start".PHP_EOL;
// 打印在交互進(jìn)程寫(xiě)入,然后在Master進(jìn)程,又在Manager進(jìn)程被修改的數(shù)據(jù)
echo"server->BaseProcess =".$server->BaseProcess.PHP_EOL;
// 打印,并修改Master寫(xiě)入給Manager的數(shù)據(jù)
echo"server->MasterToManager =".$server->MasterToManager.PHP_EOL;
$server->MasterToManager="This value has changed in worker.";
// 打印,并修改Manager傳遞給Worker進(jìn)程的數(shù)據(jù)
echo"server->ManagerToWorker =".$server->ManagerToWorker.PHP_EOL;
$server->ManagerToWorker="This value is changed in worker.";
});
// 4. 正常結(jié)束Server的時(shí)候,首先結(jié)束Worker進(jìn)程
$server->on('WorkerStop',function(\swoole_server$server,$worker_id){
echo"Worker stop".PHP_EOL;
// 分別打印之前的數(shù)據(jù)
echo"server->ManagerToWorker =".$server->ManagerToWorker.PHP_EOL;
echo"server->MasterToManager =".$server->MasterToManager.PHP_EOL;
echo"server->BaseProcess =".$server->BaseProcess.PHP_EOL;
});
// 5. 緊接著結(jié)束Manager進(jìn)程
$server->on('ManagerStop',function(\swoole_server$server){
echo"Manager stop.".PHP_EOL;
// 分別打印之前的數(shù)據(jù)
echo"server->ManagerToWorker =".$server->ManagerToWorker.PHP_EOL;
echo"server->MasterToManager =".$server->MasterToManager.PHP_EOL;
echo"server->BaseProcess =".$server->BaseProcess.PHP_EOL;
});
// 6. 最后回收Master進(jìn)程
$server->on('shutdown',function(\swoole_server$server){
echo"Master shutdown.".PHP_EOL;
// 分別打印之前的數(shù)據(jù)
echo"server->ManagerToWorker =".$server->ManagerToWorker.PHP_EOL;
echo"server->MasterToManager =".$server->MasterToManager.PHP_EOL;
echo"server->BaseProcess =".$server->BaseProcess.PHP_EOL;
});
$server->start();
這段程序測(cè)試的時(shí)候,我們需要開(kāi)兩個(gè)會(huì)話,第一個(gè)會(huì)話用于執(zhí)行并打印輸出;第二個(gè)會(huì)話用于使用kill命令通知Server執(zhí)行一些工作,然后我們看看輸出的結(jié)果:
# 在會(huì)話一中>php swoole_server_demo.phpOn master start.server->BaseProcess = I'm base process.
On manager start.
server->BaseProcess = I'm base process.server->MasterToManager = Worker startserver->BaseProcess = I'm base process.
server->MasterToManager =
server->ManagerToWorker =
從Manager start和Worker start中的輸出,我們發(fā)現(xiàn)BaseProcess、MasterToManager、ManagerToWorker并沒(méi)有分別在Master、Manager中被修改,并在子進(jìn)程中打印出被修改后的結(jié)果,這是為什么呢?別急,我們繼續(xù)做個(gè)實(shí)驗(yàn)。
打開(kāi)會(huì)話二,先執(zhí)行pstree -ap|grep php找到剛剛啟動(dòng)的Server的Master進(jìn)程的PID,然后向該進(jìn)程發(fā)送-10信號(hào),然后再次實(shí)行pstree命令看看:
>pstree -ap|grep php
||`-php,5512 swoole_server_demo.php
|||-php,5513 swoole_server_demo.php
|||`-php,5515 swoole_server_demo.php
>kill-10 5512
>pstree -ap|grep php
||`-php,5512 swoole_server_demo.php
|||-php,5513 swoole_server_demo.php
|||`-php,5522 swoole_server_demo.php
-10信號(hào)的作用是,要求Swoole重啟Worker服務(wù),我們會(huì)發(fā)現(xiàn)原來(lái)的Worker[5515]被干掉了,而產(chǎn)生了一個(gè)新的Worker[5522],此時(shí)如果我們切換回會(huì)話一,會(huì)發(fā)現(xiàn)增加了以下的輸出:
[2016-10-03 02:00:26$5513.0] ? NOTICE ?Server is reloading now.Worker stopserver->ManagerToWorker = This value is changedinworker.server->MasterToManager = This value has changedinworker.server->BaseProcess = I'm base process.
Worker start
server->BaseProcess = I'm changed by manager.server->MasterToManager = This value has changedinmanager.server->ManagerToWorker = Hello worker, I'm manager.
首先是Swoole自己打印的日志信息,Server正在被reloading,然后Worker[5515]被終止,執(zhí)行了WorkerStop的方法,此時(shí)WorkerStop輸出的值我們可以看出,在WorkerStart中的賦值都是生效了的;然后,新的Worker[5522]被啟動(dòng)了,重新觸發(fā)WorkerStart方法,這時(shí)我們發(fā)現(xiàn),BaseProcess、MasterToManager和ManagerToWorker都分別被打印了出來(lái)?這是什么原因呢?
原因在方法被執(zhí)行的順序上,我們前文中的進(jìn)程起調(diào)順序并沒(méi)有問(wèn)題,但有些地方我們要做一點(diǎn)小小的細(xì)化:
Master進(jìn)程被啟動(dòng)。
Manager進(jìn)程Master進(jìn)程fork出來(lái)。
Worker進(jìn)程被Manager進(jìn)程fork出來(lái)。
MasterStart被回調(diào)。
ManangerStart被回調(diào)。
WorkerStart被回調(diào)。
也就是說(shuō),三種進(jìn)程的OnStart方法被回調(diào)的時(shí)候都有一定的延遲,底層事實(shí)上已經(jīng)完工了fork的行為,才回調(diào)的,因此,默認(rèn)啟動(dòng)的時(shí)候,我們?cè)贠nMasterStart、OnManagerStart中寫(xiě)入的數(shù)據(jù)并不能按預(yù)期被fork到Manager進(jìn)程或者Worker進(jìn)程。
然后,我們執(zhí)行了kill -10重新拉起Worker進(jìn)程的時(shí)候,此時(shí)Worker進(jìn)程仍然是由Mananger進(jìn)程fork出來(lái)的,但此時(shí)ManangerStart已經(jīng)被執(zhí)行過(guò)了,所以我們會(huì)發(fā)現(xiàn)在OnWorkerStart的時(shí)候,輸出變成了ManagerStart中修改過(guò)的內(nèi)容。
OK,現(xiàn)在我們回到Shell會(huì)話二,向Master進(jìn)程發(fā)送kill -15命令
>kill-15 5512
然后回到會(huì)話一,我們發(fā)現(xiàn)輸出增加了如下的內(nèi)容:
[2016-10-03 02:17:35#5512.0] ? NOTICE ?Server is shutdown now.Worker stopserver->ManagerToWorker=This value is changedinworker.server->MasterToManager=This value has changedinworker.server->BaseProcess=I'm changed by manager.
Manager stop.
server->ManagerToWorker = Hello worker, I'm manager.server->MasterToManager=This value has changedinmanager.server->BaseProcess=I'm changed by manager.
Master shutdown.
server->ManagerToWorker =
server->MasterToManager = Hello manager, I'm master.server->BaseProcess=I'm changed by master.
kill -15命令是通知Swoole正常終止服務(wù),首先停止Worker進(jìn)程,觸發(fā)OnWorkerStop回調(diào),此時(shí)我們輸出的內(nèi)容懂事我們?cè)赪orkerStart中修改過(guò)的版本。
然后停止Manager進(jìn)程,這時(shí)候要留意,我們?cè)赪orker中做的所有操作并沒(méi)有反應(yīng)在Manager進(jìn)程上,OnManagerStop的輸出仍然是在OnManagerStart中賦值的內(nèi)容。
最后停止Master進(jìn)程,也會(huì)有相同的事情發(fā)生。
通過(guò)以上實(shí)驗(yàn),展示了多進(jìn)程Server的兩個(gè)重要特性:
父進(jìn)程fork出子進(jìn)程的時(shí)候,子進(jìn)程會(huì)拷貝一份父進(jìn)程的所有數(shù)據(jù)。
各個(gè)進(jìn)程之間的數(shù)據(jù)一般情況下是不共享內(nèi)存的。
所以,學(xué)習(xí)Swoole的進(jìn)一步需求就是,要弄清楚各個(gè)回調(diào)方法分別是在哪個(gè)進(jìn)程中發(fā)生的,且發(fā)生的順序是什么。
這兩個(gè)特性會(huì)引起什么問(wèn)題呢?如果沒(méi)有弄清楚當(dāng)前的代碼是在哪個(gè)進(jìn)程執(zhí)行的,很有可能就會(huì)引起數(shù)據(jù)的錯(cuò)誤,而多個(gè)進(jìn)程之間進(jìn)行協(xié)作的話,不能像以往的PHP開(kāi)發(fā)一樣,通過(guò)共享變量實(shí)現(xiàn)。
以上例子中,為了便于輸出,沒(méi)有啟用守護(hù)進(jìn)程模式,所以交互進(jìn)程與Master進(jìn)程是同一個(gè)進(jìn)程,有興趣的童鞋歡迎在守護(hù)進(jìn)程下實(shí)驗(yàn)。
所以,這又引出了下一個(gè)問(wèn)題,多進(jìn)程模型中,內(nèi)存不能共享,那進(jìn)程之間應(yīng)該怎么通訊呢?限于篇幅,今天我們先討論到這里,下一期我們?cè)賮?lái)探討這個(gè)問(wèn)題。