PHP项目Swoole服务如何平滑重启

wen PHP项目 20

本文目录导读:

PHP项目Swoole服务如何平滑重启

  1. 核心原理
  2. 方法一:发送信号(最常用、最标准)
  3. 方法二:使用 reload() 方法(编程式,推荐)
  4. 方法三:使用 Swoole 官方管理工具(如 Swoole 管理面板或 Supervisord 配合)
  5. 关键配置参数(提高平滑重启的可靠性)
  6. 常见问题与坑
  7. 总结最佳实践

在 PHP Swoole 项目中,平滑重启(也称为“无缝重启”或“热重启”)是指在不中断当前正在处理的请求的情况下,优雅地停止旧的 Worker 进程,并启动新的 Worker 进程来加载更新后的代码。

Swoole 主要通过两种方式实现平滑重启:信号机制Reload 机制

核心原理

  • Manager 进程:负责管理 Worker 进程。
  • Worker 进程:处理具体的业务逻辑。
  • 重启流程:Master 进程收到重启信号 -> 通知 Manager -> Manager 向旧的 Worker 发送 SIGTERM 信号,等待其处理完当前请求后退出 -> 同时创建新的 Worker 进程加载新代码。

发送信号(最常用、最标准)

这是 Swoole 官方推荐的方式,通过向 Master 进程发送特定的信号来实现。

  1. 找到 Master 进程 PID 通常在 Server::start() 时会写入 PID 文件,或者通过 ps aux | grep [your_server_name] 查找。

  2. 发送信号

    • SIGUSR1平滑重启所有 Worker 进程,这是最常用的操作。
    • SIGUSR2平滑重启所有 Task Worker 进程(如果启用了 Task 功能)。
    • SIGTERM强制终止,不等待请求处理完毕(不推荐用于平滑重启)。

    命令行示例:

    # 假设你的服务主进程 PID 是 1234
    # 平滑重启所有 Worker 进程
    kill -USR1 1234
    # 或者从 PID 文件读取
    kill -USR1 $(cat /var/run/your_app.pid)

优点:简单、直接、无依赖。 缺点:需要记住信号指令,且不会触发自定义的回调逻辑。

使用 reload() 方法(编程式,推荐)

在 Swoole 4.5+ 版本中,推荐使用 Server 对象的 reload() 方法,这比直接发信号更可控,因为可以结合安全重启周期

代码示例:

<?php
use Swoole\Server;
$server = new Swoole\Server('0.0.0.0', 9501);
$server->set([
    'worker_num' => 4,
    // 设置 reload_async 为 true 开启异步安全重启
    'reload_async' => true,
    // 最大等待时间(秒),防止 Worker 进程卡死
    'max_wait_time' => 30,
]);
$server->on('WorkerStart', function ($server, $workerId) {
    // 每个 Worker 启动时加载代码
    require __DIR__ . '/app_code.php';
});
$server->on('Receive', function ($server, $fd, $reactorId, $data) {
    // 处理业务逻辑...
    $server->send($fd, 'Hello');
});
$server->start();
// --- 在某个控制台命令或管理页面中 ---
// 注意:你不能在 on('Receive') 回调中调用 reload(),因为它属于 Worker 进程。
// reload() 应该由 Master 进程或用户自定义的进程来调用。
// 假设你通过 HTTP API 触发重启
$httpServer = new Swoole\Http\Server("0.0.0.0", 9502);
$httpServer->on('request', function ($request, $response) use ($server) {
    // 通过向主 Server 发送自定义协议消息触发重启
    // 更推荐的方式是让一个常驻的 Manager 脚本来管理
    if ($request->server['request_uri'] == '/reload') {
        $result = $server->reload(); // 返回 true 或 false
        if ($result) {
            $response->end("reload success");
        } else {
            $response->end("reload failed");
        }
    }
});
$httpServer->start();

关键点reload() 方法必须在 Master 进程 的上下文中调用,通常的做法是:

  1. 启动一个独立的 Swoole 管理服务(如 HTTP 服务)。
  2. 在管理服务中接收指令,调用主业务 Server 的 reload()

