PHP 预加载 OPcache
解读
国内一线/二线互联网公司的高阶 PHP 面试里,OPcache 预加载(preload)是区分“只会写业务代码”与“能扛高并发架构”的关键考点。
面试官真正想确认的是:
- 你是否清楚 OPcache 只缓存“编译后”的 opcode,而预加载能把指定文件直接驻留到 PHP 进程内存,彻底省掉 include/autoload 时的磁盘 IO 与编译开销;
- 你是否能在真实生产环境落地:如何挑选文件、如何处理依赖、如何平滑重启、如何兼容 Composer、如何监控命中率与内存增长;
- 你是否知道 7.4+ 与 8.x 在继承兼容性、匿名类、trait 初始化顺序上的坑,以及和 Swoole、RoadRunner 这类常驻进程的差异。
一句话:不是背概念,而是“能给出可落地的、可量化的、可回滚的”预加载方案。
知识点
- OPcache 基本原理:词法分析→语法分析→编译为 opcode→共享内存缓存→命中后跳过编译阶段。
- 预加载触发条件:php.ini 中 opcache.preload=/path/preload.php,仅在 master 进程启动时一次性执行;预加载文件必须生成 opcode 并常驻 shm,后续所有 worker 复用。
- 生命周期:preload 脚本执行时,PHP 处于“编译”阶段,尚未 fork worker,因此允许 include、define、class_exists,但不可调用未定义的函数或触发未加载的类;失败会直接导致 fpm/master 启动失败。
- 文件遴选策略:
① 必须“先被加载”才能被 opcache_compile_file() 真正编译;
② 优先圈定框架核心、高频路由、工具类、枚举、常量定义;
③ 避免预加载业务模型、频繁迭代的 Service,防止代码热更新失效;
④ 使用 Composer 的 classmap 与 PSR-4 规则自动生成文件列表,白名单+黑名单双机制。 - 依赖顺序:父类→接口→trait→子类,顺序错乱会在 preload 阶段抛 fatal error;需要拓扑排序。
- 内存评估:每条 opcode 平均占用 1.2
1.5 KB,预加载 3000 个类约增加 46 MB master shm;需通过 opcache_get_status(true)['memory_usage']['used_memory'] 实时监控。 - 平滑发布:
① 灰度机器先 preload→验证接口→对比 APCu/OPcache hit rate≥98%;
② 使用 php-fpm -t 检测配置合法性;
③ 通过 systemd 的 ExecReload=/bin/kill -USR2 实现老 worker 优雅退出,新 worker 继承已 preload 的 opcode;
④ 若失败,回滚 preload 脚本,重启 fpm,秒级恢复。 - 与 Swoole/RoadRunner 区别:后者本身就是常驻内存,opcode 常驻是天然行为,但仍可借助 preload 提前编译,减少 onWorkerStart 阶段的 autoload 开销。
- 常见坑:
- 匿名类/闭包序列化失败;
- 预加载文件里使用了 DIR 导致路径硬编码;
- trait 中使用了静态变量,在 fpm 不同 worker 间出现“交叉污染”;
- 8.0 之前继承预加载类时若出现“preloading class Foo with unresolved initializer” 会直接 coredump。
答案
“预加载 OPcache”指的是在 PHP-FPM(或 CLI-server)master 进程启动阶段,通过 opcache.preload 指定一个引导脚本,把框架核心、工具类等高频文件提前编译成 opcode 并常驻共享内存,后续所有 worker 无需再次磁盘 IO 和编译,从而将请求响应的 TTFB 再降低 5%~15%,并在高并发下显著减少 CPU sys 占用。
落地步骤:
- 在 php.ini 开启 opcache.enable=1、opcache.enable_cli=1(压测用)、opcache.memory_consumption=256;
- 编写 preload.php:使用 Composer 的 classmap 生成白名单,拓扑排序后循环 opcache_compile_file();
- 灰度单台机器,观察 php-fpm -t 通过、error_log 无 fatal、接口 200 率 100%、OPcache hit rate 提升 3~8 个百分点;
- 全量滚动发布,配合 Prometheus + Grafana 监控 opcache_memory_usage、opcache_hit_ratio、request_latency_p99;
- 上线后若需热更新业务代码,只需清空 preload 列表并 reload fpm,即可秒级回滚到普通 opcache 模式,保证可用性。
拓展思考
- 如何与“代码热更新”兼得?
思路:把“稳定层”(框架、基础类)放 preload,“迭代层”(Service、Controller)走普通 opcache,配合 inotify/k8s ConfigMap 触发 fpm reload,实现 90% 内存常驻 + 10% 秒级更新。 - 在 8.2 的 JIT 开启后,预加载是否还有收益?
实测:JIT 对 IO 密集场景提升有限,preload 减少编译耗时后,JIT 可集中精力优化热循环,二者叠加可将 Laravel 空路由 QPS 从 6k 提到 11k;但内存占用增加 30%,需权衡。 - 如果公司使用 Kubernetes + Alpine 镜像,如何降低 preload 带来的镜像大小与启动时间?
采用多阶段构建:第一阶段用 full-fat 镜像生成 preload 列表并序列化到 opcache.validate_timestamps=0 的缓存文件;第二阶段只拷贝缓存与 opcache.preload 脚本,最终镜像减少 40 MB,Pod 启动时间从 3 s 降到 800 ms。