11. swoole基礎-swoole之守護進程、信號和平滑重啟


守護進程

之前我們介紹過進程和線程,今天我們再來談一談守護進程。

無論是server初識還是task邂逅,不管我們程序寫的多么精彩,都沒有辦法把項目應用到實際業務中,因為我們知道,把運行server的終端關閉之后,server也就不復存在了。

那有沒有一種辦法說僅且當電腦關機的時候才終止server的運行,不管終端怎么玩,server也能夠在后台持續運行呢?

守護進程(daemon)就是一種長期生存的進程,它不受終端的控制,可以在后台運行。其實我們之前也有了解,比如說nginx,fpm等一般都是作為守護進程在后台提供服務。

熟悉linux的同學可能知道,我們可以利用nohup命令讓程序在后台跑。swoole官方也為我們提供了配置選項daemonize,默認不啟用守護進程,若要開啟守護進程,daemonize設置為true即可。

守護進程有優點,必然也存在缺點。我們啟用守護進程后,server內所有的標准輸出都會被丟棄,這樣的話我們也就無法跟蹤進程在運行過程中是否異常之類的錯誤信息了。為方便起見,swoole為我們提供了另一個配置選項log_file,我們可以指定日志路徑,這樣swoole在運行時就會把所有的標准輸出統統記載到該文件內。

信號

學習本文之前,我們了解到,swoole是常駐內存的,若想讓修改后的代碼生效,就必須Ctrl+C,然后再重啟server。對於守護進程化的server呢?了解過kill命令的同學要說了,我直接把它干掉,然后終端下再重啟,就可以了。

事實上,對於線上繁忙的server,如果你直接把它干掉了,可能某個進程剛好就只處理了一半的數據,對於天天來回倒騰的你來說,面對錯亂的數據你不頭疼,DBA也想long死你!

這個時候我們就需要考慮如何平滑重啟server的問題了。所謂的平滑重啟,也叫“熱重啟”,就是在不影響用戶的情況下重啟服務,更新內存中已經加載的php程序代碼,從而達到對業務邏輯的更新。

swoole為我們提供了平滑重啟機制,我們只需要向swoole_server的主進程發送特定的信號,即可完成對server的重啟。

我們在進程模型一文中介紹主進程的主線程的時候也提到過主線程的主要任務之一就是處理信號。

那什么是信號呢?

信號是軟件中斷,每一個信號都有一個名字。通常,信號的名字都以“SIG”開頭,比如我們最熟悉的Ctrl+C就是一個名字叫“SIGINT”的信號,意味着“終端中斷”。

平滑重啟

在swoole中,我們可以向主進程發送各種不同的信號,主進程根據接收到的信號類型做出不同的處理。比如下面這幾個

  • SIGTERM,一種優雅的終止信號,會待進程執行完當前程序之后中斷,而不是直接干掉進程
  • SIGUSR1,將平穩的重啟所有的Worker進程
  • SIGUSR2,將平穩的重啟所有的Task進程
    如果我們要實現重啟server,只需要向主進程發送SIGUSR1信號就好了。

平滑重啟的原理是當主進程收到SIGUSR1信號時,主進程就會向一個子進程發送安全退出的信號,所謂的安全退出的意思是主進程並不會直接把Worker進程殺死,而是等這個子進程處理完手上的工作之后,再讓其光榮的“退休”,最后再拉起新的子進程(重新載入新的PHP程序代碼)。然后再向其他子進程發送“退休”命令,就這樣一個接一個的重啟所有的子進程。

我們注意到,平滑重啟實際上就是讓舊的子進程逐個退出並重新創建新的進程。為了在平滑重啟時不影響到用戶,這就要求進程中不要保存用戶相關的狀態信息,即業務進程最好是無狀態的,避免由於進程退出導致信息丟失。

感覺很美好的樣子,凡是重啟只要簡單的向主進程發送信號就完事了唄。

理想很豐滿,現實並非如此。

在swoole中,重啟只能針對Worker進程啟動之后載入的文件才有效!什么意思呢,就是說只有在onWorkerStart回調之后加載的文件,重啟才有意義。在Worker進程啟動之前就已經加載到內存中的文件,如果想讓它重新生效,還是只能乖乖的關閉server再重啟。

說了這么多,我們寫個例子看看到底怎么樣向主進程發送SIGUSR1信號以便有效重啟Worker進程。

首先我們創建一個Test類,用於處理onReceive回調的數據,為什么要把onReceive回調的業務拿出來單獨寫,看完例子你就明白了。

<?php

class Test
{
    public function run($data)
    {
        echo $data;
    }
}

Test::run方法中,我們第一步僅僅是echo輸出swoole_server接收到的數據。

當前目錄下我們創建一個swoole_server的類NoReload.php

<?php

require_once("Test.php");

class NoReload
{
    private $_serv;
    private $_test;

    /**
     * init
     */
    public function __construct()
    {
        $this->_serv = new Swoole\Server("127.0.0.1", 9501);
        $this->_serv->set([
            'worker_num' => 1,
        ]);
        $this->_serv->on('Receive', [$this, 'onReceive']);

        $this->_test = new Test;
    }
    /**
     * start server
     */
    public function start()
    {
        $this->_serv->start();
    }
    public function onReceive($serv, $fd, $fromId, $data)
    {
        $this->_test->run($data);
    }
}

