本文目录导读:

- 方案一:软链接切换(最常用、最推荐)
- 方案二:平滑重启PHP-FPM + OpCache清理
- 方案三:蓝绿部署(Blue-Green Deployment)
- 方案四:金丝雀部署(Canary Deployment)
- 关键注意事项(常见坑)
- 总结建议
实现PHP代码零停机部署的核心目标是:在更新代码期间,保证用户始终访问到的是旧版或新版代码的完整逻辑,不会出现500错误、半加载或混合版本问题。
由于PHP是解释型语言,并且现代PHP框架通常有编译缓存(如OpCache),简单覆盖文件会导致共享内存中的缓存与磁盘文件不一致,从而引发错误。
以下是几种主流的实现方案,从简单到复杂排列:
软链接切换(最常用、最推荐)
这是生产环境中最标准、最可靠的零停机部署方法,适用于Nginx + PHP-FPM架构。
核心原理:
保持一个不变的公共入口(如/var/www/html/current),它是一个指向实际代码目录的软链接,更新代码时,在另一个目录部署新版本,然后原子性地切换软链接指向。
操作步骤:
-
初始目录结构:
/var/www/ ├── releases/ # 存放所有版本 │ ├── v1.0.0/ │ └── v1.0.1/ ├── current -> /var/www/releases/v1.0.0 # 软链接指向当前版本 └── shared/ # 共享资源(上传文件、日志、Session等) -
Web服务器配置(Nginx): 将
document_root直接指向current。server { listen 80; root /var/www/current/public; # 指向软链接 index index.php; # ... 其他配置 } -
部署新版本(v1.0.1):
# 1. 创建新版本目录 mkdir -p /var/www/releases/v1.0.1 # 2. 拉取代码(或解压) git clone -b v1.0.1 /path/to/repo /var/www/releases/v1.0.1 # 3. 创建共享资源的软链接(日志、上传文件等) ln -sfn /var/www/shared/.env /var/www/releases/v1.0.1/.env ln -sfn /var/www/shared/storage /var/www/releases/v1.0.1/storage # 4. 执行版本特定的任务(composer install, 数据库迁移) cd /var/www/releases/v1.0.1 composer install --no-dev # 5. **关键步骤**:瞬间切换软链接 ln -sfn /var/www/releases/v1.0.1 /var/www/current
-
清理旧版本(可选):
rm -rf /var/www/releases/v1.0.0
为什么能实现零停机?
ln -sfn命令是原子操作(在Linux文件系统层面)。- 正在处理请求的旧进程继续使用旧目录(文件描述符已打开)。
- 新请求进来时,Nginx解析新的软链接指向新目录。
- 旧版本进程结束后,旧目录安全删除。
平滑重启PHP-FPM + OpCache清理
适用场景: 架构简单,不想引入复杂部署流程,代码更新频繁但改动很小。
核心原理:
PHP-FPM支持reload(USR2信号),会优雅地重启worker进程:旧进程处理完当前请求后退出,新进程加载新代码并启动,配合清空OpCache,确保新进程不加载旧缓存。
操作步骤:
-
部署新代码: 通过rsync、git pull等方式直接覆盖服务器的原有代码目录。
-
清空OpCache: 如果开启了OpCache,必须清空。
- 通过PHP函数(推荐): 在部署脚本最后访问一个内置的端点(需在框架中集成)。
// deploy.php 或 healthcheck.php if (function_exists('opcache_reset')) { opcache_reset(); } - 通过Web请求触发: 部署脚本中执行
curl http://localhost/clear-cache.php
- 通过PHP函数(推荐): 在部署脚本最后访问一个内置的端点(需在框架中集成)。
-
平滑重启PHP-FPM:
sudo kill -USR2 $(cat /var/run/php-fpm/php-fpm.pid) # 或使用 systemctl sudo systemctl reload php8.1-fpm
reload会等待旧进程处理完请求后再完全退出,不会中断正在执行的请求。
需要注意的问题:
- 在覆盖代码到重启的瞬间间隙,如果旧进程正在处理一个请求,而该请求包含了一个刚被覆盖的类文件,可能触发
Class not found错误,此方案无法保证100%零停机,除非代码改动量极小(如只改静态资源)。 - 如果新旧版本之间有数据库表结构变更(如新增字段),旧进程可能无法正常工作。
蓝绿部署(Blue-Green Deployment)
核心原理: 准备两套完全独立的环境(两套服务器或容器),同一时刻只有一套在生产运行,更新时,将流量切到另一套环境,然后更新旧环境。
架构示例:
- 蓝环境(当前):
0.0.1:9000运行 v1.0.0 - 绿环境(待切换):
0.0.2:9000运行 v1.0.1 - 负载均衡器: Nginx / HAProxy / 云负载均衡(如AWS ALB)
操作步骤:
- 在绿环境中部署v1.0.1,并进行完整的健康检查和集成测试。
- 修改负载均衡器配置,将流量从蓝环境切换至绿环境。切换是瞬间的。
- 观察绿环境运行状态。
- 确认无误后,更新蓝环境为最新代码,作为下一次部署的备用环境。
优点:
- 零停机,切换瞬间完成。
- 回滚极其简单(把流量切回蓝环境即可)。
- 可以并行部署并测试新版本。
缺点:
- 成本翻倍(需要两倍服务器资源)。
- 如果数据库结构变更,需确保新旧版本都能兼容(通常需要向前兼容的DB迁移)。
金丝雀部署(Canary Deployment)
适合场景: 高风险更新,逐步放量,监控错误率。
核心原理: 从负载均衡器中分流一小部分流量(例如5%)到新版本服务器,如果新版本稳定,逐步增加流量比例,直至100%。
实现方式:
- 部署新版本到一组新服务器(金丝雀组)。
- 在Nginx/API网关中配置多个
upstream,并对金丝雀组设置较低的权重。upstream php_backend { server 10.0.0.1:9000 weight=95; # 旧版本95% server 10.0.0.2:9000 weight=5; # 新版本5% } - 监控新版本的错误率、响应时间。
- 如果一切正常,将权重调整为100%。
- 如果出现问题,立刻将新版本组的权重设为0(回滚)。
关键注意事项(常见坑)
-
OpCache验证时间问题:
- 即使使用了软链接切换,新代码第一次被请求时,OpCache尚未缓存,PHP仍会读取磁盘文件,如果文件路径未变(如
/var/www/current),OpCache可能已经缓存了旧文件内容。 - 解决: 在部署脚本最后,调用
opcache_reset()或在PHP配置中设置opcache.validate_timestamps=1并配合opcache.revalidate_freq=0(代价是性能降低),或使用opcache.file_cache外置缓存。
- 即使使用了软链接切换,新代码第一次被请求时,OpCache尚未缓存,PHP仍会读取磁盘文件,如果文件路径未变(如
-
数据库迁移:
- 零停机部署最大的敌人是不兼容的数据库变更,新增一个
NOT NULL列但没有默认值,旧代码插入数据时会失败。 - 最佳实践: 数据库变更需要分步进行(向后兼容)。
- 第一步:新增字段加默认值(
ALTER TABLE ... ADD COLUMN ... DEFAULT),部署新代码。 - 第二步:旧代码已废弃,再移除默认约束或旧字段。
- 第一步:新增字段加默认值(
- 零停机部署最大的敌人是不兼容的数据库变更,新增一个
-
Session & 文件锁:
- 如果Session存储在服务器本地文件,切换环境会导致用户Session丢失。建议将Session存入Redis/Memcached。
- 文件上传请使用集中存储(对象存储或NFS),不要依赖本地文件系统。
-
自动化工具(CI/CD):
- Deployer(PHP编写的部署工具,自带零停机重连功能)。
- Capistrano(Ruby,但PHP项目常用)。
- Envoy(Laravel官方工具)。
- Ansible / SaltStack 可以编写Playbook实现软链接切换。
总结建议
| 场景 | 推荐方案 |
|---|---|
| 中小项目、单台服务器 | 软链接切换 + 部署脚本中重置OpCache |
| 多台服务器、微服务 | 蓝绿部署 或 金丝雀部署 |
| 临时小改动、热修复 | 平滑重启PHP-FPM(但需接受极小概率错误) |
| 不想自己写脚本 | 使用 Deployer 工具(内置零停机发布策略) |
核心一句话: 将代码目录视为静态资源,每次发布创建一个新的不可变目录,通过原子性地切换软链接来实现瞬间切换,这是PHP零停机部署的基石。