PHP 怎么平滑重启

wen PHP项目 1

PHP 平滑重启全攻略:从原理到实战,告别服务中断的焦虑

📚 目录导读

  1. 为什么PHP需要“平滑重启”?—— 痛点与场景剖析
  2. 核心概念:什么是平滑重启?它与普通重启有何不同?
  3. 主流方案实战:PHP-FPM、Swoole、Workerman 平滑重启详解
  4. 进阶技巧:代码层面的热更新与状态保持
  5. 高频问答(Q&A):解决你最后的一丝疑虑
  6. 总结与最佳实践建议

1️⃣ 为什么PHP需要“平滑重启”?—— 痛点与场景剖析

在日常运维中,我们经常遇到这样的场景:上线新代码、修改配置、部署补丁,如果直接执行 systemctl restart php-fpm,所有正在处理的请求会被立即终止,造成:

PHP 怎么平滑重启

  • 用户请求丢失:用户正在提交的表单、下载的文件、支付流程突然中断。
  • 会话数据损坏:基于文件的Session未及时写入,导致用户被迫重新登录。
  • 队列任务中断:基于PHP的异步队列(如Laravel Queue)正在处理的任务丢失,需要人工补偿。
  • 微服务雪崩:在高并发下,瞬间断开所有worker连接,上游负载均衡器会将错误请求转发,引发连锁故障。

核心痛点:PHP作为无共享架构(Share-Nothing)的典型代表,其进程生命周期短(请求结束即销毁),但像PHP-FPM这种常驻进程管理器,一旦停止,所有子进程全部退出,无法像Nginx那样通过“旧进程处理完旧请求,新进程接管新请求”的方式平滑过渡。

平滑重启的目标:在不中断现有服务的前提下,优雅地终止旧进程,启动新进程,这不仅是运维技巧,更是保障业务连续性的必修课。


2️⃣ 核心概念:什么是平滑重启?它与普通重启有何不同?

维度 普通重启 (Hard Restart) 平滑重启 (Graceful Restart)
进程处理 主进程发送SIGTERM,强制杀死所有worker 主进程发送SIGUSR2(或特定信号),worker处理完当前请求后自杀
请求影响 正在处理的请求立即返回500或连接重置 现有请求完整处理完毕,新请求由新worker接管
状态保持 Session/缓存丢失 外部存储(Redis/DB)不受影响,内存态(如Swoole的Table)需特殊处理
操作方式 kill -9 或 restart命令 发送特定信号或使用管理命令

关键点:PHP本身运行在容器(如FPM)中时,脚本是无状态的,真正需要平滑的是进程管理器(FPM master)和常驻内存框架(Swoole/Workerman)的reload机制。


3️⃣ 主流方案实战:PHP-FPM、Swoole、Workerman 平滑重启详解

1 PHP-FPM 平滑重启(最常用)

PHP-FPM 内置了优雅重启机制,它通过信号控制:

  • SIGUSR2:平滑重启所有worker,主进程会重新加载php.iniphp-fpm.conf,并逐步淘汰旧worker。
# 方式一:向master进程发送信号(推荐)
kill -USR2 $(cat /var/run/php-fpm.pid)
# 方式二:使用systemd(注意:systemd需要配置KillMode=mixed)
systemctl reload php-fpm
# 方式三:若在Docker容器内,向PID 1发送信号
docker kill --signal=USR2 <container>

验证重启是否成功

ps -ef | grep php-fpm  # 观察master的启动时间(START列)

你会发现旧worker的PID变为新PID,而master进程的PID保持不变。

重要配置php-fpm.conf):

; 设置超时时间,允许worker处理完慢请求
request_terminate_timeout = 60s
; 平滑重启时,最多允许旧worker存活多久(0为无限等待)
process_control_timeout = 10s

缺陷:如果你修改了PHP代码,FPM重启后加载的是最新代码;但如果你使用了OpCache,需要额外注意——因为OpCache会缓存编译后的字节码,如果不清理,即使FPM重启,代码依然是旧的,解决方案:

# 单独重启opcache(需安装扩展)
php -r "opcache_reset();"

或者在代码部署后,通过touchinotify触发opcache_validate_timestamps自动检测文件变更。

2 Swoole 平滑重启(常驻内存框架)

Swoole提供了专业的reload命令,但需区分两种模式:

  • reload:仅重启worker进程,用于加载PHP代码更新(不涉及监听端口的manager进程变化)。
  • reload_task:仅重启task_worker进程(异步任务)。
// 在控制器或命令行中触发
$swoole_server->reload(); // 平滑重启worker
// 或者在shell中使用
kill -USR1 $(pgrep -f 'swoole_server.php')  // 发送SIGUSR1

Swoole的优雅之处

  • 旧worker处理完当前请求后,自动退出。
  • 新worker会重新加载onWorkerStart事件,拉起新代码。
  • 若配置了reload_async = true,会等待所有异步连接(如MySQL连接池中的连接)关闭后才退出worker。

注意

  • Swoole如果使用了Table(内存表)或全局变量,在reload后数据会丢失,如需保留,需结合RedisIPC共享内存。
3 Workerman 平滑重启

Workerman的reload命令非常简洁:

# 在项目根目录执行
php bin/workermand reload