$noReload = new NoReload;
$noReload->start();

特別提醒:我們在初始化swoole_server的時候的寫法是命名空間的寫法

new Swoole\Server

該種風格的寫法等同於下划線寫法 ,swoole對這兩種風格的寫法都支持

new swoole_server

此外我們看下server的代碼邏輯:類定義之前require_once了Test.php,初始化的時候設置了一個Worker進程,注冊了NoReload::onReceive方法為swoole_server的onReceive回調,在onReceive回調內接收到的數據傳遞給了Test::run方法處理。

接下來我們寫一個client腳本測試下運行結果

<?php

$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_SYNC);
$client->connect('127.0.0.1', 9501) || exit("connect failed. Error: {$client->errCode}\n");

// 向服務端發送數據
$client -> send("Just a test.\n");
$client->close();

客戶端的測試代碼也很簡單,連接server並向server發一個字符串信息

img

正常,沒發現問題,server所在終端輸出了客戶端send的內容。

在server不動的情況下我們修改下Test.php,代碼如下

<?php

class Test
{
    public function run($data)
    {
        // echo $data;
        $data = json_decode($data, true);
        if (!is_array($data)) {
            echo "server receive \$data format error.\n";
            return ;
        } 
        var_dump($data);
    }
}

原先echo直接輸出,現在我們改了下Test的代碼,如果接收到的數據經過json_decode處理后不是數組,就返回一段內容並結束,否則打印receive到的數據

如果這個時候我們不對server進行重啟,運行client的結果肯定還是一樣的,看下結果

img

server端新的代碼未生效,如果Test.php新的代碼生效了,會在server所在終端輸出“server receive $data format error.”,這符合我們的認知。

下面我們通過ps命令查看下左側server的主進程的pid,然后通過kill命令向該進程發送SIGUSR1信號,看看結果如何

img

結果發現即使向主進程發送了SIGUSR1信號,但是左側server終端顯示的依然是未生效的php代碼,這也是對的,因為我們說過新的程序代碼只針對在onWorkerStart回調之后才加載進來的php文件才能生效,我們事例中Test.php是在class定義之前就加載進來了,所以肯定不生效。

我們新建一個Reload.php文件,把server的代碼修改如下

<?php

class Reload
{
    private $_serv;
    private $_test;

    /**
     * init
     */
    public function __construct()
    {
        $this->_serv = new Swoole\Server("127.0.0.1", 9501);
        $this->_serv->set([
            'worker_num' => 1,
        ]);
        $this->_serv->on('Receive', [$this, 'onReceive']);
        $this->_serv->on('WorkerStart', [$this, 'onWorkerStart']);
    }
    /**
     * start server
     */
    public function start()
    {
        $this->_serv->start();
    }
    public function onWorkerStart($serv, $workerId)
    {
        require_once("Test.php");
        $this->_test = new Test;
    }
    public function onReceive($serv, $fd, $fromId, $data)
    {
        $this->_test->run($data);
    }
}

$reload = new Reload;
$reload->start();

仔細觀察,我們僅僅移除了在類定義之前引入Test.php以及在__construct中new Test的操作。

而是在__construct方法中增加了onWorkerStart回調,並在該回調內引入Test.php並初始化Test類。

Test.php的代碼,我們仍然先后用上面的兩處代碼為例,運行client看下結果

img

圖例右側運行client過程中,給主進程發送SIGUSR1信號之前,記得修改Test.php的代碼,然后再運行client腳本測試。

結果我們發現,在給主進程發送SIGUSR1信號之后,Test.php的新代碼生效了。這也便實現了熱重啟的效果。

如此,我們在Test.php中不論如何更新代碼,只需要找到主進程的PID,向它發送SIGUSR1信號即可。同理,SIGUSR2信號是只針對Task進程的,后面可以自行測試下。

熱重啟的效果實現了,現在針對Reload.php的server,讓該server進程守護化看看。

__construct中,$serv->set代碼修改如下

$this->_serv->set([   
    'worker_num' => 1,          
    'daemonize' => true,          
    'log_file' => __DIR__ . '/server.log',
]);

我們在終端下在運行下Reload.php

$ php Reload.php

代碼好像突然就執行完畢了,現在終端不“卡”着了,終端的執行權又重新交給了終端,我們的server呢?怎么回事?

其實這就是守護進程化的概念,我們開啟的swoole_server進程已經在后端跑着了,不信我們ps看下

$ ps aux | grep Reload
manks 14117   xxx...    1:51下午   0:07.49 php Reload.php
manks 14117   xxx...    1:51下午   0:07.47 php Reload.php
manks 36807   xxx...    1:54下午   0:00.00 grep Reload
manks 14116   xxx...    1:51下午   0:00.01 php Reload.php

發現還真有幾個進程在跑着。

不光如此,我們再看下當前目錄下是不是有一個server.log的日志文件,我們在swoole_server::set的log_file配置項指定了日志文件就是它,那么在server運行的過程中,所有的標准輸出都會輸出到這個文件中,此時我們再運行下client.php,然后打開server.log看看是不是終端輸出的結果都顯示在該文件內了呢?毋庸置疑。

文中代碼已上傳至github,可以點擊查看


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM