PHP 预加载 OPcache

解读

国内一线/二线互联网公司的高阶 PHP 面试里,OPcache 预加载(preload)是区分“只会写业务代码”与“能扛高并发架构”的关键考点。
面试官真正想确认的是:

  1. 你是否清楚 OPcache 只缓存“编译后”的 opcode,而预加载能把指定文件直接驻留到 PHP 进程内存,彻底省掉 include/autoload 时的磁盘 IO 与编译开销;
  2. 你是否能在真实生产环境落地:如何挑选文件、如何处理依赖、如何平滑重启、如何兼容 Composer、如何监控命中率与内存增长;
  3. 你是否知道 7.4+ 与 8.x 在继承兼容性、匿名类、trait 初始化顺序上的坑,以及和 Swoole、RoadRunner 这类常驻进程的差异。
    一句话:不是背概念,而是“能给出可落地的、可量化的、可回滚的”预加载方案。

知识点

  1. OPcache 基本原理:词法分析→语法分析→编译为 opcode→共享内存缓存→命中后跳过编译阶段。
  2. 预加载触发条件:php.ini 中 opcache.preload=/path/preload.php,仅在 master 进程启动时一次性执行;预加载文件必须生成 opcode 并常驻 shm,后续所有 worker 复用。
  3. 生命周期:preload 脚本执行时,PHP 处于“编译”阶段,尚未 fork worker,因此允许 include、define、class_exists,但不可调用未定义的函数或触发未加载的类;失败会直接导致 fpm/master 启动失败。
  4. 文件遴选策略:
    ① 必须“先被加载”才能被 opcache_compile_file() 真正编译;
    ② 优先圈定框架核心、高频路由、工具类、枚举、常量定义;
    ③ 避免预加载业务模型、频繁迭代的 Service,防止代码热更新失效;
    ④ 使用 Composer 的 classmap 与 PSR-4 规则自动生成文件列表,白名单+黑名单双机制。
  5. 依赖顺序:父类→接口→trait→子类,顺序错乱会在 preload 阶段抛 fatal error;需要拓扑排序。
  6. 内存评估:每条 opcode 平均占用 1.21.5 KB,预加载 3000 个类约增加 46 MB master shm;需通过 opcache_get_status(true)['memory_usage']['used_memory'] 实时监控。
  7. 平滑发布:
    ① 灰度机器先 preload→验证接口→对比 APCu/OPcache hit rate≥98%;
    ② 使用 php-fpm -t 检测配置合法性;
    ③ 通过 systemd 的 ExecReload=/bin/kill -USR2 实现老 worker 优雅退出,新 worker 继承已 preload 的 opcode;
    ④ 若失败,回滚 preload 脚本,重启 fpm,秒级恢复。
  8. 与 Swoole/RoadRunner 区别:后者本身就是常驻内存,opcode 常驻是天然行为,但仍可借助 preload 提前编译,减少 onWorkerStart 阶段的 autoload 开销。
  9. 常见坑:
    • 匿名类/闭包序列化失败;
    • 预加载文件里使用了 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 占用。
落地步骤:

  1. 在 php.ini 开启 opcache.enable=1、opcache.enable_cli=1(压测用)、opcache.memory_consumption=256;
  2. 编写 preload.php:使用 Composer 的 classmap 生成白名单,拓扑排序后循环 opcache_compile_file();
  3. 灰度单台机器,观察 php-fpm -t 通过、error_log 无 fatal、接口 200 率 100%、OPcache hit rate 提升 3~8 个百分点;
  4. 全量滚动发布,配合 Prometheus + Grafana 监控 opcache_memory_usage、opcache_hit_ratio、request_latency_p99;
  5. 上线后若需热更新业务代码,只需清空 preload 列表并 reload fpm,即可秒级回滚到普通 opcache 模式,保证可用性。

拓展思考

  1. 如何与“代码热更新”兼得?
    思路:把“稳定层”(框架、基础类)放 preload,“迭代层”(Service、Controller)走普通 opcache,配合 inotify/k8s ConfigMap 触发 fpm reload,实现 90% 内存常驻 + 10% 秒级更新。
  2. 在 8.2 的 JIT 开启后,预加载是否还有收益?
    实测:JIT 对 IO 密集场景提升有限,preload 减少编译耗时后,JIT 可集中精力优化热循环,二者叠加可将 Laravel 空路由 QPS 从 6k 提到 11k;但内存占用增加 30%,需权衡。
  3. 如果公司使用 Kubernetes + Alpine 镜像,如何降低 preload 带来的镜像大小与启动时间?
    采用多阶段构建:第一阶段用 full-fat 镜像生成 preload 列表并序列化到 opcache.validate_timestamps=0 的缓存文件;第二阶段只拷贝缓存与 opcache.preload 脚本,最终镜像减少 40 MB,Pod 启动时间从 3 s 降到 800 ms。