PHP OPcache 与 JIT 实测对比:内容站到底该不该开 opcache.jit=tracing,附压测判断方法

PHP 8.0 之后,JIT(Just-In-Time 编译器)成了版本升级宣传里最亮眼的一项,很多人以为开了 opcache.jit=tracing 网站就能快一截。结果不少站长升级硬盘配置、改完 ini 重启 PHP-FPM,一测发现 TTFB 纹丝不动,甚至偶尔变慢,内存还多占了几百兆。

这篇文章不讲 JIT 编译原理,只讲一件事:对于一个跑 WordPress 或 Typecho 的普通站点,JIT 到底该不该开、怎么开、用什么数据来判断。结论可能和你预期相反——大多数内容站的正确答案是「不开」,或者只开最轻量的那一档。

先分清 OPcache 和 JIT 是两件事

这是最容易混淆的地方。PHP 的加速链路分成两段:

  1. OPcache:把 PHP 源码编译成 opcode 后缓存起来,避免每次请求都重新解析编译。这是所有站点都该开的,收益巨大。
  2. JIT:在 opcode 执行阶段,把热点代码进一步编译成机器码,跳过 Zend VM 的解释执行。收益取决于代码是否是 CPU 密集型的循环计算。

关键点在于:JIT 优化的对象是纯计算,不是 IO。而一个典型的 WordPress 或 Typecho 页面请求,时间基本花在这几个地方:

  • MySQL 查询与网络往返
  • 文件系统 stat 调用(模板加载、插件扫描)
  • Redis / Memcached 网络调用
  • PHP-FPM 进程调度与进程间通信
  • 模板渲染里的字符串拼接

这些全都是 IO 和内存操作,JIT 一个都帮不上。只有当你的代码里有大量数学运算、图像处理、加解密、或者纯 PHP 实现的复杂算法时,JIT 才有明显价值。

怎么判断你的站是不是 JIT 的目标用户

看 CPU 占用和响应时间的关系

最直接的判断方法:站点压力上来的时候,CPU 是不是瓶颈。

# 实时观察 PHP-FPM 进程的 CPU 占用
top -b -n 1 | grep php-fpm | head -10

# 或者用 pidstat 看单个进程
pidstat -u -p $(pgrep -d, php-fpm) 1 5

如果 CPU 占用长期低于 30%,而 TTFB 依然很高,那瓶颈在 IO 或数据库,JIT 完全帮不上忙。反过来,如果 CPU 跑满、响应时间随 CPU 上升而线性劣化,才值得考虑 JIT。

更精确的做法是抓一次请求的函数级耗时。装个 XHProf 或者用 tideways 的免费版,跑一次文章页请求,看耗时排行榜:

# 用 Xdebug 的 trace(仅调试用,千万别在生产开)
xdebug_start_trace('/tmp/trace.xt');
// ... 你的业务代码 ...
xdebug_stop_trace();

trace 结果里,如果排在前面的是 mysqli_queryfile_get_contentscurl_execRedis::get 这类函数,那结论很明确:JIT 无用。

看 OPcache 命中率够不够高

在开启 JIT 之前,先把 OPcache 本身调好。如果 OPcache 都没吃满,谈 JIT 属于本末倒置。

# 查看 OPcache 状态(需要装个简单的状态页)
cat > /var/www/html/opcache.php <<'EOF'
<?php
header('Content-Type: text/plain');
$s = opcache_get_status(false);
printf("命中率: %.2f%%\n", $s['opcache_statistics']['opcache_hit_rate']);
printf("缓存脚本数: %d / 上限 %d\n",
    $s['opcache_statistics']['num_cached_scripts'],
    $s['opcache_statistics']['max_cached_keys']);
printf("已用内存: %.2f MB / %d MB\n",
    $s['memory_usage']['used_memory'] / 1048576,
    ($s['memory_usage']['used_memory'] + $s['memory_usage']['free_memory']) / 1048576);
printf("重启次数(OOM): %d\n", $s['opcache_statistics']['oom_restarts']);
EOF

判断标准:

  • 命中率应该达到 99% 以上。低于 95% 说明 opcache.max_accelerated_files 太小或者 validate_timestamps 设置有问题。
  • oom_restarts 必须是 0。非零说明内存不够,缓存被反复清空,性能会剧烈波动。
  • 缓存脚本数要小于上限。如果正好等于上限,立刻调大 max_accelerated_files

一个对中小站比较稳妥的 OPcache 配置:

opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=256
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=20000
opcache.validate_timestamps=1
opcache.revalidate_freq=60
opcache.save_comments=1
opcache.fast_shutdown=1

注意 save_comments=1 不要关。很多 PHP 框架和插件依赖注解(annotation)解析,关掉注释会导致功能异常——这个坑每年都有人踩。

JIT 的四种模式,别一上来就 tracing

PHP 8 的 JIT 通过 opcache.jit 配置,取值是一个四位数字(CRTO 格式),常用简写有:

# 模式对照(简写形式)
opcache.jit=disable     # 关闭 JIT
opcache.jit=off         # 只编译不执行,用于测试
opcache.jit=tracing     # 追踪热点代码,优化力度最大
opcache.jit=function    # 按函数粒度编译,最保守

