使用 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 官方提供了两个函数来处理进程号,分别是:
- pcntl_signal(SIGINT, 'signalHandler'); 用于注册收到信号后的处理函数。
- 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 协议》,转载必须注明作者和本文链接
请教楼主一个问题,在swoole文档中有这样一句描述
使用Process作为监控父进程,创建管理子进程时,父类必须注册信号SIGCHLD对退出的进程执行wait,否则子进程一旦被kill会引起父进程退出
,为些我测试过以下两种情况:测试发现这两种情况并没有引起父进程的退出,所以很奇怪究竟在什么情况下才应该注册
SIGCHLD
信号?@诺大的院子
默认情况下,父进程并不需要注册
SIGCHLD
信号,只需要执行wait
方法将退出的子进程资源进行回收,避免产生僵尸进程即可。你可以尝试一下将子进程的
while(true)
死循环改为非死循环,当此子进程退出后就会成为僵尸进程。子进程退出时会向父进程发送
SIGCHLD
信号,而父进程对此信号默认是不处理的,除非注册了SIGCHLD
信号处理函数。所以文档所说的,子进程一旦被 kill 会引起父进程退出,这个说法是有点让人费解了。
另外,文档的另一处说法应该比较准确 https://wiki.swoole.com/wiki/page/214.html :
使用Process作为监控父进程,创建管理子进程时,父类必须注册信号SIGCHLD对退出的进程执行wait,否则子进程退出时会变成僵尸进程
@Kingmax 这句话也是有问题吧,如果父进程并不需要处理其它事情,只是专心等待子进程结束,直接调用
wait
方法就可以了,并不需要监听SIGCHLD
信号。