Skip to content

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 代码的执行流程通常分为四个阶段:

  1. 词法分析(Lexing):将 PHP 源代码拆分为 Token
  2. 语法分析(Parsing):将 Token 组装为抽象语法树(AST)
  3. 编译(Compilation):将 AST 编译为操作码(Opcode)
  4. 执行(Execution):Zend 引擎执行操作码

在没有 OPcache 的情况下,这四个阶段在每次请求时都会重复执行。OPcache 通过缓存编译后的操作码,将后续请求的执行流程缩短为直接执行阶段。

共享内存模型

OPcache 使用操作系统的共享内存机制(如 /dev/shm、System V Shared Memory、POSIX MMAP)来存储缓存的操作码,使得所有 PHP-FPM worker 进程都能共享同一份缓存。

安装与启用

验证 OPcache 状态

bash
# 查看 OPcache 是否已加载
php -m | grep -i opcache

# 查看 OPcache 配置
php -i | grep -i opcache

# 查看详细配置
php --ini | grep opcache

编译安装(如未启用)

bash
# PHP 源码编译时启用 OPcache
./configure --enable-opcache --enable-opcache-jit ...
make && make install

详细配置参数

核心开关

opcache.enable

ini
; 启用 OPcache(CLI 模式下默认关闭)
opcache.enable = 1
取值说明
0禁用 OPcache
1启用 OPcache(默认值)

作用域

PHP_INI_ALL,可在 php.ini.htaccessini_set() 中设置。但在 php.ini 中设置最为推荐。

opcache.enable_cli

ini
; CLI 模式下启用 OPcache
opcache.enable_cli = 1
取值说明
0CLI 模式下禁用(默认值)
1CLI 模式下启用

CLI 模式注意事项

默认情况下,CLI 模式的 OPcache 处于禁用状态。因为 CLI 脚本通常只执行一次,缓存意义不大。但对于长期运行的 CLI 守护进程(如队列 worker),启用此选项是有益的。

opcache.enable_dl

ini
; 允许动态加载扩展(建议关闭)
opcache.enable_dl = 0

禁用 dl() 函数可以提升安全性,防止动态加载恶意扩展。

内存管理

opcache.memory_consumption

ini
; 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

ini
; 内部字符串缓冲区大小(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

ini
; 最大缓存文件数量
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

ini
; 操作码磁盘缓存目录
opcache.file_cache = /tmp/opcache

; 仅缓存到磁盘不加载到内存
; opcache.file_cache_only = 1

; 优化文件缓存(包括 OPcache 优化过的操作码)
; opcache.file_cache_consistency_checks = 1

当共享内存不足时,可将操作码缓存到磁盘。磁盘缓存重启 PHP-FPM 后依然有效,加速首次请求。

php
<?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

ini
; 验证脚本时间戳(开发环境开启,生产环境关闭)
opcache.validate_timestamps = 0
取值环境说明
1开发环境每次请求检查 PHP 文件是否被修改(默认值)
0生产环境不检查文件修改,性能最优

生产环境务必关闭

生产环境中应将此值设为 0。每次请求都检查文件时间戳会增加额外的 stat() 系统调用开销,在高并发场景下影响显著。

opcache.revalidate_freq

ini
; 时间戳验证频率(秒)
opcache.revalidate_freq = 0

validate_timestamps = 1 时,此参数控制验证频率。0 表示每次请求都验证,60 表示每 60 秒验证一次。

推荐配置

生产环境推荐:validate_timestamps = 0(不需要此参数)。 开发环境推荐:validate_timestamps = 1 + revalidate_freq = 0

代码优化

opcache.optimization_level

ini
; 优化级别(位掩码)
opcache.optimization_level = 0x7FFFBFFF

这是一个位掩码,控制 OPcache 执行的优化级别:

优化项说明
0x01pass1常量折叠、简单优化
0x02pass2基本块合并
0x04pass3CFG 优化
0x08pass4DFA 优化
0x10pass5类型推断
0x20pass6调用图优化
0x40pass7内联优化
0x80pass8闭包优化
0x100pass9值编号
0x200pass10SSA 优化
0x400pass11跳转优化
0x800pass12DCE(死代码消除)
0x1000pass13常量传播
0x2000pass14调用图扩展
0x4000pass15跳转线程优化
0x8000passN其他优化

默认值

0x7FFFBFFF 是默认值,启用了几乎所有优化。通常不需要修改此值,除非遇到优化导致的问题。

opcache.opt_debug_level

ini
; 优化调试级别
opcache.opt_debug_level = 0
取值说明
0不输出调试信息(默认值)
0x10000输出优化前后的操作码对比

调试用途

此选项仅用于调试 OPcache 优化行为,不应在生产环境中启用。

注释与文档

opcache.save_comments

ini
; 保留注释
opcache.save_comments = 1
取值说明
1保留代码注释(默认值)
0移除所有注释

不要关闭

许多框架和工具依赖 PHP 文档注释(DocBlocks)来进行反射、生成 API 文档、依赖注入等。关闭此选项会导致 ReflectionClassDoctrine 注解等无法正常工作。

opcache.load_comments

ini
; 加载注释(PHP 8.0 已移除)
; opcache.load_comments = 1

PHP 8.0 起此选项已被移除,注释总是会被加载。

快速关闭与黑名单

opcache.fast_shutdown

ini
; 快速关闭
opcache.fast_shutdown = 1

启用后,PHP 关闭阶段会释放 OPcache 的句柄以加速请求结束。此选项在 PHP 7.0 之后默认启用。

opcache.blacklist_filename

ini
; 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.php

JIT 相关配置

opcache.jit

ini
; JIT 模式配置(PHP 8.0+)
opcache.jit = tracing

JIT 编译器配置,详见 JIT 编译器 章节。

opcache.jit_buffer_size

ini
; JIT 缓冲区大小
opcache.jit_buffer_size = 256M

关联

JIT 配置的完整说明请参考 JIT 编译器 页面。

实战示例

生产环境推荐配置

ini
; /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

开发环境推荐配置

ini
; /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
<?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
<?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
<?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
<?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 加载:

ini
; 正确的加载方式
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_restartshash_restarts 指标

3. 部署流程集成

bash
# 部署脚本示例
#!/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.php

4. 监控告警

建议将以下指标接入监控系统:

  • opcache_hit_rate:命中率低于 95% 应告警
  • oom_restarts:大于 0 应告警
  • hash_restarts:大于 0 应告警
  • 内存使用率:超过 80% 应告警

下一节

继续学习:OPcache 优化策略

参考链接