OPcache 配置详解
概述
OPcache(Opcode Cache)是 PHP 内置的字节码缓存扩展,通过将 PHP 脚本编译为操作码(Opcode)并缓存到共享内存中,避免每次请求都重新解析和编译 PHP 文件。合理配置 OPcache 是提升 PHP 应用性能最直接、最有效的手段之一,通常可带来 3-10 倍的请求处理速度提升。
PHP 版本要求
OPcache 自 PHP 5.5 起内置。PHP 8.0 起 JIT 编译器也集成在 OPcache 扩展中。本文基于 PHP 8.1+ 编写。
基础概念
操作码缓存原理
PHP 代码的执行流程通常分为四个阶段:
- 词法分析(Lexing):将 PHP 源代码拆分为 Token
- 语法分析(Parsing):将 Token 组装为抽象语法树(AST)
- 编译(Compilation):将 AST 编译为操作码(Opcode)
- 执行(Execution):Zend 引擎执行操作码
在没有 OPcache 的情况下,这四个阶段在每次请求时都会重复执行。OPcache 通过缓存编译后的操作码,将后续请求的执行流程缩短为直接执行阶段。
共享内存模型
OPcache 使用操作系统的共享内存机制(如 /dev/shm、System V Shared Memory、POSIX MMAP)来存储缓存的操作码,使得所有 PHP-FPM worker 进程都能共享同一份缓存。
安装与启用
验证 OPcache 状态
# 查看 OPcache 是否已加载
php -m | grep -i opcache
# 查看 OPcache 配置
php -i | grep -i opcache
# 查看详细配置
php --ini | grep opcache编译安装(如未启用)
# PHP 源码编译时启用 OPcache
./configure --enable-opcache --enable-opcache-jit ...
make && make install详细配置参数
核心开关
opcache.enable
; 启用 OPcache(CLI 模式下默认关闭)
opcache.enable = 1| 取值 | 说明 |
|---|---|
0 | 禁用 OPcache |
1 | 启用 OPcache(默认值) |
作用域
PHP_INI_ALL,可在 php.ini、.htaccess、ini_set() 中设置。但在 php.ini 中设置最为推荐。
opcache.enable_cli
; CLI 模式下启用 OPcache
opcache.enable_cli = 1| 取值 | 说明 |
|---|---|
0 | CLI 模式下禁用(默认值) |
1 | CLI 模式下启用 |
CLI 模式注意事项
默认情况下,CLI 模式的 OPcache 处于禁用状态。因为 CLI 脚本通常只执行一次,缓存意义不大。但对于长期运行的 CLI 守护进程(如队列 worker),启用此选项是有益的。
opcache.enable_dl
; 允许动态加载扩展(建议关闭)
opcache.enable_dl = 0禁用 dl() 函数可以提升安全性,防止动态加载恶意扩展。
内存管理
opcache.memory_consumption
; OPcache 共享内存大小(MB)
opcache.memory_consumption = 256控制 OPcache 用于存储操作码的共享内存总量。该值应根据项目规模调整:
| 项目规模 | 建议值 |
|---|---|
| 小型项目(< 50 个文件) | 64 - 128 |
| 中型项目(50-500 个文件) | 128 - 256 |
| 大型项目(> 500 个文件) | 256 - 512 |
| 超大型项目(如 Laravel + 大量 Vendor) | 512 - 1024 |
计算公式
一般规则:总内存 ≈ 项目所有 PHP 文件编译后操作码大小 × 1.5。可通过 opcache_get_status()['memory_usage'] 查看实际使用量来确定最优值。
opcache.interned_strings_buffer
; 内部字符串缓冲区大小(MB)
opcache.interned_strings_buffer = 16用于存储内部化字符串(Interned Strings)的共享内存大小。内部化字符串包括 PHP 脚本中的字面量字符串、类名、方法名、变量名等。
| 项目规模 | 建议值 |
|---|---|
| 小型项目 | 4 - 8 |
| 中型项目 | 8 - 16 |
| 大型项目 | 16 - 32 |
PHP 8.0+ 变化
PHP 8.0 起,内部字符串缓冲区被所有 PHP 进程共享(之前每个进程独立),因此通常只需较小的值即可满足需求。
缓存容量
opcache.max_accelerated_files
; 最大缓存文件数量
opcache.max_accelerated_files = 20000控制 OPcache 可以缓存的 PHP 文件数量上限。这是一个哈希表大小的近似值,实际值会向上取最近的质数。
| 项目规模 | 建议值 |
|---|---|
| 小型项目 | 4000 |
| 中型项目 | 8000 - 20000 |
| 大型项目(含 Vendor) | 20000 - 40000 |
| 超大型项目(Monorepo) | 40000 - 80000 |
低于实际文件数量
如果此值设置过小,超出部分的 PHP 文件将无法被缓存,导致性能下降。可以通过 opcache_get_status()['opcache_statistics']['num_cached_scripts'] 查看实际缓存文件数。
文件缓存(PHP 7.0+)
opcache.file_cache
; 操作码磁盘缓存目录
opcache.file_cache = /tmp/opcache
; 仅缓存到磁盘不加载到内存
; opcache.file_cache_only = 1
; 优化文件缓存(包括 OPcache 优化过的操作码)
; opcache.file_cache_consistency_checks = 1当共享内存不足时,可将操作码缓存到磁盘。磁盘缓存重启 PHP-FPM 后依然有效,加速首次请求。
<?php
declare(strict_types=1);
// 检查文件缓存是否可用
$status = opcache_get_status(false);
if (isset($status['file_cache_full'])) {
echo "文件缓存已满: " . ($status['file_cache_full'] ? '是' : '否') . PHP_EOL;
}时间戳验证
opcache.validate_timestamps
; 验证脚本时间戳(开发环境开启,生产环境关闭)
opcache.validate_timestamps = 0| 取值 | 环境 | 说明 |
|---|---|---|
1 | 开发环境 | 每次请求检查 PHP 文件是否被修改(默认值) |
0 | 生产环境 | 不检查文件修改,性能最优 |
生产环境务必关闭
生产环境中应将此值设为 0。每次请求都检查文件时间戳会增加额外的 stat() 系统调用开销,在高并发场景下影响显著。
opcache.revalidate_freq
; 时间戳验证频率(秒)
opcache.revalidate_freq = 0当 validate_timestamps = 1 时,此参数控制验证频率。0 表示每次请求都验证,60 表示每 60 秒验证一次。
推荐配置
生产环境推荐:validate_timestamps = 0(不需要此参数)。 开发环境推荐:validate_timestamps = 1 + revalidate_freq = 0。
代码优化
opcache.optimization_level
; 优化级别(位掩码)
opcache.optimization_level = 0x7FFFBFFF这是一个位掩码,控制 OPcache 执行的优化级别:
| 位 | 优化项 | 说明 |
|---|---|---|
0x01 | pass1 | 常量折叠、简单优化 |
0x02 | pass2 | 基本块合并 |
0x04 | pass3 | CFG 优化 |
0x08 | pass4 | DFA 优化 |
0x10 | pass5 | 类型推断 |
0x20 | pass6 | 调用图优化 |
0x40 | pass7 | 内联优化 |
0x80 | pass8 | 闭包优化 |
0x100 | pass9 | 值编号 |
0x200 | pass10 | SSA 优化 |
0x400 | pass11 | 跳转优化 |
0x800 | pass12 | DCE(死代码消除) |
0x1000 | pass13 | 常量传播 |
0x2000 | pass14 | 调用图扩展 |
0x4000 | pass15 | 跳转线程优化 |
0x8000 | passN | 其他优化 |
默认值
0x7FFFBFFF 是默认值,启用了几乎所有优化。通常不需要修改此值,除非遇到优化导致的问题。
opcache.opt_debug_level
; 优化调试级别
opcache.opt_debug_level = 0| 取值 | 说明 |
|---|---|
0 | 不输出调试信息(默认值) |
0x10000 | 输出优化前后的操作码对比 |
调试用途
此选项仅用于调试 OPcache 优化行为,不应在生产环境中启用。
注释与文档
opcache.save_comments
; 保留注释
opcache.save_comments = 1| 取值 | 说明 |
|---|---|
1 | 保留代码注释(默认值) |
0 | 移除所有注释 |
不要关闭
许多框架和工具依赖 PHP 文档注释(DocBlocks)来进行反射、生成 API 文档、依赖注入等。关闭此选项会导致 ReflectionClass、Doctrine 注解等无法正常工作。
opcache.load_comments
; 加载注释(PHP 8.0 已移除)
; opcache.load_comments = 1PHP 8.0 起此选项已被移除,注释总是会被加载。
快速关闭与黑名单
opcache.fast_shutdown
; 快速关闭
opcache.fast_shutdown = 1启用后,PHP 关闭阶段会释放 OPcache 的句柄以加速请求结束。此选项在 PHP 7.0 之后默认启用。
opcache.blacklist_filename
; OPcache 黑名单文件
opcache.blacklist_filename = /etc/php/opcache-blacklist.txt黑名单文件中指定不缓存的 PHP 文件路径(支持通配符):
# opcache-blacklist.txt
# 不缓存特定文件
/var/www/html/debug.php
# 不缓存特定目录
/var/www/html/dev/*
/var/www/html/tmp/*
# 不缓存特定扩展名的文件
# *.inc.phpJIT 相关配置
opcache.jit
; JIT 模式配置(PHP 8.0+)
opcache.jit = tracingJIT 编译器配置,详见 JIT 编译器 章节。
opcache.jit_buffer_size
; JIT 缓冲区大小
opcache.jit_buffer_size = 256M关联
JIT 配置的完整说明请参考 JIT 编译器 页面。
实战示例
生产环境推荐配置
; /etc/php/8.1/fpm/conf.d/opcache-recommended.ini
[OPcache]
; 基本开关
opcache.enable = 1
opcache.enable_cli = 0
opcache.enable_dl = 0
; 内存配置(根据项目调整)
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
; 生产环境关闭时间戳验证
opcache.validate_timestamps = 0
opcache.revalidate_freq = 0
; 优化配置
opcache.optimization_level = 0x7FFFBFFF
opcache.save_comments = 1
opcache.fast_shutdown = 1
; 保存操作码到磁盘(可选,加速重启后的首次请求)
opcache.file_cache = /var/cache/php/opcache
opcache.file_cache_consistency_checks = 1
; JIT 配置
opcache.jit = tracing
opcache.jit_buffer_size = 256M
; 黑名单
opcache.blacklist_filename = /etc/php/opcache-blacklist.txt开发环境推荐配置
; /etc/php/8.1/fpm/conf.d/opcache-dev.ini
[OPcache]
; 基本开关
opcache.enable = 1
opcache.enable_cli = 1
opcache.enable_dl = 0
; 较小的内存配置
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 10000
; 开发环境开启时间戳验证
opcache.validate_timestamps = 1
opcache.revalidate_freq = 0
; 保留注释(开发环境必须)
opcache.save_comments = 1
; 禁用 JIT(避免干扰调试)
opcache.jit = off
opcache.jit_buffer_size = 64M监控脚本
<?php
declare(strict_types=1);
/**
* OPcache 状态监控脚本
*/
class OpcacheMonitor
{
public static function report(): void
{
$status = opcache_get_status(false);
if ($status === false) {
echo "OPcache 未启用" . PHP_EOL;
return;
}
echo "=== OPcache 状态报告 ===" . PHP_EOL;
echo PHP_EOL;
// 内存使用情况
echo "--- 内存使用 ---" . PHP_EOL;
$mem = $status['memory_usage'];
$usedMB = round($mem['used_memory'] / 1024 / 1024, 2);
$totalMB = round($mem['used_memory'] + $mem['free_memory'], 2) / 1024 / 1024;
echo "已用内存: {$usedMB}MB / {$totalMB}MB" . PHP_EOL;
echo "内存命中率: " . self::formatPercent(
$mem['used_memory'],
$mem['used_memory'] + $mem['free_memory']
) . PHP_EOL;
if (isset($mem['wasted_memory']) && $mem['wasted_memory'] > 0) {
$wastedMB = round($mem['wasted_memory'] / 1024 / 1024, 2);
echo "浪费内存: {$wastedMB}MB" . PHP_EOL;
}
echo PHP_EOL;
// 缓存统计
echo "--- 缓存统计 ---" . PHP_EOL;
$stats = $status['opcache_statistics'];
echo "已缓存脚本数: {$stats['num_cached_scripts']}" . PHP_EOL;
echo "缓存命中次数: {$stats['hits']}" . PHP_EOL;
echo "缓存未命中次数: {$stats['misses']}" . PHP_EOL;
echo "命中率: " . self::formatPercent($stats['hits'], $stats['opcache_hit_rate']) . PHP_EOL;
// 缓存的键数量
echo "已缓存键数: {$stats['num_cached_keys']}" . PHP_EOL;
echo "最大缓存键数: {$stats['max_cached_keys']}" . PHP_EOL;
if ($stats['oom_restarts'] > 0) {
echo PHP_EOL;
echo "::: warning" . PHP_EOL;
echo "内存溢出重启次数: {$stats['oom_restarts']}" . PHP_EOL;
echo "建议增加 opcache.memory_consumption" . PHP_EOL;
echo ":::" . PHP_EOL;
}
if ($stats['hash_restarts'] > 0) {
echo PHP_EOL;
echo "::: warning" . PHP_EOL;
echo "哈希表溢出重启次数: {$stats['hash_restarts']}" . PHP_EOL;
echo "建议增加 opcache.max_accelerated_files" . PHP_EOL;
echo ":::" . PHP_EOL;
}
}
private static function formatPercent(int|float $numerator, int|float $denominator): string
{
if ($denominator === 0.0) {
return '0%';
}
return round(($numerator / $denominator) * 100, 2) . '%';
}
}
// CLI 模式下运行
OpcacheMonitor::report();动态配置查询
<?php
declare(strict_types=1);
/**
* 获取当前 OPcache 所有配置
*/
function getOpcacheConfig(): array
{
$directives = [
'opcache.enable',
'opcache.enable_cli',
'opcache.enable_dl',
'opcache.memory_consumption',
'opcache.interned_strings_buffer',
'opcache.max_accelerated_files',
'opcache.validate_timestamps',
'opcache.revalidate_freq',
'opcache.optimization_level',
'opcache.save_comments',
'opcache.fast_shutdown',
'opcache.jit',
'opcache.jit_buffer_size',
];
$config = [];
foreach ($directives as $directive) {
$value = ini_get($directive);
$config[$directive] = $value;
}
return $config;
}
// 打印配置表格
$config = getOpcacheConfig();
echo str_pad('配置项', 40) . str_pad('值', 20) . PHP_EOL;
echo str_repeat('-', 60) . PHP_EOL;
foreach ($config as $key => $value) {
echo str_pad($key, 40) . str_pad((string) $value, 20) . PHP_EOL;
}缓存预热脚本
<?php
declare(strict_types=1);
/**
* OPcache 缓存预热脚本
* 部署新代码后运行此脚本,避免首次请求性能下降
*/
class OpcacheWarmer
{
private string $projectRoot;
public function __construct(string $projectRoot)
{
$this->projectRoot = rtrim($projectRoot, '/');
}
/**
* 递归扫描并预热 PHP 文件
*/
public function warmup(string $directory = '', array $excludeDirs = []): array
{
$targetDir = $this->projectRoot . ($directory ? '/' . $directory : '');
$results = ['cached' => 0, 'failed' => 0, 'skipped' => 0];
if (!is_dir($targetDir)) {
echo "目录不存在: {$targetDir}" . PHP_EOL;
return $results;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$targetDir,
RecursiveDirectoryIterator::SKIP_DOTS
),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $file) {
/** @var SplFileInfo $file */
if (!$file->isFile() || $file->getExtension() !== 'php') {
continue;
}
$relativePath = substr($file->getPathname(), strlen($this->projectRoot) + 1);
// 跳过排除目录
$skip = false;
foreach ($excludeDirs as $excludeDir) {
if (str_starts_with($relativePath, $excludeDir)) {
$skip = true;
break;
}
}
if ($skip) {
$results['skipped']++;
continue;
}
// 通过 opcache_compile_file 预热
if (opcache_compile_file($file->getPathname())) {
$results['cached']++;
echo "[OK] {$relativePath}" . PHP_EOL;
} else {
$results['failed']++;
echo "[FAIL] {$relativePath}" . PHP_EOL;
}
}
return $results;
}
}
// 使用示例
$warmer = new OpcacheWarmer('/var/www/html/myapp');
$results = $warmer->warmup('', ['vendor/', 'tests/', 'node_modules/']);
echo PHP_EOL . "=== 预热完成 ===" . PHP_EOL;
echo "缓存成功: {$results['cached']}" . PHP_EOL;
echo "缓存失败: {$results['failed']}" . PHP_EOL;
echo "跳过文件: {$results['skipped']}" . PHP_EOL;注意事项
配置冲突排查
<?php
declare(strict_types=1);
/**
* OPcache 配置问题诊断
*/
function diagnoseOpcache(): void
{
$issues = [];
// 检查 OPcache 是否启用
if (!function_exists('opcache_get_status')) {
echo "OPcache 扩展未加载!" . PHP_EOL;
echo "请确保在 php.ini 中添加: zend_extension=opcache.so" . PHP_EOL;
return;
}
// 检查 validate_timestamps 在生产环境
if (ini_get('opcache.validate_timestamps') === '1') {
$issues[] = 'validate_timestamps 已启用,在生产环境中应关闭以获得最佳性能';
}
// 检查内存使用
$status = opcache_get_status(false);
if ($status) {
$stats = $status['opcache_statistics'];
$mem = $status['memory_usage'];
// 内存不足
$usedPercent = $mem['used_memory'] / ($mem['used_memory'] + $mem['free_memory']);
if ($usedPercent > 0.9) {
$issues[] = sprintf(
'内存使用率 %.1f%%,建议增加 opcache.memory_consumption(当前: %s)',
$usedPercent * 100,
ini_get('opcache.memory_consumption')
);
}
// 哈希表不足
if ($stats['num_cached_scripts'] > $stats['max_cached_keys'] * 0.9) {
$issues[] = sprintf(
'已缓存脚本数接近上限 (%d/%d),建议增加 opcache.max_accelerated_files',
$stats['num_cached_scripts'],
$stats['max_cached_keys']
);
}
// OOM 重启
if ($stats['oom_restarts'] > 0) {
$issues[] = "内存溢出重启 {$stats['oom_restarts']} 次,必须增加 opcache.memory_consumption";
}
}
// 输出结果
if (empty($issues)) {
echo "OPcache 配置正常" . PHP_EOL;
} else {
echo "发现以下问题:" . PHP_EOL;
foreach ($issues as $i => $issue) {
echo " " . ($i + 1) . ". {$issue}" . PHP_EOL;
}
}
}
diagnoseOpcache();INI 文件加载顺序
OPcache 作为 Zend 扩展,需要在 php.ini 中使用 zend_extension 加载:
; 正确的加载方式
zend_extension=opcache.so
; 错误的加载方式(不会工作)
; extension=opcache.so加载顺序
zend_extension 必须在 extension 指令之前加载。如果有多个 Zend 扩展(如 Xdebug),加载顺序很重要。通常 OPcache 应该在其他 Zend 扩展之前加载。
最佳实践
1. 分环境配置
- 生产环境:关闭
validate_timestamps,增大内存,启用文件缓存 - 开发环境:开启
validate_timestamps,保留注释,禁用 JIT - 测试环境:参考开发环境配置,但可启用 JIT 进行性能测试
2. 内存配置建议
- 通过监控脚本确定实际需求
- 预留 20-30% 的缓冲空间
- 定期检查
oom_restarts和hash_restarts指标
3. 部署流程集成
# 部署脚本示例
#!/bin/bash
# 1. 部署新代码
rsync -avz ./src/ /var/www/html/myapp/
# 2. 重启 PHP-FPM 以刷新 OPcache
systemctl reload php8.1-fpm
# 3. 预热缓存(如果关闭了 validate_timestamps)
php /var/www/html/scripts/warmup.php
# 4. 验证缓存状态
php /var/www/html/scripts/opcache-monitor.php4. 监控告警
建议将以下指标接入监控系统:
opcache_hit_rate:命中率低于 95% 应告警oom_restarts:大于 0 应告警hash_restarts:大于 0 应告警- 内存使用率:超过 80% 应告警
下一节
继续学习:OPcache 优化策略