在上一篇文章《使用 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 类。