使用 swoole 實現程式的守護(三)

Kingmax發表於2019-09-01

在上一篇文章《使用 swoole 實現程式的守護(二)》中,實現了一個能透過讀取配置同時守護多個指令碼的 Daemon 類。
本文嘗試繼續擴充套件這個 Daemon 類,讓它能夠在不重啟程式的情況下實現配置的過載。
最常見的一種熱過載的方式,就是向程式傳送系統訊號,當程式監聽到相應訊號時,即執行重新載入配置到程式空間的記憶體即可。
像 Nginx 和 Caddy 這種高效能的常駐程式伺服器,為了避免重啟程式導致的伺服器不可用,也是透過這種方式來實現熱過載的。

在 Linux 的 bash 可以透過 kill -l 命令來檢視所有支援的程式訊號:

1) SIGHUP    2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF 28) SIGWINCH    29) SIGIO   30) SIGPWR
31) SIGSYS  34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

我們可以透過選擇監聽使用者自定義訊號 SIGUSR1 來實現。

PHP 官方提供了兩個函式來處理程式號,分別是:

  1. pcntl_signal(SIGINT, 'signalHandler'); 用於註冊收到訊號後的處理函式。
  2. pcntl_signal_dispatch() 用於呼叫每個等待訊號透過 pcntl_signal() 註冊的處理器。

那麼,註冊訊號處理器的示例程式碼可以類似如下:

pcntl_signal(SIGHUP, function () {
    printf("收到過載配置訊號\n");
    $this->loadWorkers();
    printf("過載配置完成\n");
});

而排程訊號處理器可以在每次檢查程式回收的時候執行:

while (1) {
    pcntl_signal_dispatch();
    if ($ret = Process::wait(false)) {
        // todo something
    }
}

於是,Daemon 類可以擴充套件如下:

namespace App;

use Swoole\Process;

class Daemon
{
    /**
     * @var string
     */
    private $configPath;

    /**
     * @var Command[]
     */
    private $commands;

    /**
     * @var Worker[]
     */
    private $workers = [];

    public function __construct(string $configPath)
    {
        $this->configPath = $configPath;
    }

    public function run()
    {
        $this->loadWorkers();

        pcntl_signal(SIGHUP, function () {
            printf("收到過載配置訊號\n");
            $this->loadWorkers();
            printf("過載配置完成\n");
        });

        $this->waitAndRestart();
    }

    /**
     * 收回程式並重啟
     */
    private function waitAndRestart()
    {
        while (1) {
            pcntl_signal_dispatch();
            if ($ret = Process::wait(false)) {

                $retPid = intval($ret["pid"] ?? 0);
                $index = $this->getIndexOfWorkerByPid($retPid);

                if (false !== $index) {
                    if ($this->workers[$index]->isStopping()) {
                        printf("[%s] 移除守護 %s\n", date("Y-m-d H:i:s"), $this->workers[$index]->getCommand()->getId());

                        unset($this->workers[$index]);
                    } else {
                        $command = $this->workers[$index]->getCommand()->getCommand();
                        $newPid = $this->createWorker($command);
                        $this->workers[$index]->setPid($newPid);

                        printf("[%s] 重新拉起 %s\n", date("Y-m-d H:i:s"), $this->workers[$index]->getCommand()->getId());
                    }
                }

            }
        }
    }

    /**
     * 載入 workers
     */
    private function loadWorkers()
    {
        $this->parseConfig();
        foreach ($this->commands as $command) {
            if ($command->isEnabled()) {
                printf("[%s] 啟用 %s\n", date("Y-m-d H:i:s"), $command->getId());
                $this->startWorker($command);
            } else {
                printf("[%s] 停用 %s\n", date("Y-m-d H:i:s"), $command->getId());
                $this->stopWorker($command);
            }
        }
    }

    /**
     * 啟動 worker
     * @param Command $command
     */
    private function startWorker(Command $command)
    {
        $index = $this->getIndexOfWorker($command->getId());
        if (false === $index) {
            $pid = $this->createWorker($command->getCommand());

            $worker = new Worker();
            $worker->setPid($pid);
            $worker->setCommand($command);
            $this->workers[] = $worker;
        }
    }

    /**
     * 停止 worker
     * @param Command $command
     */
    private function stopWorker(Command $command)
    {
        $index = $this->getIndexOfWorker($command->getId());
        if (false !== $index) {
            $this->workers[$index]->setStopping(true);
        }
    }

    /**
     *
     * @param $commandId
     * @return bool|int|string
     */
    private function getIndexOfWorker(string $commandId)
    {
        foreach ($this->workers as $index => $worker) {
            if ($commandId == $worker->getCommand()->getId()) {
                return $index;
            }
        }
        return false;
    }

    /**
     * @param $pid
     * @return bool|int|string
     */
    private function getIndexOfWorkerByPid($pid)
    {
        foreach ($this->workers as $index => $worker) {
            if ($pid == $worker->getPid()) {
                return $index;
            }
        }
        return false;
    }

    /**
     * 解析配置檔案
     */
    private function parseConfig()
    {
        if (is_readable($this->configPath)) {
            $iniConfig = parse_ini_file($this->configPath, true);

            $this->commands = [];
            foreach ($iniConfig as $id => $item) {
                $commandLine = strval($item["command"] ?? "");
                $enabled = boolval($item["enabled"] ?? false);

                $command = new Command();
                $command->setId($id);
                $command->setCommand($commandLine);
                $command->setEnabled($enabled);
                $this->commands[] = $command;
            }
        }
    }

    /**
     * 建立子程式,並返回子程式 id
     * @param $command
     * @return int
     */
    private function createWorker(string $command): int
    {
        $process = new Process(function (Process $worker) use ($command) {
            $worker->exec('/bin/sh', ['-c', $command]);
        });
        return $process->start();
    }
}

注意:為了程式碼簡潔,以上程式碼新增了一個 Worker 類如下:

class Worker
{
    /**
     * @var Command
     */
    private $command;

    /**
     * @var int
     */
    private $pid;

    /**
     * @var bool
     */
    private $stopping;

    // ... 以下省略了 Get Set 方法
}

最後,這個 Daemon 類的使用方法,仍然是:

$pid = posix_getpid();
printf("主程式號: {$pid}\n");

$configPath = dirname(__DIR__) . "/config/daemon.ini";

$daemonMany = new Daemon($configPath);
$daemonMany->run();

那麼,假如我們知道 Daemon 程式正在執行的程式號為 522,則可透過以下命令來實現配置的熱過載:

kill -USR1 522

到目前為止,這個 Daemon 類可以說是功能完備了,但是仍有可以改進的地方,比如,有沒有辦法不需要使用者手動去給程式傳送訊號來過載配置,由程式自己去自動應用最新的配置呢?

下一篇文章 使用 swoole 實現程式的守護(四)將 swoole 的協程嘗試繼續擴充套件這個 Daemon 類。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
看看自己是不是一個靠譜的程式設計師,來做題試試。job.xyh.io

相關文章