PHP 8.0 之后,JIT(Just-In-Time 编译器)成了版本升级宣传里最亮眼的一项,很多人以为开了 opcache.jit=tracing 网站就能快一截。结果不少站长升级硬盘配置、改完 ini 重启 PHP-FPM,一测发现 TTFB 纹丝不动,甚至偶尔变慢,内存还多占了几百兆。
这篇文章不讲 JIT 编译原理,只讲一件事:对于一个跑 WordPress 或 Typecho 的普通站点,JIT 到底该不该开、怎么开、用什么数据来判断。结论可能和你预期相反——大多数内容站的正确答案是「不开」,或者只开最轻量的那一档。
先分清 OPcache 和 JIT 是两件事
这是最容易混淆的地方。PHP 的加速链路分成两段:
- OPcache:把 PHP 源码编译成 opcode 后缓存起来,避免每次请求都重新解析编译。这是所有站点都该开的,收益巨大。
- 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_query、file_get_contents、curl_exec、Redis::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=12051205 是 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 = 500max_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() 在代码里改是无效的——这也是「配了但没生效」的常见原因。