Worker 进程重启时的平滑迁移方案
解读
在国内高并发业务(电商大促、直播秒杀、金融支付)中,PHP 常以 CLI 模式常驻内存运行:Swoole、WorkerMan、RoadRunner 等组件把业务逻辑托管在 Worker 进程内。一旦需要热更新代码、修复内存泄漏或做滚动发布,就必须重启 Worker,但粗暴 kill 会导致:
- 正在处理的请求被强制断开,用户看到 502/504;
- 消息队列、RPC、WebSocket 连接直接丢失,产生脏数据或重复消费;
- 监控曲线抖动,触发告警,影响 SLA。
因此面试官想确认候选人是否“真的在一线扛过流量”,能否给出“代码 + 流程 + 监控”三位一体、可落地的平滑迁移方案,而不是简单说一句“用 reload 命令”。
知识点
- 进程生命周期:Master 管理 Worker,Worker 处理业务,Reload 信号只重启 Worker,不中断监听端口。
- 信号与 IPC:SIGUSR1/SIGUSR2、SIGTERM 与 Swoole 的 onWorkerExit/onWorkerStop 事件。
- 连接迁移:监听端口复用(SO_REUSEPORT)、内核层负载均衡、全连接队列(backlog)不丢失。
- 请求优雅收尾:设置 max_request + 定时器,让 Worker 在“当前请求结束”而非“任意时刻”退出。
- 资源清理:异步关闭数据库连接、释放 Redis 长连接、ack 消息队列、flush 日志。
- 无锁共享:利用 SysVMsg、Redis 队列或 Unix Socket 把“待关闭”Worker 中的任务转给“新”Worker。
- 发布系统:Kubernetes 滚动更新、蓝绿、金丝雀;国内常用 Nginx + Consul + 自研网关做流量灰度。
- 观测指标:QPS 跌落时长、连接数、upstream_response_time、进程重启次数、容器重启次数。
答案
以 Swoole 为例,给出一套可在 20 台 4C8G 机器、峰值 3w QPS 场景落地的“零中断”方案,其他 Runtime 思路同理。
-
代码层:让 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,防止死循环导致超时。
-
运维层:让 Master“只重启 Worker”
- 发布脚本先把新代码 rsync 到临时目录,做 md5 校验,再 mv 原子替换。
- 向 Master 发
kill -USR1 $(cat master.pid),Swoole 会: a. 启动一批新 Worker 接新连接; b. 向老 Worker 发 SIGTERM; c. 老 Worker 处理完当前请求后自动退出。 - 设置
reload_async => true和max_wait_time => 30s,保证极端情况下 30 秒强制回收,防止僵死。
-
流量层:让网关“感知不到抖动”
- Nginx upstream 配置:
max_fails=0 防止reload瞬间被摘掉。upstream backend { server 127.0.0.1:9501 max_fails=0 fail_timeout=0; # 关闭熔断 keepalive 300; # 长连接复用 } - 如果用了 K8s,给 Pod 加 readinessProbe,探测 9501 的 /health,老 Worker 退出前把 readiness 文件摘掉,但 Service 层面仍保持 30s 优雅时间。
- Nginx upstream 配置:
-
观测层:让故障“看得见”
- Prometheus 埋点:swoole_worker_exit_total、swoole_task_running gauge。
- 发布窗口每 10s 拉一次指标,若 QPS 跌落超过 5% 或 P99 上涨超过 20%,立即 rollback。
-
灰度策略:让风险“可灰度”
- 先重启 10% 的节点,观察 5 分钟曲线,再全量。公司内部发布平台(如蓝鲸、Spug)已集成该流程,候选人要强调“平台化 + 自动化”,而不是手动敲命令。
一句话总结:利用 Master-Worker 信号机制 + onWorkerExit 回调 + 网关零熔断配置,实现“请求自然结束、连接不丢、流量不掉”的平滑重启,全程对用户无感知。
拓展思考
-
如果业务里混用了传统的 php-fpm + Nginx,是否也能“平滑”?
答:可以借助 php-fpm 的process_control_timeout和pm.max_requests,但 fpm 模型下长连接(WebSocket、MQTT)必须上移一层到 Gateway,否则无法保持。对比可见常驻内存方案在连接保持场景更优。 -
当 Worker 里跑了耗时任务(如 30s 的报表导出),优雅时间如何设置?
答:把耗时任务丢到 Task 进程或消息队列,Worker 只负责收请求和写队列,优雅时间可降到 3s 以内;否则需要把max_wait_time调到 60s,但会拉长发布窗口,需权衡。 -
双活机房场景下,如何做到“跨机房无损重启”?
- 利用 DNS + 权重流量调度,先把重启机房权重降到 5%,内部再按上述流程 reload,之后权重恢复;
- 或者采用 Service Mesh(Istio),在 sidecar 层做熔断与重试,PHP 业务无需改造。
-
如果未来迁移到 RoadRunner 或 FrankenPHP,它们同样支持“worker-num + max-requests + graceful timeout”,核心思路不变,但信号换成 HTTP RPC 指令,候选人应表现出“语言级无关”的迁移能力。