改了PHP代码部署上去,刷新页面还是老逻辑;或者压测时CPU一直下不来,看OPcache状态命中率只有百分之七十几。这两种现象其实是同一个东西的两面:OPcache要么压根没接管,要么缓存太小在不停淘汰。按排查顺序走一遍。
先确认到底有没有开
php -v输出里带with Zend OPcache那行,只说明命令行那个PHP加载了扩展,跟FPM没关系。两套配置目录是分开的,Ubuntu与Debian上是/etc/php/8.3/cli/与/etc/php/8.3/fpm/,CentOS与RHEL上是/etc/php.d/与/etc/php-fpm.d/。而opcache.enable_cli默认值是0,命令行下字节码缓存本来就是关的,php -v照样会把那行名字打出来。
真正要确认的是FPM进程池里生效的值:
php-fpm8.3 -i | grep -E "^opcache\.(enable|memory_consumption|max_accelerated_files|validate_timestamps|revalidate_freq) " systemctl show -p MainPID php8.3-fpm
或者建一个状态页。OPcache的共享内存挂在FPM master进程上,只有通过FPM跑起来的请求才读得到它的数据,在命令行里调opcache_get_status()拿到的是CLI自己那一块,多数时候直接返回false。
<?php
$s = opcache_get_status(false);
$st = $s['opcache_statistics'];
printf("hit rate: %.2f%%\n", $st['hits'] / ($st['hits'] + $st['misses']) * 100);
printf("cache_full: %s\n", $s['cache_full'] ? 'yes' : 'no');
printf("scripts: %d / %d\n", $st['num_cached_scripts'], $st['max_cached_keys']);
printf("mem used/free/wasted: %d / %d / %d\n", $s['memory_usage']['used_memory'], $s['memory_usage']['free_memory'], $s['memory_usage']['wasted_memory']);
这个文件要放在web根目录之外,或者用nginx限制成只允许127.0.0.1。它本身是个信息泄露口,见过直接被扫到的,opcache_get_status(true)会把整个项目的文件路径列表原样吐出去。opcache.restrict_api可以指定哪个脚本才允许调这些函数。
状态里几个字段怎么读
opcache_statistics里的hits与misses算命中率,跑热的应该在99.9%往上,misses停在一个固定数字不动。hits涨、misses也跟着涨,说明缓存在淘汰,脚本被反复重新编译。num_cached_scripts逼近max_cached_keys、同时cache_full为true,就是文件数超了。
max_cached_keys不会等于opcache.max_accelerated_files填的那个数。配置项默认10000,实际生效的是不小于它的第一个素数,候选表里是223、463、983、1979、3907、7963、16229、32531、65407、130987这几档。填10000会落到16229,填20000落到32531,状态页里看到的数比配置大,不用奇怪。
memory_usage里的wasted_memory是改过代码的脚本留下来的空洞,current_wasted_percentage超过opcache.max_wasted_percentage(默认5)就触发一次整体重启,hash_restarts加一。oom_restarts涨说明memory_consumption(默认128,单位MB)不够,脚本挤不进去。这两个计数但凡持续增加,命中率一定难看。
还有一个常被忽略的:interned_strings_usage。类名、方法名、数组键这类字符串常量存在interned_strings_buffer(默认8,单位MB)里,它满了字符串会退化成普通分配,状态页上free_memory接近0就是信号,命中率却未必掉得明显。
缓存写满了怎么给
按代码量给。一个中型的Laravel或Symfony项目,vendor加上自己的代码,文件数轻松过两万,128MB也常常压不住。常见做法是memory_consumption给到256或512,max_accelerated_files抬到20000以上,interned_strings_buffer从8提到16或32。
; /etc/php/8.3/fpm/conf.d/99-opcache-tuning.ini opcache.enable=1 opcache.memory_consumption=256 opcache.interned_strings_buffer=16 opcache.max_accelerated_files=20000 opcache.validate_timestamps=1 opcache.revalidate_freq=2
别动发行版自带的10-opcache.ini,另建一个序号更大的drop-in,加载靠后能覆盖前面的值。改完systemctl reload php8.3-fpm。
reload与restart对OPcache的影响不同。reload只重建worker,共享内存由master持有,缓存不丢;restart是master一起重启,缓存清空,之后要重新预热,刚起来的那几分钟全是miss。线上想清缓存又不想丢,通过FPM请求触发opcache_reset()更合适。反过来在命令行里调opcache_reset()清不掉FPM那块,两块内存不搭界,这也是部署脚本里最常写错的一处。
改了代码不生效
validate_timestamps=1时,OPcache每隔revalidate_freq秒(默认2)去stat一次文件,改动最多延迟2秒可见。生产环境关掉它能省掉每个请求一次stat,代价是部署时必须自己清:reload不清缓存,要restart或者调opcache_reset()。
opcache.file_update_protection默认2秒,意思是修改时间落在2秒以内的文件不缓存,防止读到写了一半的内容。用rsync或git pull部署完立刻压测,前两秒的请求全是miss,命中率会被拉下来。之前碰到过部署脚本刚跑完就去看状态页,free_memory掉得莫名其妙,隔几秒再测就正常了。
用了opcache.preload的(PHP 7.4.0起)要单独留意:preload文件本身变了必须restart,reload与opcache_reset都不重新加载它。opcache.preload_user不设的话以root身份预加载,共享内存里的权限跟worker对不上,实测在低权限池里会取不到。
版本差异
PHP 7.4、8.0、8.1、8.2、8.3、8.4、8.5,这条线上OPcache的默认行为动过好几次。PHP 8.4.0(2024年11月发布)把opcache.jit的默认值从tracing改成disable,jit_buffer_size从0改成64M,等于8.4.0之前默认完全不编译JIT,之后默认开着但走保守路径。PHP 8.5.0(2025年11月20日发布)把OPcache直接编译进内核,配置文件里那行zend_extension=opcache要删掉,留着会报模块重复加载;同一版里jit_hot_loop的默认值从64调到了61。
opcache.consistency_checks在PHP 8.1.18、8.2.5,被禁用,8.3.0移除,老配置里留着这条的,升上去之后被当成未知指令处理,不报错、直接忽略,容易以为参数生效了。opcache.save_comments默认1,关掉能省一点内存,但依赖注解的框架会取不到注释。opcache.validate_permission与opcache.validate_root自7.0.14起可用,多人共用一台机器时开着更稳妥。
数据截至2026年10月。