Setup: PHP 8.2.13, php-fpm 8.2.13, Debian 12, 32 vCPU / 128 GB, opcache with opcache.memory_consumption=512, opcache.max_accelerated_files=65536, opcache.validate_timestamps=0. We added opcache.file_cache=/var/cache/php-opcache and opcache.file_cache_only=0 to survive fpm restarts without a cold compile. Traffic is ~1.2k req/s across 48 pool workers, ~9k PHP files in the include graph. Right after the change, p99 went from 180ms to 2.1s for about 40 seconds following every deploy, and only on the workers that restarted first.
The cause was not the file cache being slow. It was that every worker restart now reads the file cache from disk, and the first request to a not-yet-cached script blocks on stat + read of the cache file while holding the pool slot. With 48 workers restarting in a rolling window, the cache directory saw a thundering herd of small reads. strace -f -e trace=openat,read -p <pid> showed repeated 4–8 KB reads on the same inode from multiple workers, and iostat -x 1 showed r_await on that disk climb to 30ms.
What worked: warm the file cache before fpm starts, and serialize the warm-up so workers do not race. A small CLI script that includes every file once is enough:
php -d opcache.enable_cli=1 \
-d opcache.file_cache=/var/cache/php-opcache \
-d opcache.file_cache_only=1 \
/usr/local/bin/warm-opcache.php
warm-opcache.php walks the Composer autoload classmap plus the app’s controller directory and calls opcache_compile_file($path) for each path, then checks opcache_get_status(true)['opcache_statistics']['num_cached_scripts'] against the file count and exits non-zero on mismatch. In the systemd unit, ExecStartPre=/usr/local/bin/warm-opcache.php runs before the fpm master. On deploy we run the same script, then kill -USR2 the master so the new workers inherit a warm file cache.
Why this shape: opcache.file_cache is a fallback, not a shared memory layer. Each worker still holds its own in-memory opcache; the file cache only helps after a restart. So the cost is paid at restart time, and the fix has to move that cost out of the request path. Warming in CLI is cheap because opcache.file_cache_only=1 skips shared memory and writes straight to the directory. The trade-off is that you must re-run it on every deploy, and the file cache is keyed by the PHP build, so a minor-version bump invalidates everything and you are back to a cold start.
Where it breaks: if your include graph is generated at runtime (dynamic require from a router cache, or eval-ed templates), the warm script cannot see those files, and the first request still pays. On network storage the pre-warm is worse than useless because the reads are still slow; keep /var/cache/php-opcache on local disk. Also opcache_compile_file() does not execute the file, so files with top-level side effects (legacy require-time constants, define() in an included config) will not be fully exercised; those still compile on first real request. We hit this with a config file that called define() at include time and silently warmed nothing useful.
One thing I have not measured: whether the same pre-warm helps when opcache.file_cache_only=1 is used in production for read-only containers. If you run that, what does your cold-start p99 look like?


0 条评论
还没有评论,写下第一条。