关于 tracing 模式,有几个必须知道的现实:

  • 编译本身要消耗 CPU。请求量小的站,编译成本可能超过节省的执行时间。
  • 需要更多内存存放编译后的机器码,实测常多占 100 到 300 MB。
  • PHP 8.0 到 8.3 的 JIT 在部分代码模式下有稳定性问题,尤其是涉及 eval、动态变量、反射的代码。
  • WordPress 生态里大量使用动态调用和 call_user_func,这些正是 JIT 最不擅长的场景。

推荐的缓冲配置

opcache.jit_buffer_size=64M
opcache.jit=1205

1205 是 tracing 模式中相对保守的一组参数组合,触发阈值较高,只对反复执行无数次的热点代码生效。相比直接写 tracing,它编译的代码少,副作用小。

关键:opcache.jit_buffer_size=64M 必须显式写出来。 如果只写 opcache.jit=tracing 而 JIT buffer 为 0,PHP 会直接忽略 JIT 配置,你测了半天其实根本没开——这是最经典的「以为开了其实没开」事故。

实测对比:怎么自己测

不要照搬网上的结论,用 ab 或 wrk 在自己站上跑一轮。

# 1. 先准备一个未登录状态下能访问的文章页
URL="https://www.example.com/1234.html"

# 2. 开 JIT 前压测 30 秒,记录 P95
ab -n 2000 -c 20 -k "$URL" 2>&1 | grep -E "Requests per second|Time per request|95%"

# 3. 改完 ini 重启,等 OPcache 预热(先手动访问 100 次)
for i in $(seq 1 100); do curl -s -o /dev/null "$URL"; done

# 4. 再压测一轮,对比数字
ab -n 2000 -c 20 -k "$URL" 2>&1 | grep -E "Requests per second|Time per request|95%"

重点看两个数字:吞吐量(Requests per second)P95 响应时间。均值容易被少数极快请求拉平,P95 才能反映真实体感。

在多数内容站上,你会看到 JIT 带来的吞吐提升在 0% 到 5% 之间,属于噪声范围。如果压测结果是负的,或者波动很大,说明这台机器本来就 IO 受限,别开。

别忘了关掉再测一次

测试要严格:JIT 开 → 压测 → JIT 关(重启)→ 压测,两轮的缓存状态要一致。如果第一轮是冷缓存第二轮是热缓存,你测出来的差距全是缓存造成的,跟 JIT 无关。

比 JIT 收益更大的几个优化

与其纠结 JIT,不如先把这些做了。按投入产出排序:

1. 打开 realpath_cache

PHP 每次 include 文件都要解析真实路径,涉及多次 stat 调用。WordPress 一次请求可能触发上千次 stat。

realpath_cache_size=4096K
realpath_cache_ttl=600

这两行在几乎所有内容站上都能带来可见提升,成本为零。

2. 调 PHP-FPM 进程模型

默认的 pm=dynamic 在流量波动时频繁创建销毁进程。对配置较低的 VPS,改成 ondemand 反而更省内存:

pm = ondemand
pm.max_children = 20
pm.process_idle_timeout = 10s
pm.max_requests = 500

max_requests=500 很重要——它让进程定期退出重建,避免长期运行累积内存泄漏。设太小会频繁重建损耗性能,500 到 1000 是常用区间。

3. 把 OPcache 预热做起来

重启 PHP-FPM 后第一批请求总是慢的,因为 OPcache 是空的。写个脚本,重启后自动跑一遍全站热门 URL:

#!/bin/bash
# /root/opcache_warmup.sh
URLS=$(mysql -uroot -pPASSWORD blog_db -N -e \
  "SELECT CONCAT('https://www.example.com/', cid, '.html')
   FROM typecho_contents WHERE type='post' AND status='publish'
   ORDER BY created DESC LIMIT 200;")

for u in $URLS; do
  curl -s -o /dev/null -H "User-Agent: warmup" "$u"
done
echo "预热完成: $(date)" >> /var/log/warmup.log

把它挂到 PHP-FPM 重启之后执行,用户就永远遇到的是热缓存。

4. 真·IO 优化:把 session 挪出去

如果 PHP 用文件存储 session,每次请求都要锁文件。改成 Redis:

session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379?timeout=1&database=1"

注意 database=1 和那些 ?& 的写法——这个 DSN 格式很挑,多一个空格就静默失效回退到文件存储。

结论:什么情况下才值得开 JIT

给一个简单的决策清单:

  • 你的站是 WordPress、Typecho、Discuz 这类成熟 CMS → 不开,或者只开 function 模式
  • 你的代码里有大量数值计算、图像处理、自定义加解密 → 值得开 tracing
  • 你用的是 PHP 8.1 以下版本 → 先升级版本,新版本自身的性能提升比 JIT 大得多
  • 你的 VPS 内存小于 2G → 不开,JIT 的内存开销会挤占宝贵的余量
  • 你的站刚上线、流量很小 → 不开,编译成本大于收益

PHP 版本升级本身的收益(8.0 到 8.1、8.1 到 8.2)通常远大于 JIT 在内容站上的收益。与其花时间调 JIT 参数,不如确认一下 OPcache 配置是不是正确、realpath_cache 有没有开、PHP-FPM 进程数是不是合理。

最后提醒一句:改完 php.ini 一定要 php -i | grep jit 确认配置真的生效了。opcache.jit 是 PHP_INI_SYSTEM 级别,只能通过 ini 文件或启动参数设置,用 ini_set() 在代码里改是无效的——这也是「配了但没生效」的常见原因。

Last modification:September 21st, 2026 at 01:27 pm

Leave a Comment