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循环体执行时才收到信号(先处理完当前的任务),收到信号后进程开始处理退出逻辑,这里就是所谓让对应进程处理完毕当前请求后才退出

平滑重启过程应该注意些什么

  1. 如果没有额外扩展比如libevent\Swoole\Event加持时,在定时器或者触发其他事件时,不应有长睡眠或者while(true)的业务,否则队列中排在该子进程后面的进程将无法得到重启。
  2. 不要在子进程中保留客户端的长期数据,因为就算没有人为重启,进程运行过程中有意外退出时框架也会自动重启该进程,重启后就是一个新的进程,包括内存信息也是新的。
  3. 因为重启过程是针对子进程的,主进程不会进行重启,所以如果要更新主进程代码信息时需要把主进程也手动重启一遍。

一些疑问

  1. case 'reload'时执行了exit,进程就退出了,为什么能继续重启?

    因为执行php workerman.php reload -g的时候,系统新开了一个进程,它负责向正在运行的workerman进程发送信号,这属于两个进程组,退出任何一个都相互不影响。

  2. 一定要平滑重启吗?

    并不是,平滑重启和普通重启的区别在于,普通重启会产生一个延时定时器,用于在进程退出超时(默认是2s)后将进程强制kill掉并重启,所以需要保证你的子进程业务中没有长时间堵塞IO的代码。对于上述“让对应进程处理完毕当前请求后才退出”的策略是贯穿于普通重启和平滑重启的,所以两种重启策略不需要关心是否影响当前请求。

  3. 哪些是属于主进程的代码?

    举个例子:

    <?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();

    通过上述代码不难看出,其实几乎所有的回调函数都属于是子进程,而写在回调外面的代码都属于是主进程代码,主进程负责启动和监视子进程。


workerman平滑重启原理
http://blog.icy8.cn/posts/6713/
作者
icy8
发布于
2022年11月5日
许可协议