编程 opcache.jit=tracing 白掉 17% RPS:PHP tracing JIT 的 use-after-free 与 buffer_free 不回收

2026-11-03 21:31:41

PHP 上游 JIT 的 use-after-free:我们为什么把默认值开了又关

今年 8 月,ePHPm 在单站点 serve 模式下把 PHP 的 tracing JIT 打开成了默认。三天后,所有平台上的默认又退回关闭,到现在仍然是关闭状态。原因是 PHP tracing JIT 里的一个 use-after-free,跟 ePHPm 本身没有关系:原版 php -S 配原版 Laravel skeleton,几个请求就会崩。修复已经在上游发布。

之前为什么一直关着:-17% RPS

从 7 月起 ePHPm 就关着 JIT,这是有实测依据的。在 cpu.php 基准上,opcache.jit=tracing 让 RPS 掉了 17%。这个基准大部分时间跑在 hash() 这个内建函数里。JIT 编译的是 PHP 字节码;当热点代码是 C 内建函数时,没有字节码可编译,tracing 的开销就是纯亏损。

Windows 上的反向数据

后来做 Windows 性能时,情况反过来了。MSVC 构建的 PHP 用的是最慢的 VM dispatch 模式。在 Ryzen 9 5950X 上跑纯 PHP 的 CPU 循环,JIT 把单个请求从 5.56 ms 降到 2.33 ms。

所以 ephpm#350 交付的是按场景分档的默认值,不是全局开关:

模式默认值
单站点 ephpm servetracing
多租户(sites_dir)disable
worker 模式disable(等 soak)
ephpm dev关

多租户为什么保持关闭:buffer_free 不回收

多租户继续关着,来自第二组测量。按站点调用 opcache_invalidate() 不会回收 JIT buffer,buffer_free 一动不动,只有完整的 opcache_reset 才能把它拿回来,而 hardening preset 把 reset 禁掉了。在一个租户天天部署的宿主里,buffer 会被填满,JIT 会悄无声息地停止编译。

相关配置项是 [php] opcache_jit = "tracing" | "function" | "disable"。启动时会打一行日志说明 JIT 在做什么、为什么这么做,另外还有两个 gauge 分别显示 buffer 大小和剩余空间。

复现这个现象只需要看 opcache_get_status() 里的 jit 段:

<?php
$s = opcache_get_status();
printf("buffer_size=%d buffer_free=%d\n",
    $s['jit']['buffer_size'], $s['jit']['buffer_free']);

在 FPM 里连续触发几次 opcache_invalidate() 后再读一次,buffer_free 不变;执行一次 opcache_reset() 才会回升。要验证 JIT 本身是否生效,直接写 ini:

opcache.jit=tracing
opcache.jit_buffer_size=64M

命令行临时验证时注意 CLI 和 FPM 不共享 opcache 缓存,各进程有各自的内存段,php -d opcache.enable_cli=1 -d opcache.jit=tracing -r '...' 里读到的数字不代表 FPM 的状态:

php -d opcache.enable_cli=1 -d opcache.jit=tracing -d opcache.jit_buffer_size=64M \
    -r 'var_dump(opcache_get_status()["jit"]);'

定位到上游 UAF,修复进 8.4.26 / 8.5.11

问题一路追到 php-src 的 use-after-free。PR 在 8 月 24 日以 landed 状态关闭,修复随 PHP 8.4.26 和 8.5.11 发布:9 月 22 日打 tag,24 日正式发布。我拿 release tag 和修复 commit 逐个核对过,两个版本都包含该修复。我们的 php-sdk 流水线在 9 月底发布了 8.5.11 和 8.4.26 的构建。

现在:pin 还没升,默认仍是 disable

到今天为止,ePHPm 的 SDK pin 仍然是 8.4.23 和 8.5.7。这两个版本都受影响,所以默认值保持 disable。原因枚举上的注释写得很清楚:等「php-src PR 21710 合并、且 pin 的 SDK 越过它」之后再回来讨论。前半句已经完成,后半句的门槛是:

  1. 把 pin 升到 8.4.26 和 8.5.11 或更高。
  2. 在新的 SDK 上强制 opcache_jit = "tracing",重跑两个 bisect fixture:Windows 上的 Laravel skeleton,以及 Linux 上 uncached-parent 那条路由。
  3. 只有这两步过了,才重新考虑单站点的默认值,而且只改单站点。

多租户保持关闭的理由(buffer 拿不回来)跟这个 bug 没有关系,不受上述进度影响。

复制全文 生成海报 PHP OPcache JIT 性能优化 use-after-free

推荐文章

程序员茄子在线接单