PHP 慢的锅,一半要算在「每次请求都重新编译」上
PHP 是解释型语言,传统执行模型是:每次 HTTP 请求进来,PHP 都要把 .php 源文件读出来、词法分析、语法分析、编译成中间字节码(opcode),然后才交给 Zend 虚拟机执行。对同一个页面,这个「编译」过程在每一次请求里都重复了一遍——明明文件内容一个字都没变。
OPcache 就是为了消灭这份重复劳动而生的。它把编译好的 opcode 缓存在共享内存里,下一次请求直接取出执行,跳过整个编译阶段。开启 OPcache 后,一个 WordPress 或 Typecho 站的 PHP 执行时间通常能砍掉 30% 到 50%。但很多人只是把它「打开」,参数全是默认值,白白浪费了一大半性能。
这篇文章讲的是 OPcache 的实战调优:每个关键参数该设多少、怎么算内存够不够、以及上线后必须处理的那个「改了代码不生效」的头号坑。
先确认 OPcache 是不是真的在跑
第一步永远是确认现状,别凭记忆。写一个探针文件放到站点目录:
cat > /var/www/html/op.php <<'EOF'
<?php
var_dump(opcache_get_status());
EOF浏览器访问 https://example.com/op.php,重点看几个值:opcache_enabled 是否为 true,cache_full 是否为 false,opcache_statistics 里的 hits 和 misses 比例,以及 used_memory 和 free_memory。
如果 opcache_enabled 是 false,说明根本没开。命令行 PHP 和 FPM 的 OPcache 是分开的——php -i | grep opcache 看到的是 CLI 的配置,网站跑的是 FPM,要用 <?php phpinfo(); 或探针确认 FPM 的情况。
另外注意:探针文件用完务必删除。它会把服务器内部信息暴露给任何人,是个实打实的安全隐患。
核心参数怎么设:一份个人站的推荐值
OPcache 的配置写在 php.ini 或单独的 opcache.ini 里(Debian/Ubuntu 下通常在 /etc/php/8.x/fpm/conf.d/10-opcache.ini)。逐个说关键项:
opcache.enable=1 —— 总开关,必须开。生产环境绝不能是 0。
opcache.memory_consumption=256 —— 给 opcode 缓存分配的共享内存(MB)。默认 128 对小型站够用,但如果你跑 WordPress + 一堆插件,或者有多个站点共用 FPM 池,建议提到 256 甚至 512。判断标准:看探针里的 used_memory,如果长期徘徊在总量的 80% 以上,就该加。
opcache.interned_strings_buffer=16 —— 用于存储驻留字符串(函数名、类名等重复的字符串)的内存(MB)。默认 8,现代框架建议 16 到 32。这个参数不容易被注意,但设小了会浪费内存去反复存储相同的字符串。
opcache.max_accelerated_files=20000 —— 最多能缓存多少个 PHP 文件。默认只有 10000。WordPress + 插件动辄几千个文件,加上主题、库文件,很容易超。设成 20000 以上比较安全。真实值的计算:find /var/www -name '*.php' | wc -l,然后取比它大的下一个 opcache.max_accelerated_files 合法值(该值必须是素数或 2 的幂附近的合法集合,PHP 会自动上调到最近的合法值,所以直接写大一点即可)。
opcache.validate_timestamps=1 和 opcache.revalidate_freq=60 —— 这是关于「代码变更是否自动生效」的两个参数,也是部署时的头号坑,下面单独讲。
opcache.save_comments=1 —— 保留注释。看着无所谓,但很多框架(尤其是用注解/Attribute 做路由和 ORM 映射的,如 Doctrine、Symfony)依赖注释。如果设成 0,这些框架会直接崩。除非你确定自己的代码不用注解,否则保持 1。
opcache.jit —— PHP 8 引入的 JIT 编译器。对典型的 CMS/网站类负载(大量字符串处理、I/O 密集),JIT 收益有限,有时甚至因为优化开销反而略慢。个人站建议保持默认关闭(opcache.jit=off),把精力放在上面那些基础参数上。JIT 更适合 CPU 密集的计算型任务。
头号大坑:改了代码,网站还是旧的
开启 OPcache 后,最常见的新手困惑是:「我明明改了 PHP 文件,刷新页面怎么还是老样子?」
原因在 opcache.validate_timestamps 和 opcache.revalidate_freq:
- 当
validate_timestamps=1时,OPcache 会定期检查源文件的时间戳,发现文件被改了就重新编译。revalidate_freq决定检查间隔(秒)。默认 2 秒,意味着最多 2 秒延迟——对开发环境很友好。 - 当
validate_timestamps=0时,OPcache 完全不检查文件是否变化,永远用缓存里的旧 opcode,性能最高,但代码改了必须手动清缓存才生效。
生产环境的推荐做法是:validate_timestamps=0(省去每次请求的 stat 系统调用,性能最优),配合部署流程里自动重载 PHP-FPM 或调用 opcache_reset()。这样两全其美——运行时不检查,部署时主动刷新。
手动清缓存的几个方式:
# 方式一:重启(或 reload)PHP-FPM,最彻底
systemctl reload php8.2-fpm
# 方式二:Web 端调用 opcache_reset()(需谨慎,加访问限制)
# cache_clear.php:
# <?php if ($_GET['token']==='你的密钥') { opcache_reset(); echo 'cleared'; }
# 方式三:命令行重置(对 FPM 池无效,只影响 CLI)
php -r 'opcache_reset();'注意方式三只重置换行环境下 CLI 的 OPcache,对网站跑的 FPM 进程无效——这是另一个常见误解。真正清 FPM 缓存的可靠方法是 reload/restart FPM,或者通过 FPM 能读到的 Web 探针调用 opcache_reset()。
一个容易被忽略的限制:内存满了会怎样
当缓存的 opcode 总量超过 memory_consumption,或者文件数超过 max_accelerated_files,OPcache 会进入「缓存满」状态,新的文件就不再被缓存,退回到每次编译。这时候你表面看 OPcache 是开的,实际性能却回到了原点。
探针里 opcache_get_status()['cache_full'] 会变成 true,opcache_statistics 里还会有一个 oom_restarts 或 hash_restarts 计数器,非零就说明发生过重启。所以调优后不要一劳永逸,隔一段时间回来看一眼这两个值。数值持续增长,就加大内存或文件数上限。
另一个细节:多个 FPM 池如果配置了不同的 opcache.memory_consumption,共享内存是会冲突的(同一 user 下)。多站点场景下,要么统一参数,要么给不同站点分配不同的 FPM 用户。
内存与文件数:被撑满的缓存等于没开
调优 OPcache 时,最容易被忽略的其实不是「怎么开」,而是「开满了怎么办」。OPcache 的共享内存是有限资源:opcache.memory_consumption 管字节数,opcache.max_accelerated_files 管文件数量上限。两者任意一个被撑满,OPcache 就会退化成「部分缓存」甚至「不再缓存新文件」的状态——表面上开关是开的,实际性能却悄悄回到了没开的水平。
怎么发现撑满?探针里的 cache_full 返回 true,或者 opcache_statistics 里的 oom_restarts、hash_restarts 计数器非零且在持续增长,这就是明确信号。一个健康的生产站,这两个计数器应该长期保持零。持续增长说明要么内存给小了,要么文件数上限给小了,需要调大后重启 FPM 让新值生效。
估算合适的文件数上限有个实用方法:直接数站点的 PHP 文件数量,再加一个安全余量。find /var/www -name '*.php' | wc -l 的结果,通常乘以 2 到 3 比较稳妥,因为运行时还会加载框架的 vendor 目录、依赖库等零星文件。设成 20000 到 30000 对大多数个人站都够用,多设一点不占多少内存——max_accelerated_files 决定的是哈希表容量,不是每个文件的实际 opcode 空间。
还有一个多站点共用的坑:如果一台服务器上跑多个站点、共用一个 PHP-FPM 用户,它们会共享同一块 OPcache 内存。此时内存竞争更激烈,memory_consumption 和文件数上限都要按「所有站点加起来」的量来设,而不是按单站。稳妥的做法是给不同站点分配不同的 FPM 池用户,让它们各自拥有独立的 OPcache 空间,互不干扰、便于单独排查。
上线前的验证与回滚
改完参数,重启 FPM,然后做两件事确认没问题:
- 再看一次探针,确认
cache_full=false、hits增长、used_memory健康(别贴到 90% 以上)。 - 实测响应时间。用
curl -o /dev/null -s -w '%{time_total}' https://example.com/连跑几次,和调优前的数字对比。还可以看 FPM 的slowlog有没有变少。
建议在改参数前把原配置备份成 10-opcache.ini.bak,出问题能秒回滚。OPcache 的参数大多不影响正确性(除了 save_comments),但 memory_consumption 设得太小可能导致某些大文件永远无法被缓存,表现是「大部分页面变快了,个别页面没变」。
总结一下这份调优清单:enable=1、memory_consumption=256、interned_strings_buffer=16、max_accelerated_files=20000、生产环境 validate_timestamps=0 并配合部署时 reload FPM、save_comments=1、JIT 保持关闭。这一套下来,配合前端的 HTTP/2、gzip/br 压缩和合理的缓存策略,一个本来要 400ms 的页面降到 150ms 以内并不难。PHP 的性能优化里,OPcache 是性价比最高的一环——前提是,你真的把参数调对了,而不是让它带着默认值空转。