Worker 进程重启时的平滑迁移方案

解读

在国内高并发业务(电商大促、直播秒杀、金融支付)中,PHP 常以 CLI 模式常驻内存运行:Swoole、WorkerMan、RoadRunner 等组件把业务逻辑托管在 Worker 进程内。一旦需要热更新代码、修复内存泄漏或做滚动发布,就必须重启 Worker,但粗暴 kill 会导致:

  1. 正在处理的请求被强制断开,用户看到 502/504;
  2. 消息队列、RPC、WebSocket 连接直接丢失,产生脏数据或重复消费;
  3. 监控曲线抖动,触发告警,影响 SLA。

因此面试官想确认候选人是否“真的在一线扛过流量”,能否给出“代码 + 流程 + 监控”三位一体、可落地的平滑迁移方案,而不是简单说一句“用 reload 命令”。

知识点

  1. 进程生命周期:Master 管理 Worker,Worker 处理业务,Reload 信号只重启 Worker,不中断监听端口。
  2. 信号与 IPC:SIGUSR1/SIGUSR2、SIGTERM 与 Swoole 的 onWorkerExit/onWorkerStop 事件。
  3. 连接迁移:监听端口复用(SO_REUSEPORT)、内核层负载均衡、全连接队列(backlog)不丢失。
  4. 请求优雅收尾:设置 max_request + 定时器,让 Worker 在“当前请求结束”而非“任意时刻”退出。
  5. 资源清理:异步关闭数据库连接、释放 Redis 长连接、ack 消息队列、flush 日志。
  6. 无锁共享:利用 SysVMsg、Redis 队列或 Unix Socket 把“待关闭”Worker 中的任务转给“新”Worker。
  7. 发布系统:Kubernetes 滚动更新、蓝绿、金丝雀;国内常用 Nginx + Consul + 自研网关做流量灰度。
  8. 观测指标:QPS 跌落时长、连接数、upstream_response_time、进程重启次数、容器重启次数。

答案

以 Swoole 为例,给出一套可在 20 台 4C8G 机器、峰值 3w QPS 场景落地的“零中断”方案,其他 Runtime 思路同理。

  1. 代码层:让 Worker“可优雅死”

    • 设置 max_request=8000~10000,防止内存泄漏,也给出自然重启时机。
    • 在 onWorkerExit 回调里做“最后一圈”清理:
      $server->on('WorkerExit', function ($serv, $workerId) {
          // 1. 关闭连接池
          app('db')->disconnect();
          app('redis')->close();
          // 2. 等待 50ms 给协程收尾
          Co::sleep(0.05);
          // 3. 告诉队列消费者不再拉新消息
          app('kafkaConsumer')->pause();
      });
      
    • 业务逻辑里所有循环要监听 $server->isWorkerExit() 标志,收到后主动 break,防止死循环导致超时。
  2. 运维层:让 Master“只重启 Worker”

    • 发布脚本先把新代码 rsync 到临时目录,做 md5 校验,再 mv 原子替换。
    • 向 Master 发 kill -USR1 $(cat master.pid),Swoole 会: a. 启动一批新 Worker 接新连接; b. 向老 Worker 发 SIGTERM; c. 老 Worker 处理完当前请求后自动退出。
    • 设置 reload_async => truemax_wait_time => 30s,保证极端情况下 30 秒强制回收,防止僵死。
  3. 流量层:让网关“感知不到抖动”

    • Nginx upstream 配置:
      upstream backend {
          server 127.0.0.1:9501 max_fails=0 fail_timeout=0; # 关闭熔断
          keepalive 300;                                    # 长连接复用
      }
      
      max_fails=0 防止reload瞬间被摘掉。
    • 如果用了 K8s,给 Pod 加 readinessProbe,探测 9501 的 /health,老 Worker 退出前把 readiness 文件摘掉,但 Service 层面仍保持 30s 优雅时间。
  4. 观测层:让故障“看得见”

    • Prometheus 埋点:swoole_worker_exit_total、swoole_task_running gauge。
    • 发布窗口每 10s 拉一次指标,若 QPS 跌落超过 5% 或 P99 上涨超过 20%,立即 rollback。
  5. 灰度策略:让风险“可灰度”

    • 先重启 10% 的节点,观察 5 分钟曲线,再全量。公司内部发布平台(如蓝鲸、Spug)已集成该流程,候选人要强调“平台化 + 自动化”,而不是手动敲命令。

一句话总结:利用 Master-Worker 信号机制 + onWorkerExit 回调 + 网关零熔断配置,实现“请求自然结束、连接不丢、流量不掉”的平滑重启,全程对用户无感知。

拓展思考

  1. 如果业务里混用了传统的 php-fpm + Nginx,是否也能“平滑”?
    答:可以借助 php-fpm 的 process_control_timeoutpm.max_requests,但 fpm 模型下长连接(WebSocket、MQTT)必须上移一层到 Gateway,否则无法保持。对比可见常驻内存方案在连接保持场景更优。

  2. 当 Worker 里跑了耗时任务(如 30s 的报表导出),优雅时间如何设置?
    答:把耗时任务丢到 Task 进程或消息队列,Worker 只负责收请求和写队列,优雅时间可降到 3s 以内;否则需要把 max_wait_time 调到 60s,但会拉长发布窗口,需权衡。

  3. 双活机房场景下,如何做到“跨机房无损重启”?

    • 利用 DNS + 权重流量调度,先把重启机房权重降到 5%,内部再按上述流程 reload,之后权重恢复;
    • 或者采用 Service Mesh(Istio),在 sidecar 层做熔断与重试,PHP 业务无需改造。
  4. 如果未来迁移到 RoadRunner 或 FrankenPHP,它们同样支持“worker-num + max-requests + graceful timeout”,核心思路不变,但信号换成 HTTP RPC 指令,候选人应表现出“语言级无关”的迁移能力。