它内部实现原理:

  • 主进程读取pid文件,向所有worker进程发送SIGUSR1
  • 旧worker完成手头请求后,调用Worker::stopAll()
  • 新worker创建,加载新代码。

与Swoole不同:Workerman原生不支持reload_async,如果你的连接是长连接(如WebSocket),旧连接会被强制断开,解决办法是结合GatewayWorker(基于Workerman)提供的reload机制,它会等待所有Gateway连接完成握手后再回收。


4️⃣ 进阶技巧:代码层面的热更新与状态保持

即使有了平滑重启,频繁重启仍会带来性能损耗,我们可以通过子进程平滑过渡实现“零重启”热更新:

  • 方案A:双进程守护
    使用Supervisor守护一个父进程(go或PHP CLI),父进程监控代码变化,动态创建子进程处理请求,每次代码更新,父进程先创建新子进程,待新子进程就绪后,再杀掉旧子进程(类似Nginx的优雅升级)。

  • 方案B:流量切换
    在Gitlab CI/CD中,先启动新版本服务到另一个端口(如9090),测试健康检查通过后,用负载均衡器(如Nginx upstream)将流量从8080切换到9090,完成后,再优雅关闭旧服务,这虽非PHP本身的重启,但实现了业务层的平滑。

  • 方案C:利用OpCache的validate_timestamps
    在开发/测试环境,设置opcache.validate_timestamps=1opcache.revalidate_freq=0,这样每次文件修改后,FPM自动重载新代码,无需任何重启,但生产环境出于性能考虑,通常会设为0,因此生产环境必须配合FPM的reload

状态保持最佳实践

  • Session:一定要存入Redis/数据库,避免基于文件。
  • 缓存:使用Redis/Memcached,重启进程不影响数据。
  • 数据库连接池:Swoole/Workerman需使用连接池库,并在onWorkerStop时优雅释放连接(避免破池)。

5️⃣ 高频问答(Q&A):解决你最后的一丝疑虑

Q1:我执行 systemctl restart php-fpmreload 有什么区别?
A:restart是“硬重启”,先停止全部进程再启动(会丢请求)。reload(reload命令内部会发送SIGUSR2)是平滑重启,master进程不退出,只替换worker,生产环境务必用reload

Q2:平滑重启后,我的用户Session会丢失吗?
A:不会,因为Session默认存储在服务器磁盘或Redis,只要不是存储在进程内存中(比如$_SESSION默认是文件存储),重启进程不影响已写入的Session文件,但若你用了Swoole的Table存储Session ID映射,则可能丢失,需改用Redis存储。

Q3:如果某些请求非常慢(比如下载大文件),平滑重启要等多久?
A:PHP-FPM默认会等worker空闲后自杀,但你可以设置process_control_timeout(例如10秒),如果旧worker超过该时间还没处理完,master会强制杀掉它,这种极端情况下会丢失该慢请求,合理调大这个参数,或配合Nginx的proxy_read_timeout保证两端时间匹配。

Q4:我正在用Swoole开发WebSocket服务,如何重启而不掉线?
A:Swoole原生的reload会断开所有连接(因为WebSocket连接属于worker进程的上下文),要实现不掉线,需要结合Swoole的reload_async(要求Swoole >= 4.5),并配合Redis pub/subChannel将连接信息转移到新worker,具体做法:在onWorkerStart中订阅Redis频道,将旧worker的连接描述符通过swap迁移。

Q5:代码部署后,为什么OpCache会缓存旧代码,即使重启了FPM?
A:OpCache是独立于PHP进程的缓存,重启FPM只是清空了进程内存,但OpCache的共享内存段可能还在(如果opcache.file_cache设为外部文件),解决办法:在部署脚本末尾执行php -r "opcache_reset();",或使用opcache.invalidate函数,推荐在CI/CD中调用。

Q6:在Kubernetes中部署PHP服务,如何平滑?
A:K8s的滚动更新(Rolling Update)天然实现了平滑,它会先启动新Pod,待健康检查通过后,才摘除旧Pod,但你的PHP-FPM镜像启动命令应设置trap 'kill -USR2 $PID' SIGTERM,让Pod终止时先触发FPM平滑重启而非直接kill。


6️⃣ 总结与最佳实践建议

核心结论

  • PHP-FPM平滑重启最可靠,用 kill -USR2 <master_pid>
  • Swoole/Workerman用reload命令,但需注意内存状态和长连接问题。
  • 业务层切换(双端口+负载均衡)是最彻底的热更新方案。

实操建议清单

  1. 生产环境部署脚本统一使用reload,禁止restart
  2. 配置process_control_timeout为合理值(如10秒)。
  3. 部署后必须执行opcache_reset()
  4. Session/缓存全面迁移至外部存储(Redis)。
  5. 对Swoole项目,开启reload_async并设计好连接迁移逻辑。
  6. 监控平滑重启后的错误日志(监听 SIGUSR2 关键字),确保无异常。

踩坑提醒:切勿忽视php-fpm.pid文件不存在或权限不足的情况,使用systemd时,需要保证服务文件中KillMode=mixed,否则systemd在reload时可能会强制杀掉worker。


希望本文能让你彻底掌握PHP平滑重启的精髓,平滑重启不只是一个命令,而是一套保障服务稳定性的完整思维框架。

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