[L3] OPcache 编译缓存的工作原理是什么?
一句话结论
OPcache 将首次编译产生的 Opcodes 存入共享内存,后续请求跳过编译直接执行缓存。
体系讲解
原理:PHP 的编译流程
PHP 在执行每个脚本前须经历完整编译链路:
编译是纯 CPU 密集型操作。对于不会变化的文件(生产环境的框架代码、业务代码),每次请求都重新编译是完全没有意义的浪费。
OPcache 的介入点
OPcache 在「生成 Opcodes」后、「Zend VM 执行前」拦截,将 Opcodes 序列化存入共享内存(shared memory)。后续请求到达时,OPcache 先检查共享内存中是否有有效缓存:
- 命中:直接从共享内存取出 Opcodes,交由 Zend VM 执行,完全跳过编译链路
- 未命中:重新编译,写入共享内存
共享内存架构与 PHP-FPM 的关系
PHP-FPM 运行时存在一个 master 进程和多个 worker 进程。OPcache 将共享内存映射到所有 worker 进程的地址空间中,使得:
- 同一份 Opcodes 只编译一次(通常由第一个处理该文件的 worker 完成)
- 其余所有 worker 共享读取同一块物理内存,不需要各自维护副本
- 在内存层面,100 个 worker 的 OPcache 内存消耗与 1 个 worker 相近
⚠️ 需查证:以上共享内存模型的描述基于 PHP 官方文档 opcache.memory_consumption 及 OPcache 扩展的公开行为,不涉及内部数据结构细节。
缓存失效机制:开发 vs 生产的核心差异
validate_timestamps(来源:php.net/manual/en/opcache.configuration.php)控制 OPcache 是否主动检查文件变更:
| 配置 | 行为 | 适用场景 |
|---|---|---|
validate_timestamps=1(默认开启)+ revalidate_freq=2 | 每 2 秒检查一次文件 mtime;有变化则重新编译 | 开发环境:代码频繁变更,需要及时生效 |
validate_timestamps=0 | 完全不检查文件变更,缓存永久有效直到手动清除 | 生产环境:代码通过部署脚本更新,部署时调用 opcache_reset() 或重启 FPM |
PHP 7.4+ Preload 机制
opcache.preload 允许在 FPM master 进程启动时(请求进入前)预先将指定脚本加载到共享内存,使这些文件的 Opcodes 在第一个请求到达之前就已就绪。
opcache.preload=/var/www/html/preload.php
opcache.preload_user=www-dataPreload 的效果:框架核心类(如 Laravel 的 Container、Router 等)被永久驻留内存,不受 validate_timestamps 影响,也不会因 opcache_reset() 而失效,需重启 FPM 才能更新。
Interned Strings(字符串驻留)
OPcache 还负责管理 interned strings:PHP 运行时大量重复出现的字符串常量(函数名、类名、字符串字面量等)在共享内存中只存储一份,所有 worker 共享指向同一地址,减少重复分配。
⚠️ 需查证:interned strings 的概念来源于 opcache.interned_strings_buffer 配置的官方文档说明,内部实现细节不在本题范围内。
考察意图
- 考察候选人是否理解 PHP 完整编译流程,而非仅知道「OPcache 能提速」
- 验证是否理解共享内存模型与 PHP-FPM 多进程的关系
- 考察生产环境与开发环境的缓存策略差异,以及部署时的失效操作
追问链
生产环境部署新代码后,OPcache 缓存如何清除?
简答:有三种方式。① 重启 PHP-FPM(最彻底,但会中断正在处理的请求);② 调用
opcache_reset()(通过一个 Web 请求执行,清除所有缓存);③ 调用opcache_invalidate($file, true)(只清除指定文件的缓存,精细化控制)。CI/CD 流程中常见做法是部署完成后发一个 HTTP 请求触发opcache_reset(),或使用 PHP-FPM 的 graceful reload(USR2信号)。为什么生产环境推荐将
validate_timestamps设为0?有什么风险?简答:
validate_timestamps=0避免每次请求检查 mtime,消除文件系统 I/O 开销,OPcache 命中率接近 100%。风险在于:部署新代码后如果忘记清除 OPcache,线上仍会执行旧代码。因此必须在部署流程中明确包含缓存清除步骤。Preload 与普通 OPcache 缓存有什么本质区别?
简答:普通 OPcache 缓存由「第一次被请求执行的 worker」触发编译并写入,有冷启动问题(重启 FPM 后首批请求需要重新编译)。Preload 在 FPM master 启动时执行,不需要等待请求触发,且 preload 的内容不会因
opcache_reset()失效(只能重启 FPM 清除)。代价是 preload 文件列表是静态的,框架升级时需要修改 preload 脚本。opcache_get_status()能观察到哪些关键信息?简答:返回数组包含:
opcache_enabled(是否启用)、memory_usage(已用/空闲/浪费内存,浪费比例过高意味着需要opcache_reset())、opcache_statistics(命中次数、未命中次数、命中率、缓存的脚本数量)。生产环境应定期监控命中率,通常健康值 > 99%。
易错点
以为重启 Nginx 能清除 OPcache:OPcache 是 PHP-FPM 的扩展,存储在 FPM 进程的共享内存中。重启 Nginx 只影响反向代理层,PHP-FPM 进程和其共享内存不受影响。清除 OPcache 必须针对 PHP-FPM(重启或调用
opcache_reset())。误以为
validate_timestamps=1是每次请求都重新编译:validate_timestamps=1配合revalidate_freq=N时,只在上次验证超过 N 秒后才检查文件 mtime,并非每次请求都检查。设为revalidate_freq=0才是每次请求都验证(完全不缓存效果,通常不建议)。忽略
opcache.memory_consumption与opcache.interned_strings_buffer的关系:interned strings 的内存从memory_consumption总量中划拨,而非独立分配。若两者设置不合理(如memory_consumption=64M但interned_strings_buffer=32M),留给 Opcodes 缓存的内存只剩 32 MB,大型框架可能因内存不足而频繁驱逐缓存(来源:php.net 官方文档用户评注)。
代码示例
<?php
// 查看 OPcache 运行状态(生产环境监控)
$status = opcache_get_status(include_scripts: false);
echo "命中率: " . round(
$status['opcache_statistics']['hits']
/ ($status['opcache_statistics']['hits'] + $status['opcache_statistics']['misses'])
* 100, 2
) . "%\n";
echo "缓存脚本数: " . $status['opcache_statistics']['num_cached_scripts'] . "\n";
echo "内存使用: " . round($status['memory_usage']['used_memory'] / 1024 / 1024, 1) . " MB\n";
echo "内存浪费: " . round($status['memory_usage']['wasted_memory'] / 1024 / 1024, 1) . " MB\n";
// 部署脚本:精细化失效单个文件(避免全量 reset 影响正在服务的请求)
opcache_invalidate('/var/www/html/src/Service/UserService.php', force: true);
// 全量清除(重新部署后)
opcache_reset();; php.ini — 生产环境推荐配置(来源:php.net/manual/en/opcache.configuration.php)
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0 ; 生产关闭自动验证,部署时手动清除
opcache.save_comments=1 ; 保留注释(Doctrine/PHPUnit 等框架依赖注解)
opcache.preload=/var/www/html/preload.php ; PHP 7.4+
opcache.preload_user=www-data