摘要:接手一个跑在 PHP 7.4 上的老项目后,决定分三次把它升到 8.3:8.0(构造器属性提升 + JIT)、8.1(枚举 + 只读属性 +纤程)、8.2/8.3(动态属性废弃 + 类型化类常量)。升级本身不难,难的是老代码里那些"7.4 时代合法、8.x 直接报错"的写法,以及 OPcache/JIT 在生产环境的真实收益评估。本文是一份可以直接照着做的升级清单 + 性能调优记录。
关键词:PHP 8.3、PHP 升级、OPcache、JIT、Laravel、性能优化
适合人群:维护老 PHP 项目的工程师、准备升级 PHP 版本的团队
一、升级策略:小步快跑,而不是一步登天
大版本跨级升级(7.4 直接上 8.3)是最容易翻车的方式。我们的节奏:
7.4 → 8.0:处理最大的破坏性变更(命名参数、严格类型影响) 8.0 → 8.1:处理序列化/枚举相关,收益是纤程和只读属性 8.1 → 8.3:动态属性废弃是主要工作量每个版本:CI 全量跑测试 → 预发环境灰度 3 天 → 观察错误日志(重点看 Deprecation)→ 全量。升级前先把 PHPStan(level 6 起步)跑一遍,它对废弃特性的静态扫描比人肉 grep 靠谱得多。
工具清单:
# 静态分析,优先扫废弃用法phpstan analyse src--level=6# Rector 自动修复大量机械性变更rector process src--set=php80二、破坏性变更实战清单
1. 动态属性废弃(8.2 起,8.3 报错级)
老代码里随手给对象挂属性是重灾区:
// 7.4 合法,8.2 Deprecation,8.3 彻底报错$report=newReport();$report->cachedTotal=123;// 类里根本没声明这个属性修复有三种姿势,按优先级:
// ① 该声明声明(首选)classReport{public?int$cachedTotal=null;}// ② 确实需要动态结构的场景:允许动态属性#[AllowDynamicProperties]classReport{}// ③ 真正的键值对需求:改用数组或 stdClass —— 说明这个类设计本来就有问题2. 函数签名与返回值收紧
大量内置函数在 8.x 加了参数/返回值类型声明,老代码传错类型从 Warning 升级为TypeError。高频踩点:
utf8_encode/utf8_decode已废弃 → 用mb_convert_encodingstr_contains/str_starts_with替代手写strpos(...) !== false- 传 null 给非 nullable 参数的内部函数(如
strlen(null))从静默容忍变成报错——表单和 HTTP 入参是重灾区,入口处补类型校验
3. 枚举:顺便重构掉一批魔法字符串
8.1 的枚举不只是新语法,是老代码"魔法字符串/类常量簇"的还债契机:
// 以前constSTATUS_PENDING=1;constSTATUS_PAID=2;...// 现在enumOrderStatus:int{casePending=1;casePaid=2;caseRefunding=3;publicfunctioncanRefund():bool{return$this===self::Paid;}}注意:老数据库里存的还是 int,from()找不到值会抛ValueError,入库出口都要 try/catch 或用tryFrom()。
三、OPcache 与 JIT:真实收益测量
JIT 在 Web 场景的现实
JIT 是 PHP 8 最大的营销点,但对典型 Web 业务(IO 密集:DB/Redis/HTTP),收益接近于零。我们压测结果:开启 JIT 后接口 P95 基本没动,因为时间都花在等数据库上了。
JIT 真正有收益的场景:CPU 密集的批处理、图片处理、复杂计算脚本。我们的最终配置:
; php.ini 生产配置 opcache.enable=1 opcache.memory_consumption=256 opcache.interned_strings_buffer=32 opcache.max_accelerated_files=20000 ; 大项目文件数多,默认值不够 opcache.validate_timestamps=0 ; 生产关掉时间戳校验,配合发布重载 opcache.jit=disable ; Web 场景实测无收益,关掉省内存validate_timestamps=0是双刃剑:性能最好,但发布后必须手动重载 OPcache,否则线上跑的还是旧代码。我们的发布脚本里加了一步:
# 发布完成后重载 opcache(php-fpm 重启或 web 专用重载端点)php-r'opcache_reset();'# CLI 无效,实际用 fpm reload 或应用内管理端点systemctl reload php8.3-fpm实测最大的性能来源不是 JIT
升级 8.x + OPcache 调优后接口 P95 从 210ms 降到 140ms,但拆解收益发现:
- OPcache 调优(max_accelerated_files、interned_strings):约 30% 提升
- 8.x 本身的引擎优化(命名参数、match、字符串处理优化):约 15%
- 代码层修复 N+1:约 40%(这才是大头,和版本无关)
- JIT:0%
所以"升级 PHP 8 能让网站快 10 倍"的营销话术听听就好,慢的根源九成在 N+1 查询和缺索引。
四、Laravel/常驻场景的配套问题
队列与定时任务:Supervisor 配置的三个细节
[program:laravel-worker] command=php /var/www/artisan queue:work redis --sleep=1 --tries=3 --max-time=3600 numprocs=4 autostart=true autorestart=true stopwaitsecs=30 ; 关键:给长任务优雅退出的时间--max-time=3600:worker 每小时自杀重启,防内存泄漏累积(老代码里总有static缓存和查询构建器堆积)stopwaitsecs小于队列任务的预期最大执行时长,否则强制 kill 造成任务假死- 发发布时
php artisan queue:restart+ Supervisor 不冲突:restart 只是让 worker 处理完当前任务后退出,Supervisor 会拉起新代码的 worker
FPM 进程池按流量算,别拍脑袋
pm = dynamic pm.max_children = 30 ; = 可用内存 / 单进程峰值内存(先实测) pm.start_servers = 8 pm.min_spare_servers = 4 pm.max_spare_servers = 12 pm.max_requests = 1000 ; 每处理 1000 请求回收进程,同样防泄漏算max_children前先压测单进程内存峰值(常见误区是按 30MB 算,实际带框架的进程轻松 80MB+)。一台 4G 的机器按 2G 给 FPM 算,max_children = 2048 / 80 ≈ 25。
五、升级后必做的回归检查
- 序列化兼容:跨版本升级期间,Redis 里旧版本序列化的对象(尤其含枚举的)反序列化会炸。缓存类数据升级前统一清空,或全部改 JSON 序列化
- 依赖矩阵:
composer check-platform-reqs过一遍,老框架(Laravel < 9)根本不支持 8.2+,框架版本要和 PHP 版本一起升 - 扩展兼容:老项目的自研/三方 pecl 扩展(redis、swoole、加密扩展)在 8.3 下编译不过的不少,提前在 CI 镜像里验证
- 错误日志监控加一条规则:
Deprecated:和TypeError突增告警,升级灰度期它们就是你的烟雾报警器
六、写在最后
PHP 版本升级的账要这么算:语言特性和性能提升是顺带的,真正的价值是新版本是所有现代工具链的门票——静态分析、新框架、新扩展全都要求 8.x。老项目越晚升级,欠的债越滚越大,这笔账早晚要还,早还便宜。
同系列已发布《Laravel 队列在生产环境的三种死法与自救:Supervisor、超时与失败任务》,感兴趣的关注一下。
有升级问题的评论区见,点赞 + 收藏是更新的动力。