workerman平滑重启原理
前言
只针对linux下的重启解析,windows下没有重启和平滑重启的策略。代码区域只展示方法名称和部分的代码片段,完整代码需要自行参阅workerman
项目。下述的名词“主进程”和“父进程”是同一个意思。
梳理流程
命令入口:
php workerman.php reload -g
进入
\Workerman\Worker::parseCommand()
:case 'reload': if($mode === '-g'){ $sig = \SIGUSR2; }else{ $sig = \SIGUSR1; } \posix_kill($master_pid, $sig); exit;
workerman
主进程
收到进程信号,进入\Workerman\Worker::signalHandler()
:// Reload. case \SIGUSR2: case \SIGUSR1: static::$_gracefulStop = $signal === \SIGUSR2; static::$_pidsToRestart = static::getAllWorkerPids(); static::reload();
workerman
主进程
开始向子进程
逐个发送退出信号,进入\Workerman\Worker::reload()
:// For master process. if (static::$_masterPid === \posix_getpid()) { // 主进程要做的事情 // 主进程开始收集可以重启的待重启子进程栈,然后逐个下发退出信号,子进程重启完成后就将pid出栈,继续处理下一个子进程 // 主进程会收到平滑重启的命令,即此时static::$_gracefulStop=true } // For child processes. else { // 子进程要做的事情 // 子进程是不会收到平滑重启命令的,即此时static::$_gracefulStop=false }
workerman
子进程
开始处理退出业务,进入\Workerman\Worker::stopAll()
:// For master process. if (\DIRECTORY_SEPARATOR === '/' && static::$_masterPid === \posix_getpid()) { // 主进程退出的逻辑 // 在平滑重启的业务中,主进程是不会退出的,所以可以不理会这里的业务。 } // For child processes. else { // Execute exit. // 子进程逻辑 foreach (static::$_workers as $worker) { if(!$worker->stopping){ $worker->stop();// 这里做了什么:暂停接收数据、关闭服务端套接字链接、关闭客户端链接。 $worker->stopping = true; } } // 上述说明了子进程static::$_gracefulStop属性值是false if (!static::$_gracefulStop || ConnectionInterface::$statistics['connection_count'] <= 0) { static::$_workers = array(); if (static::$globalEvent) { static::$globalEvent->destroy();// 关闭事件轮询器 } try { exit($code);// 退出子进程,并向主进程发送一个退出代码。 } catch (Exception $e) { } } }
这时候子进程已经完全退出,父进程收到子进程退出的信号,进入
\Workerman\Worker::monitorWorkersForLinux()
:// Is still running state then fork a new worker process. if (static::$_status !== static::STATUS_SHUTDOWN) { // 如果子进程在运行阶段退出时,就重新复刻(fork)一遍子进程 static::forkWorkers(); // If reloading continue. if (isset(static::$_pidsToRestart[$pid])) { unset(static::$_pidsToRestart[$pid]);// 这个子进程重启成功了,退出预备重启的队列 static::reload();// 继续重启其他子进程 } }
“让对应进程处理完毕当前请求后才退出”是如何做到的
先结合代码看\Workerman\Events\Select::loop
:
while (1) {
if(\DIRECTORY_SEPARATOR === '/') {
// Calls signal handlers for pending signals
\pcntl_signal_dispatch();
}
// 下面的代码省略
}
这里有个pcntl_signal_dispatch
函数,这是用于分发进程信号的,如果收到信号会马上执行\Workerman\Worker::signalHandler()
。值得注意的是pcntl_signal_dispatch
和信号回调是并行运行的,所以执行到stopAll()
时进程就退出了,下面的事件处理代码不再执行。
通俗来讲,主进程向子进程发送信号时,子进程的轮询器并没有立即收到信号,直到发送信号后的下一次while
循环体执行时才收到信号(先处理完当前的任务),收到信号后进程开始处理退出逻辑,这里就是所谓让对应进程处理完毕当前请求后才退出
。
平滑重启过程应该注意些什么
- 如果没有额外扩展比如
libevent
、\Swoole\Event
加持时,在定时器或者触发其他事件时,不应有长睡眠或者while(true)
的业务,否则队列中排在该子进程后面的进程将无法得到重启。 - 不要在子进程中保留客户端的长期数据,因为就算没有人为重启,进程运行过程中有意外退出时框架也会自动重启该进程,重启后就是一个新的进程,包括内存信息也是新的。
- 因为重启过程是针对子进程的,主进程不会进行重启,所以如果要更新主进程代码信息时需要把主进程也手动重启一遍。
一些疑问
case 'reload'
时执行了exit
,进程就退出了,为什么能继续重启?因为执行
php workerman.php reload -g
的时候,系统新开了一个进程,它负责向正在运行的workerman进程发送信号,这属于两个进程组,退出任何一个都相互不影响。一定要平滑重启吗?
并不是,平滑重启和普通重启的区别在于,普通重启会产生一个延时定时器,用于在进程退出超时(默认是2s)后将进程强制kill掉并重启,所以需要保证你的子进程业务中没有长时间堵塞IO的代码。对于上述“让对应进程处理完毕当前请求后才退出”的策略是贯穿于普通重启和平滑重启的,所以两种重启策略不需要关心是否影响当前请求。
哪些是属于主进程的代码?
举个例子:
<?php include "vendor/autoload.php"; use Workerman\Worker; // 只是示范作用,代码不保证合理性 // 这里就是主进程的代码,如果希望改动这个配置后使子进程也生效,需要重启主进程。 $database = [ 'host' => '127.0.0.1', 'password' => 'root', 'user' => 'root', 'database' => 'user', ]; $worker = new Worker("websocket://0.0.0.0:996"); $worker->onWorkerStart = function (Worker $that) use ($database) {// 这里是你的子进程代码 $that->dbcon = new \PDO('mysql:dbname=' . $database['database'] . ';host=' . $database['host'] . ';port=3306', $database['user'], $database['password']); }; $worker->onMessage = function($connection) { // 这里是你的子进程代码 }; $worker->onWorkerExit = function ($worker, $status, $pid) {// 这里也是子进程代码 }; Worker::runAll();
通过上述代码不难看出,其实几乎所有的回调函数都属于是子进程,而写在回调外面的代码都属于是主进程代码,主进程负责启动和监视子进程。