使用 swoole 实现进程的守护(三)

在上一篇文章《使用 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
Kingmax
讨论数量: 3

请教楼主一个问题,在swoole文档中有这样一句描述使用Process作为监控父进程,创建管理子进程时,父类必须注册信号SIGCHLD对退出的进程执行wait,否则子进程一旦被kill会引起父进程退出,为些我测试过以下两种情况:

  1. 在主进程中使用Process创建子进程,然后kill这个子进程
$process = new Process(function () {
    while (true) {
        echo 'Process running ' . posix_getpid() . PHP_EOL;
        sleep(1);
    }
});

$process->start();

while (1) {
    echo 'Main running' . PHP_EOL;
    sleep(1);
}

echo 'Main end' . PHP_EOL;
  1. 在主进程中使用Process创建子进程,然后在子进程中使用Process再创建孙进程,然后kill这个孙进程
$process = new Process(function () {

    $subProcess = new Process(function () {
        while (true) {
            echo 'Sub process running ' . posix_getpid() . PHP_EOL;
            sleep(1);
        }
    });
    $subProcess->start();

    while (true) {
        echo 'Process running ' . posix_getpid() . PHP_EOL;
        sleep(1);
    }
});

$process->start();

while (1) {
    echo 'Main running' . PHP_EOL;
    sleep(1);
}

echo 'Main end' . PHP_EOL;

测试发现这两种情况并没有引起父进程的退出,所以很奇怪究竟在什么情况下才应该注册SIGCHLD信号?

4年前 评论
Kingmax

@诺大的院子
默认情况下,父进程并不需要注册 SIGCHLD 信号,只需要执行 wait 方法将退出的子进程资源进行回收,避免产生僵尸进程即可。
你可以尝试一下将子进程的 while(true) 死循环改为非死循环,当此子进程退出后就会成为僵尸进程。
子进程退出时会向父进程发送 SIGCHLD 信号,而父进程对此信号默认是不处理的,除非注册了 SIGCHLD 信号处理函数。
所以文档所说的,子进程一旦被 kill 会引起父进程退出,这个说法是有点让人费解了。

另外,文档的另一处说法应该比较准确 https://wiki.swoole.com/wiki/page/214.html

使用Process作为监控父进程,创建管理子进程时,父类必须注册信号SIGCHLD对退出的进程执行wait,否则子进程退出时会变成僵尸进程

4年前 评论

使用Process作为监控父进程,创建管理子进程时,父类必须注册信号SIGCHLD对退出的进程执行wait,否则子进程退出时会变成僵尸进程

@Kingmax 这句话也是有问题吧,如果父进程并不需要处理其它事情,只是专心等待子进程结束,直接调用wait方法就可以了,并不需要监听SIGCHLD信号。

4年前 评论
Kingmax (楼主) 4年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!