Karp 的技术博客

1. 背景说明

在生产环境中,通过 Supervisor 管理 Swoole 服务是常见做法。然而,在执行 supervisorctl restartstop → start 时,开发者常遇到以下问题:

  • 子进程(Swoole worker)无法被正常杀死
  • 重启后依旧存在残留 worker
  • 强制 kill -9 也无效(可能处于 IO 阻塞)
  • 重启过程缓慢导致服务无法即时恢复

本文从 Swoole 进程模型、信号处理机制、Supervisor 行为、业务阻塞问题 等角度进行技术分析,并提供最佳实践配置与建议。


2. Swoole 进程模型概述

Swoole Server(如 HTTP / WebSocket / TCP)的基本进程结构:

Master Process
│
├── Manager Process
│
└── Worker Processes (N 个)

关键特点:

  • Worker 是独立子进程,由 Swoole 的 Master 和 Manager 管理
  • Worker 在处理请求时必须“优雅退出”,不能强行中断
  • Worker 使用事件循环 + 协程执行任务
  • Worker 退出前会尝试等待:

    • 当前请求处理完成
    • 当前协程执行完
    • 当前 IO 任务结束

因此,如果 Worker 正在执行耗时任务或出现 IO 阻塞,就会:

✔ 不立即退出
✔ 收到信号后延迟退出
✔ 占用进程表造成“杀不死”现象


3. Supervisor 的进程管理方式

Supervisor 在停止进程时的默认流程:

  1. 发送 SIGTERM
  2. 等待 stopwaitsecs 指定时间
  3. 若进程未退出 → 发送 SIGKILL (kill -9)
  4. 若仍未退出 → 进程处于不可杀状态(D 状态),无法强制终止

Supervisor 与 Swoole 的行为差异导致了“不易杀死”的问题:

项目Swoole Worker 行为Supervisor 行为
SIGTERM优雅退出,等待任务尝试快速终止
任务未完成等待完成超时后强制 kill
IO 阻塞无法退出kill 无效

4. 子进程“杀不死”的技术原因分析

4.1 Worker 正在处理未完成的业务

包括:

  • 长时间业务逻辑
  • 无限循环未 yield
  • 大量同步阻塞 IO(MySQL、Redis、文件)
  • sleep、usleep 等阻塞函数
  • 繁忙协程无法调度

Swoole 在任务未完成前无法退出,因此 ignore SIGTERM。


4.2 Worker 处于不可杀状态(D 状态)

通过 ps aux 可看到 D

STAT = D    (Uninterruptible sleep)

此状态下:

  • kill -9 无效
  • Supervisor 无法结束进程
  • 通常是 IO 阻塞导致(NFS、磁盘坏块、Redis / MySQL hang 等)

4.3 Swoole 未关闭 daemonize

如果配置:

'daemonize' => 1

则:

  • Swoole 会从 Supervisor 分离
  • Worker 不在同一进程组
  • Supervisor 无法管理子进程

导致大量残留 zombie 或 orphan。


5. 最佳实践配置(Supervisor + Swoole)

5.1 Supervisor 配置示例

[program:app]
command=php /Service/script/bin/server.php start --conf-SService
autostart=true
autorestart=true
stdout_logfile=/service/SService/app.out

stopasgroup=true
killasgroup=true
stopsignal=TERM
stopwaitsecs=10

说明:

  • stopasgroup=true:停止时给整个进程组发信号
  • killasgroup=true:确保 worker 一并被 kill
  • stopsignal=TERM:适用于 Swoole 优雅退出
  • stopwaitsecs=10:给 Swoole 10 秒处理未完成任务

5.2 Swoole 配置示例(关键)

$server->set([
    'daemonize' => 0,
    'max_wait_time' => 3,   // 优雅退出最多允许 3 秒等待
    'log_file' => '/opt/log/swoole.log',
]);

max_wait_time 是最关键参数

收到 SIGTERM 后,Worker 至多等待 X 秒,否则强制退出。

目的:

  • 避免 Worker 长期因业务阻塞而无法退出
  • 避免 Supervisor restart 卡住
  • 确保服务可平滑更新

6. 如何确认 Worker 是否阻塞

方法一:查看 worker 状态

ps -o pid,ppid,stat,cmd -p <pid>
  • D → IO 阻塞
  • S → 正在等待退出
  • R → 忙(正在执行)

方法二:strace 调用观察

strace -p <pid>

如果看到:

  • read(...)
  • accept(...)
  • recvfrom(...)
  • 卡在 Redis, MySQL 等 IO 调用

说明 worker 正被阻塞。


7. 总结

如果你遇到:

  • Supervisor restart 时 Swoole worker “杀不死”
  • worker 残留、僵尸进程、卡住无法退出
  • kill -9 无效

那么原因基本是:

  1. worker 正在处理未完成的业务
  2. worker 有同步阻塞 IO
  3. daemonize = 1 导致进程脱离 Supervisor
  4. 没有设置 max_wait_time 导致优雅退出卡死

解决方案:

  • 关闭 daemonize
  • 设置 max_wait_time
  • 使用 killasgroup / stopasgroup
  • 避免阻塞业务

8. 附:推荐的完整 Swoole + Supervisor 环境模板

server.php(核心配置)

$server->set([
    'worker_num' => 4,
    'daemonize' => 0,
    'max_wait_time' => 3,
    'log_file' => '/opt/log/swoole.log',
]);

Supervisor 配置

[program:app]
command=php /Service/script/bin/server.php start
autostart=true
autorestart=true

stopasgroup=true
killasgroup=true
stopsignal=TERM
stopwaitsecs=10
stdout_logfile=/opt/log/app.out

版权属于:karp
作品采用:本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
更新于: 2025年12月24日 12:16
0

目录

来自 《【踩坑】 Supervisor 管理 Swoole 服务的优雅重启机制分析》