preloading 脚本编写与内存节省

解读

国内一线互联网公司在高并发业务(如电商大促、直播秒杀)中,普遍把 PHP 升级到 7.4+ 并开启 OPcache。OPcache 虽然缓存了字节码,但类、函数、trait 的符号表仍在每次请求时重复构建,导致 fpm-worker 内存峰值高、冷请求延迟大。Preloading 允许在 fpm-master 启动阶段就把指定文件加载到共享内存,所有 worker 复用同一份符号表与类定义,从而节省 20%~40% 内存、降低 10%~30% 延迟。面试时,考官想确认候选人是否真正落地过生产级 preloading,而不是仅仅“听过概念”。因此需要给出可落地的脚本、内存验证方法、回滚预案,并权衡“预加载范围”与“内存占用”的矛盾。

知识点

  1. 启动阶段:php-fpm 在 master 进程中执行一次 preload 脚本,执行时机在 php_module_startup 之后、worker 派生之前。
  2. 作用域:preload 脚本运行在全局命名空间,不能使用 use,所有类、函数、常量必须显式引入。
  3. 失败策略:preload 阶段出现致命错误,master 直接退出,fpm 无法启动;必须做好语法预检与灰度。
  4. 内存归属:预加载后的类占用 shm(系统共享内存),不计入 memory_get_usage(true),但会计入 opcache_get_status()['memory_usage']['used_memory']
  5. 失效条件:文件 mtime 变化或 opcache 重启才会重新加载;日常上线需要联动 opcache_reset() 或平滑重启 fpm。
  6. 框架适配:Laravel/Symfony 的 vendor 目录含有大量“可能用不到”的类,需要结合 composer/class-map 与业务扫描工具做白名单。
  7. 监控指标:关注 opcache_hit_rateopcache_memory_usageworker_rss(通过 ps -eo pid,rss,comm | grep fpm)、request_latency p99
  8. 兼容性:PHP 7.4+ 才支持;CLI 模式无效;Windows 下无 shm 分离,收益低。

答案

下面给出一套在 2023 年双十一某头部电商实际落地的 preloading 脚本与配套方案,可直接写进简历或面试口述。

  1. 目录约定
/project
  ├─ preload/           # 预加载专属目录
  │   ├─ generate.php   # 白名单生成器
  │   └─ preload.php    # 真正的 preload 脚本
  ├─ app/               # 业务代码
  ├─ vendor/
  └─ composer.json
  1. 白名单生成器 preload/generate.php
<?php
declare(strict_types=1);
// 只扫描 composer 的 class-map,保证不遗漏框架核心
$classMap = require __DIR__ . '/../vendor/composer/autoload_classmap.php';
$whitelist = [];
foreach ($classMap as $class => $file) {
    // 过滤测试类、命令行类、废弃类
    if (strpos($class, 'Tests\\') !== false) continue;
    if (strpos($class, 'Command\\') !== false) continue;
    if (!is_file($file)) continue;
    $whitelist[] = $file;
}
// 追加高频业务目录
$iterator = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator(realpath(__DIR__ . '/../app/Models'))
);
foreach ($iterator as $file) {
    if ($file->isFile() && $file->getExtension() === 'php') {
        $whitelist[] = $file->getRealPath();
    }
}
file_put_contents(
    __DIR__ . '/whitelist.txt',
    implode(PHP_EOL, array_unique($whitelist))
);
echo "生成白名单 " . count($whitelist) . " 个文件\n";
  1. preload 脚本 preload/preload.php
<?php
declare(strict_types=1);
// 必须在全局作用域,不能出现 namespace
$files = file(__DIR__ . '/whitelist.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$success = 0;
foreach ($files as $file) {
    if (!is_file($file)) continue;
    // 手动 require,失败立即记录并继续,保证 master 不退出
    try {
        require_once $file;
        $success++;
    } catch (Throwable $e) {
        error_log("[preload] failed: $file " . $e->getMessage());
    }
}
// 把成功数写进日志,方便监控采集
error_log("[preload] success $success files");
  1. php.ini 片段
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=512
opcache.preload=/project/preload/preload.php
opcache.preload_user=www-data   ; 必须与 fpm pool 用户一致
  1. 灰度验证
  • 先在 staging 环境执行 php -d opcache.preload=/project/preload/preload.php -d opcache.enable_cli=1 -r "var_dump(opcache_get_status()['preload']);",确认无 fatal error。
  • 上线单台机器,观察 ps -eo pid,rss,comm | grep fpm 的 RSS 列,相比未开启机器下降 80 MB(约 30%)。
  • 压测 2w QPS,p99 延迟从 42 ms 降到 29 ms,opcache hit rate 保持 99.2%。
  1. 回滚预案
  • 注释掉 php.ini 中的 opcache.preload 行,执行 systemctl reload php-fpm 即可秒级回滚,无需代码回退。
  • 上线脚本中增加 php-fpm -t 语法检测,防止配置错误导致 master 起不来。
  1. 内存节省量化
  • 未开启:单 worker 平均 RSS 280 MB,峰值 320 MB。
  • 开启后:单 worker 平均 RSS 200 MB,峰值 230 MB。
  • 32 核机器开 64 个 worker,节省约 (280-200)*64 ≈ 5 GB,可腾出 1/8 内存给 Redis 缓存,降低 3% 机器预算。

拓展思考

  1. 动态 vs 静态:业务代码每日上线,白名单文件变化后,如何“热更新”预加载?目前 PHP 内核不支持卸载已 preload 的类,只能走“平滑重启 fpm”或“双池滚动”方案。可进一步调研 FrankenPHPSwoole 的热重载能力。
  2. 与 JIT 叠加:PHP 8.0+ 引入 JIT,preload 与 JIT 同时开启时,JIT 会把预加载的类直接编译成机器码,CPU 敏感型接口收益更大。需要调整 opcache.jit_buffer_size=128M 并重新压测,防止 JIT 占用过多共享内存。
  3. 私有云混部:在 Kubernetes 场景下,把 preload 脚本做成 InitContainer,提前生成白名单并写进 emptyDir,主容器挂载同一 emptyDir,保证镜像重启后仍能复用预加载列表,避免每次拉新镜像都要重新扫描。
  4. 安全边界:preload 阶段如果加载了包含 __destructregister_shutdown_function 的恶意文件,会在 master 中留下后门。上线前必须对白名单文件做 php -l 语法检测与 phpcs 安全规则扫描,禁止动态 eval 代码进入预加载范围。
  5. 量化考核:把“内存节省量”与“延迟下降量”写进 OKR,例如“2024 H1 通过 preload+JIT 组合,让订单接口 p99 延迟 < 25 ms,单台机器节省 6 GB 内存,全年减少 100 台 16C32G 实例,降本 120 万元”,面试时可直接量化收益,体现技术价值与业务价值闭环。