使用 Swoole 官方管理工具(如 Swoole 管理面板或 Supervisord 配合)

虽然信号是底层机制,但在生产环境中,通常配合以下工具:

  1. Supervisord + 信号

    • 在 Supervisor 配置文件的 [program:your_app] 中添加 stopasgroup=truekillasgroup=true
    • 执行 supervisorctl signal USR1 your_app
    • 注意:Supervisor 默认的 stoprestart 命令是发送 SIGTERM,这是强杀,不是平滑,必须明确使用 signal USR1
  2. 自定义管理脚本(推荐): 编写一个 bash 脚本,让你可以像操作 Nginx 一样操作 Swoole。

    #!/bin/bash
    # swoole_manager.sh
    PID_FILE="/var/run/swoole_app.pid"
    SERVER_SCRIPT="/path/to/your_server.php"
    case "$1" in
        start)
            php $SERVER_SCRIPT
            ;;
        stop)
            kill -TERM $(cat $PID_FILE)
            ;;
        reload)
            echo "Sending reload signal to Swoole Master PID: $(cat $PID_FILE)"
            kill -USR1 $(cat $PID_FILE)
            echo "Reload signal sent."
            ;;
        *)
            echo "Usage: $0 {start|stop|reload}"
            exit 1
    esac

关键配置参数(提高平滑重启的可靠性)

为了确保平滑重启真正平滑,需要在 Server::set() 中设置以下参数:

配置项 默认值 说明
reload_async false 强烈建议设为 true,启用异步安全重启,开启后,Manager 会等待 Worker 进程的所有异步连接关闭、事件循环清空后才退出,有效防止 TCP 连接断开、Redis/MySQL 连接未释放等问题。
max_wait_time 3 reload_async = true 时,设置 Worker 进程退出前的最大等待时间(秒),Worker 在等待时间内没有退出(例如有长时间阻塞的任务),Swoole 会强制杀死它。
max_request 0 设置每个 Worker 进程处理完 max_request 次请求后自动重启,这可以很好地解决 PHP 脚本的内存泄漏问题,平滑重启会自动触发。

推荐配置:

$server->set([
    'worker_num' => 4,
    'reload_async' => true,
    'max_wait_time' => 60,
    'max_request' => 10000, // 处理1万次请求后自动重启
]);

常见问题与坑

  1. 代码未更新?

    • 原因onWorkerStart 中的 requireinclude 被 OPcache 缓存。
    • 解决:确保在 onWorkerStart 中加载核心业务代码,而不是在全局一次性加载,或者配合 opcache.revalidate_freq = 0opcache.validate_timestamps = 1 使用。最推荐的做法是:在 onWorkerStart 中动态 require 框架的入口文件。
  2. 长时间阻塞连接(如 WebSocket 或长连接 TCP)?

    • 问题reload_async = true 虽然会等待,但如果业务逻辑不检查 Server::shutdown() 状态,可能会导致无法退出。
    • 解决:在业务逻辑中定期调用 $server->shutdown() 来判断服务是否在重启中,或者在 onWorkerStop 中手动清理连接。
  3. Task Worker 的平滑重启?

    • 使用 kill -USR2 <pid> 信号,或者调用 $server->reload() 方法(它默认只重启 Worker,会忽略 Task Worker),如需重启 Task Worker,需调用 $server->taskworker_reload()

总结最佳实践

  1. 设置 reload_async = true
  2. 设置合理的 max_wait_timemax_request
  3. onWorkerStart 中加载业务代码(而不是在全局)。
  4. 使用自定义管理脚本或 Supervisor 的 signal 命令
  5. 重启后检查日志:查看是否有 WARNING: WorkerX timedWARNING: WorkerX exit timeout 的警告,如果有,说明 max_wait_time 设置过短或业务逻辑有长时间阻塞。

执行命令:

# 实际生产中最常用的就是这一行命令
kill -USR1 $(cat /var/run/swoole.pid)

抱歉,评论功能暂时关闭!