《让PHP扩展开拓编程前路》 之 性能优化利器 OPcache

PECL - OPcache

  • Title: 《让PHP扩展开拓编程前路》 之 性能优化利器 OPcache
  • Title-En: let-php-extension-broaden-the-programming-horizon_OPcache-and-JIT
  • Tag: PECLPHPOPcache性能优化JIT
  • Author: Tacks
  • Create-Date: 2023-10-31
  • Update-Date: 2023-10-31

PHP-JIT

Ref

Prepare

  • 扩展
// 查看 PHP 配置文件 php.ini 文件
[root@Centos7 ~]# php --ini | grep php.ini

// 查看 Opcache 扩展
[root@Centos7 ~]# php -m | grep OPcache
Zend OPcache
Zend OPcache

// 查看扩展配置
[root@Centos7 ~]# php --ri "Zend OPcache"

Zend OPcache

Opcode Caching => Disabled
Optimization => Disabled
SHM Cache => Enabled
File Cache => Disabled
Startup Failed => Opcode Caching is disabled for CLI

Directive => Local Value => Master Value
opcache.enable => On => On
opcache.use_cwd => On => On
opcache.validate_timestamps => On => On
opcache.validate_permission => Off => Off
opcache.validate_root => Off => Off
opcache.dups_fix => Off => Off
opcache.revalidate_path => Off => Off
opcache.log_verbosity_level => 1 => 1
opcache.memory_consumption => 64 => 64
opcache.interned_strings_buffer => 16 => 16
opcache.max_accelerated_files => 4000 => 4000
opcache.max_wasted_percentage => 5 => 5
opcache.consistency_checks => 0 => 0
opcache.force_restart_timeout => 180 => 180
opcache.revalidate_freq => 2 => 2
opcache.file_update_protection => 2 => 2
opcache.preferred_memory_model => no value => no value
opcache.blacklist_filename => no value => no value
opcache.max_file_size => 0 => 0
opcache.protect_memory => Off => Off
opcache.save_comments => On => On
opcache.optimization_level => 0x7FFEBFFF => 0x7FFEBFFF
opcache.opt_debug_level => 0 => 0
opcache.enable_file_override => Off => Off
opcache.enable_cli => Off => Off
opcache.error_log => no value => no value
opcache.restrict_api => no value => no value
opcache.lockfile_path => /tmp => /tmp
opcache.file_cache => no value => no value
opcache.file_cache_only => Off => Off
opcache.file_cache_consistency_checks => On => On
opcache.huge_code_pages => Off => Off

1、PHP 主流两种运行模式

1.1 PHP-FPM + Nginx

执行函数 php_sapi_name() 将返回 fpm-fcgi

1.1.1 CGI 通用网关接口协议 (Common Gateway Interface)

一种标准接口规范,它定义了 Web 浏览器和服务器之间的数据传输格式,如Header、URL、Get Query、POST等,使得 Web 服务器可以调用外部程序处理用户请求,并将处理结果返回给客户端浏览器。

在原始 CGI 模式下,每个请求都会启动一个新的进程,也就是 fork-and-execute 模式来处理,这对服务器的性能造成了较大的负担,导致响应时间变慢,难以处理大量请求。

如果是 PHP 程序每次还需要加载 php.ini ,每次都需要启动进程才能处理一个请求,确实不太合适,于是有了 FastCGI 协议的解决方案。

  • 优点
    • 灵活度高:CGI 可以执行任何可执行文件,可以与多种编程语言集成;
    • 可移植性高:不同 WEB 服务器和编程语言按照 CGI 规范就可以进行交互;
    • 安全性高: 每个 CGI 程序都是一个独立的请求,互相独立;
  • 缺点
    • 性能较低:每次请求都需要进程的创建和销毁,耗费系统资源;
    • 初始化:每次都需要加载 php.ini

1.1.2 FastCGI 协议 (Fast Common Gateway Interface)

一种高性能的 CGI 协议,和语言无关,类似常驻 Long Live 类型的 CGI。

它实现了长连接将CGI解释器进程保持在内存中,不需要每次都花时间 fork 一次,以减少进程的创建和销毁,同时还支持并行处理多个请求。

  • 大致过程
    • fastcgi 会先启一个 master,解析配置文件,初始化执行环境,然后再启动多个 worker
    • 当请求过来时,master 会传递给一个 worker,然后立即可以接受下一个请求

1.1.3 PHP-CGI 进程 (PHP Common Gateway Interface)

[root@Centos7 ~]# /php/php73/bin/php-cgi -v
PHP 7.3.20 (cgi-fcgi) (built: Jan 11 2021 17:21:59)
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.20, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.3.20, Copyright (c) 1999-2018, by Zend Technologies

PHP的解释器是 php-cgi , php-cgi 只是个CGI程序,它自己本身只能解析请求,返回结果,不会进程管理。

  • 面临问题
    • php-cgi 变更 php.ini 配置后需重启 php-cgi 才能让新的 php-ini 生效,不可以平滑重启
  • 请求过程
    • HTTP Rquest <-> Web Server 请求分发 <-> PHP-CGI 解释器 、Fork 子进程 <-> PHP 处理
    • php-cgi
      • 初始化 PHP 相关变量
      • 调用初始化 Zend 虚拟机
      • 加载并解释 php.ini
      • 激活 Zend ,加载PHP文件,词法分析、语法分析、编译 opcode 、执行、输出结果、关闭Zend
      • 返回结果

1.1.4 PHP-FPM 进程管理器 (FastCGI Process Manager)

[root@Centos7 ~]#  /php/php73/sbin/php-fpm -v
PHP 7.3.20 (fpm-fcgi) (built: Jan 11 2021 17:21:45)
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.20, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.3.20, Copyright (c) 1999-2018, by Zend Technologies

php-fpm 是 fastcgi 进程管理器 ,管理对象是 php-cgi

php-fpm 是 多进程同步模型

  • 请求过程 Nginx 与 PHP-FPM
    • 数据流转
      • HTTP Rquest <-> Web Server Nginx Location <-> fastcgi Request <-> TCP Socket/Unix Socket <-> PHP-FPM Master <-> PHP-FPM Worker PHP-CGI
    • 启动服务
      • PHP-FPM 启动
        • Master 进程: 端口监听、任务分配、Worker进程管理
        • Worker 进程: PHP-CGI 程序,负责解释执行 PHP脚本
      • Nginx 启动
        • 加载 ngx_http_fastcgi_module 模块
        • 初始化 FastCGI 环境,实现 FastCGI 协议代理
    • 接收请求
      • Nginx 基于 Location 配置 触发到对应后端服务,如 PHP-FPM
    • 请求转发
      • Nginx 将请求翻译成 FastCGI 请求
      • 通过 Socket (TCP Socket/Unix Socket) 发送给 PHP-FPM Master 进程
    • 任务分配
      • PHP-FPM Master 进程接收到请求
      • 分配 Worker 进程执行 PHP 脚本
    • 任务执行
      • PHP-FPM Worker 进程也就是 PHP-CGI,处理PHP脚本
        • PHP 初始化执行环节,启动 Zend 引擎,加载 php.ini ,加载注册的扩展模块
        • 初始化后读取脚本文件,Zend 引擎对脚本文件进行 词法分析(lex),语法分析(bison),生成语法树(Token)
        • Zend 引擎编译语法树,生成 opcode
        • Zend 引擎执行 opcode ,返回执行结果
      • 处理结束,返回结果
    • 结果响应
      • PHP-FPM Worker 进程返回处理结果,关闭连接,等待下一个请求
      • PHP-FPM Master 进程通过 Socket 返回处理结果
      • Nginx 进行响应客户端
  • 解决问题
    • php-cgi 的平滑重启
      • PHP-FPM 对此的处理机制是新的 Worker进程用新的配置,已经存在的 Worker进程处理完手上的活就可以歇着了,通过这种机制来平滑过渡
    • php-cgi 的常驻管理

1.2 CLI 命令行运行 (Command Line Interface)

[root@Centos7 ~]# /php/php73/bin/php -v 
PHP 7.3.20 (cli) (built: Jan 11 2021 17:21:40) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.20, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.3.20, Copyright (c) 1999-2018, by Zend Technologies

执行函数 php_sapi_name() 将返回 cli

  • PHP 初始化执行环节,启动 Zend 引擎,加载 php.ini ,加载注册的扩展模块
  • 初始化后读取脚本文件,Zend 引擎对脚本文件进行 词法分析(lex),语法分析(bison),生成语法树(Token)
  • Zend 引擎编译语法树,生成 opcode
  • Zend 引擎执行 opcode ,返回执行结果

2、PHP 生命周期

2.1 PHP 生命周期-五大阶段

PHP 程序无论是那种模式运行的,基本都要经过 MINIT、RINIT、EXEC、RSHUTDOWN、MSHUTDOWN 五个阶段。

  • MINIT ModuleInit (模块初始化)
    • php_module_startup()
    • 执行:在整个 SAPI 生命周期内,过程只进行一次
    • 操作
      • 注册 php.ini ,进行映射
      • 注册 类、函数、变量 等
      • 初始化 GC 垃圾回收器
      • 启动 Zend 引擎、注册 Zend 核心扩展、Zend 标准常量
  • RINIT RequestInit (请求初始化)
    • php_request_startup()
    • 执行:请求阶段,SAPI 将控制器转交给 PHP
    • 操作
      • 初始化全局变量,例如 $_GET $_POST 类似
      • 开启输出缓冲区
      • 激活 Zend 引擎,初始化执行器
  • EXEC Execute (执行PHP)
  • RSHUTDOWN RequestShutdown (请求关闭)
    • php_request_shutdown()
    • 操作
      • 关闭缓冲区
      • 释放全局变量
      • 资源释放,例如文件打开、数据库连接等
  • MSHUTDOWN ModuleShutdown (模块关闭)
    • php_module_shutdown()
    • 操作
      • flush 输出内容,返回 http 响应结果,关闭 php 执行器
      • 全局资源清理和释放、对各个扩展进行关闭
      • 关闭 Zend 引擎

2.2 PHP 解释编译-四大步骤

即使 php 脚本没有发生变化,通常还是要走下面四步,词法分析、语法分析、解释编译、执行,这就是解释性语言天生的。

  • Lexing (词法分析)
    • 获得 Token
    • 将源代码进行语法检测、关键词识别等,然后分割为多个字符串,解析成一系列词法单元 Token
    • PHP 中有函数 token_get_all() 可以利用 Zend 引擎的语法分析器 解析提供的 code 源码字符得到 Token
  • Parse (语法分析)
    • 获得 AST (Abstract Syntax Tree)
    • 将 Token 转化为易于理解和执行的抽象语法树
  • Compile (解释编译)
    • 获得 OPcache
    • Zend 引擎将 AST 解析成 OPcodes
  • Execute (执行)
    • 获得 结果
    • Zend 引擎接收 OPcodes 进行执行

2.2.1 词法分析 (Lexical Analysis) - Token Generation

将 PHP 代码分解成一个个 token,并进行词法分析。这个过程主要是将输入的代码转化为 token 序列

  • Token 分析

    • 字符
      • 源码中的 字符串,字符,空格,分号,都会原样返回每个源代码中的字符,都会出现在相应的顺序处
    • 数组
      • 标签,操作符,语句,都会被转换成一个包含俩部分的 Array
        • [0] Token ID
          • 在 Zend 内部的该 Token 的对应码,比如,T_ECHO,T_STRING
          • 利用 token_name() 获得 PHP 解析器代号的符号名称
        • [1] 源码中的原来的内容
        • [2] 行号
  • Code

<?php

$source = "<?php echo 1+1;";
$tokens = token_get_all($source, TOKEN_PARSE);
var_dump($tokens);
  • Tokens
// 得到结果
array(7) {
  [0]=>
  array(3) {
    [0]=>
    int(379)
    [1]=>
    string(6) "<?php "
    [2]=>
    int(1)
  }
  [1]=>
  array(3) {
    [0]=>
    int(328)
    [1]=>
    string(4) "echo"
    [2]=>
    int(1)
  }
  [2]=>
  array(3) {
    [0]=>
    int(382)
    [1]=>
    string(1) " "
    [2]=>
    int(1)
  }
  [3]=>
  array(3) {
    [0]=>
    int(317)
    [1]=>
    string(1) "1"
    [2]=>
    int(1)
  }
  [4]=>
  string(1) "+"
  [5]=>
  array(3) {
    [0]=>
    int(317)
    [1]=>
    string(1) "1"
    [2]=>
    int(1)
  }
  [6]=>
  string(1) ";"
}

2.2.2 语法分析 (Parse) - AST Generation

将词汇分析后的 token 序列转化为 AST,通过语法分析来检查代码的语法是否合法,并生成对应的抽象语法树 AST , 对生成的 AST 进行优化,提高程序的效率和性能

  • AST 分析

    • 丢弃 Tokens 中的多于的空格,然后将剩余的 Tokens 转换成一个一个的简单的表达式
    • Stmt_Echo 输出
    • exprs 表达式
      • Expr_BinaryOp_Plus 运算符 加号
      • Scalar_LNumber 左值
      • Scalar_LNumber 右值
  • Code

[root@Centos7 code]# composer require nikic/php-parser
[root@Centos7 code]# cat composer.json 
{
    "require": {
        "nikic/php-parser": "^4.15"
    }
}
[root@Centos7 code]# cat main.php 
<?php
require_once("./vendor/autoload.php");

use PhpParser\Error;
use PhpParser\NodeDumper;
use PhpParser\ParserFactory;

$code = "<?php echo 1+1;";
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
try {
    $ast = $parser->parse($code);
} catch (Error $error) {
    echo "Parse error: {$error->getMessage()}" . PHP_EOL;
    return;
}

$dumper = new NodeDumper;
echo $dumper->dump($ast) . PHP_EOL;
  • AST
[root@Centos7 code]# php main.php 
array(
    0: Stmt_Echo(
        exprs: array(
            0: Expr_BinaryOp_Plus(
                left: Scalar_LNumber(
                    value: 1
                )
                right: Scalar_LNumber(
                    value: 1
                )
            )
        )
    )
)

2.2.3 解释编译 (Compile) - OPcodes Generation

  • OPcodes 分析
    • 遍历 AST,并将所有节点转化成 Opcode 形式
    • 对于每个节点,都有相应的 Opcode 来执行它所对应的操作,例如赋值、函数调用等
    • Opcode OPArray 数组形式
      • opcode 标识
        • 指明了每个op_array的操作类型,比如add , echo
      • result 结果
        • IS_CV 编译变量(Compiled Variable):这个操作数类型表示一个PHP变量:以$something形式在PHP脚本中出现的变量,vld输出中以!0、!1形式出现
        • IS_VAR 供Zend引擎内部使用的变量,它可以被其他的OPCode重用,跟$php_variable很像,只是只能供Zend引擎内部使用,vld输出中以$0、$1、$2形式出现
        • IS_TMP_VAR Zend引擎内部使用的变量,但是不能被其他的OPCode重用,vld输出中以0、1、~2形式出现
        • IS_CONST 一个常量,它们都是只读的,值不可改变,vld中直接以常量值的形式出现
        • IS_UNUSED 这个表示操作数没有被使用
      • op1 操作数1
      • op2 操作数2
      • extended_value 扩展值
    • 例如 echo 1+1;
      • ZEND_ADD ~0 1 1
      • ZEND_ECHO ~0
// OPcode 结构体
// 每个OPCode都会有一个 handler,一个op1,一个op2,以及result
struct _zend_op {
    opcode_handler_t handler;   /* opcode执行时会调用的处理函数,一个C函数 */
    znode_op op1; /* 操作数1 */
    znode_op op2; /* 操作数2 */
    znode_op result; /* 结果 */
    ulong extended_value; /* 额外信息 */
    uint lineno;
    zend_uchar opcode; /* opcode代号 */
    zend_uchar op1_type; /* 操作数1的类型 */
    zend_uchar op2_type; /* 操作数2的类型 */
    zend_uchar result_type; /* 结果类型 */
};
  • Code
    • 安装 vld ,然后在 cli 下执行
// 安装扩展 vld 0.18.0
[root@Centos7 phpext]# git clone https://github.com/derickr/vld.git vld
[root@Centos7 phpext]# cd vld/
[root@Centos7 vld]# ls
[root@Centos7 vld]# /xxx/bin/phpize
Configuring for:
PHP Api Version:         20180731
Zend Module Api No:      20180731
Zend Extension Api No:   320180731
[root@Centos7 vld]# ./configure --with-php-config=/xxx/bin/php-config &&  make && make install
[root@Centos7 vld]# systemctl restart php-fpm
// 查看扩展
[root@Centos7 vld]# php73 -m | grep vld
vld
[root@Centos7 vld]# php73 --ri vld
vld

vld support => enabled

Directive => Local Value => Master Value
vld.active => 0 => 0
vld.skip_prepend => 0 => 0
vld.skip_append => 0 => 0
vld.execute => 1 => 1
vld.verbosity => 1 => 1
vld.format => 0 => 0
vld.col_sep =>      =>     
vld.save_dir => /tmp => /tmp
vld.save_paths => 0 => 0

[root@Centos7 code]# cat demo.php
<?php echo 1+1;;
  • OPcodes
    • vld 扩展
      • vld.active 是否激活vld, 1开启 0关闭
      • vld.execute 是否执行脚本, 1执行 0关闭
      • vld.skip_prepend 是否跳过php.ini配置文件中 auto_prepend_file 指定的文件, 默认为0,即不跳过包含的文件
      • vld.skip_append 是否跳过php.ini配置文件中 auto_append_file 指定的文件 , 默认为0,即不跳过包含的文件
      • vld.verbosity 是否显示更详细的信息,默认为1,其值可以为0,1,2,3
      • vld.format 是否自定义的格式显示,1是 0否
      • vld.col_sep 自定义格式,间隔字符,如 \t
      • vld.save_paths 是否保存路径信息到文件中, 1保存 0关闭
      • vld.save_dir 设置保存路径的参数,默认是/tmp
    • php -dvld.active=1 -dvld.execute=0 demo.php
      • -dvld.active=1 使用 vld
      • -dvld.execute=0 不执行脚本内容
    • 文字
      • filename
        • 执行脚本路径
      • function name
        • 执行函数名称
      • number of ops
        • Opcode 个数,也就是 OPArray 中包含的OPCode的个数
      • compiled vars
        • 脚本中定义的变量
      • branch
        • 分支
      • path
        • 路径
    • 表格
      • line
        • PHP 程序中代码的行号
      • #*
        • PHP 编译为一个 OPArray ,这个 OPArray 中就包含了所有的 Opcode
        • Zend 引擎就是从这个数组中取出 Opcode , 一个接一个地执行的,所以这个序号代表了 Opcode 的执行顺序
        • 编译器并不是把 PHP 程序中的一行转换为 1 个 OPCode,有时候 1行 PHP 代码可能会被编译为多个 Opcode
      • E
        • Entry Points OPcode 入口
      • I
        • I 表示分支入口, 标注’>’的
      • O
        • O 跳出分支的出口,标注’>’的
      • op
        • 真正的 OPcode
      • ext
        • OPcode 结构体中的 extended_value
      • return
        • Opcode 执行后返回的结果
        • 示例程序中,echo 后没有返回值
        • 没有任何代码的PHP文件,它被编译后依然会有一个 RETURN 的 OPcode ,因为这个 OPcode 会告诉 Zend 引擎这个 OPArray 的执行工作可以正常结束了
      • operands
        • 操作数,OPCode 执行时会用到的参数
        • 示例程序中,echo 后面是要输出的 1+1 也就是 2
[root@Centos7 code]# php -dvld.verbosity=0 -dvld.active=1 -dvld.verbosity=3 -dvld.execute=0  demo.php 
Finding entry points
Branch analysis from position: 0
Add 0
Add 1
1 jumps found. (Code = 62) Position 1 = -2
filename:       /root/code/demo.php
function name:  (null)
number of ops:  2
compiled vars:  none
line      #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
    1     0  E >   < 40> ECHO                                                      OP1[  IS_CONST (4) 2 ]
    2     1      > < 62> RETURN                                                    OP1[  IS_CONST (3) 1 ]

branch: #  0; line:     1-    2; sop:     0; eop:     1; out0:  -2
path #1: 0, 



// 尝试保存 OPcode
[root@Centos7 code]# php -dvld.dump_paths=1 -dvld.verbosity=0 -dvld.save_paths=1 -dvld.active=1 demo.php
[root@Centos7 code]# cat /tmp/paths.dot 
digraph {
subgraph cluster_file_0x7fbe4de7c2a0 { label="file /root/code/demo.php";
subgraph cluster_0x7fbe4de7c2a0 {
    label="__main";
    graph [rankdir="LR"];
    node [shape = record];
    "__main_0" [ label = "{ op #0-1 | line 1-2 }" ];
    __main_ENTRY -> __main_0
    __main_0 -> __main_EXIT;
}
}
}

// --- 额外工具 --- 
// 利用 graphviz 工具将dot文件生成图片
[root@Centos7 graphviz]# dot -V
dot - graphviz version 8.0.2~dev.20230409.0338 (20230409.0338)

[root@Centos7 graphviz]# dot -Tpng /tmp/paths.dot > /tmp/paths.png

3、PHP Zend OPcache

PHP Opcache 是一种用于缓存已编译的PHP代码的扩展,以提高 PHP 应用程序的性能 。 原理是 将编译好的操作码 OPcodes 放入共享内存,提供给其他进程访问。

3.1 OPcache 扩展作用

3.1.1 PHP7 下的 OPcache - 缓存 OPcodes (Operand Codes) 中间代码格式

为了避免每次代码都需要经过词法分析、语法分析、解释编译,我们可以利用 PHP 的 OPcache 扩展缓存 OPcodes 来加快速度。

  • code
    • OPcache
    • Zend Execute
    • CPU
  • Lexing
  • Parse
  • Compile
  • Zend Execute
  • CPU

PHP OPcache 流程图

PHP-OPcache

当 PHP 解释器启动时,OPcache 扩展会被初始化。在这个过程中,OPcache 会分配一块内存用来缓存编译后的源码。当一个 PHP 脚本请求到达时,OPcache 会检查内存缓存是否包含这个脚本的编译结果。如果已经包含,则直接使用缓存中的字节码 OPcodes 执行。否则,OPcache 会将源码解析为一个语法树然后生成对应的 OPcodes 并存储到内存缓存中,以便下次使用。

3.1.2 PHP8 下的 OPcache - 引入 JIT (Just In Time) 即时编译

之前 OPcache 扩展可以更快的获取 OPcodes 将其直接转到 Zend VM ,现在 JIT 让它们完全不使用 Zend VM 即可运行,Zend VM 是用 C 编写的程序,充当 OPcodes 和 CPU 之间的一层。

PHP 的 JIT 使用了名为 DynASM (Dynamic Assembler) 的库,在运行时直接生成编译后的代码,该库将一种特定格式的一组 CPU 指令映射为许多不同 CPU 类型的汇编代码。因此,编译器只需要使用 DynASM 就可以将 OPcodes 转换为特定结构体的机器码。

但是并不是所有的 OPcodes 都可以直接编译的, PHP 的 JIT 尝试只编译有价值的 OPcodes 。不然 PHP 就直接成为 编译型语言了,而不是脚本语言。 很重要的一个原因就是 PHP 弱语言类型,是在运行时推断类型,Zend VM 尝试执行某个操作码之前,PHP 通常不知道变量的类型。

  • code
    • OPcache
      • JIT
      • CPU
    • Zend Execute
    • CPU
  • Lex
  • Parse
  • Compile
  • Zend Execute
  • CPU

PHP JIT 流程图

PHP-JIT

3.2 OPcache 重点配置

下列配置是在 php7.4 中可用的

3.2.1 opcache.enable opcache.enable_cli 是否开启 OPcache

  • opcache.enable = 1 opcache.enable_cli = 1
    • 生产环境强烈建议开启!(如果不开启,后面就不用看了)
  • opcache.enable = 0 opcache.enable_cli = 0
    • 关闭 OPcache

3.2.2 opcache.validate_timestamps 是否检查脚本文件的时间戳来判断缓存是否过期

  • opcache.validate_timestamps = 1
    • 每次请求时检查文件的修改时间戳。如果文件的修改时间戳比缓存中设置的时间戳 opcache.revalidate_freq 更晚,OPcache 将会重新编译该文件并更新缓存
    • 生产环境中,更新服务器代码的时候, 如果代码较多,更新操作会有延迟的, 那就可能出现新老代码混合的情况, 此时对用户请求的处理存在不确定性
  • opcache.validate_timestamps = 0
    • 生产环境强烈建议关闭!
    • OPcache 将不再检查文件的时间戳,opcache.revalidate_freq 配置将被忽略,直到服务重启或者手动清除缓存为止
    • 这将意味着如果修改了代码, 把它更新到服务器上, 在浏览器上请求更新的代码对应的功能, 会看不到更新的效果, 必须得重新加载 PHP ,来重新生成缓存

3.2.3 opcache.revalidate_freq 多长时间(以秒为单位)后重新检查是否需要更新缓存中的文件

  • opcache.revalidate_freq = 2 默认值
    • 每隔2s检查缓存中的文件是否需要更新。如果有任何文件已被修改,则 OPcache 将重新编译生成新的 OPcodes , 并更新这些文件的缓存
    • 当然,这个时间间隔可以通过修改该配置项的值来调整
  • opcache.revalidate_freq = 0
    • 开发环境强烈可以设置为0
    • 值为0 表示每次请求都会检查 PHP 代码是否更新
    • 这将意味着增加很多次 stat 系统调用
    • stat 系统调用是读取文件的状态, 这里主要是获取最近修改时间, 这个系统调用会发生磁盘I/O, 所以必然会消耗一些CPU时间

3.2.4 opcache.memory_consumption 设置 OPcache 所分配的共享内存大小 (以 MB 为单位)

  • opcache.memory_consumption = 64 默认值
  • opcache.memory_consumption = 192
    • 如果你的代码很多,可以 opcache_get_status() 来获取 OPcache 使用的内存的总量, 如果这个值很大, 可以把这个选项设置得更大一些
    • 设置得太小,那么 OPcache 可能会无法缓存所有的 PHP 文件,从而导致一些性能问题
    • 设置得太大,那么可能会浪费系统的资源

3.2.5 opcache.interned_strings_buffer 设置 OPcache 字符串缓存池的大小 (以 MB 为单位)

  • string intern 字符串驻留
    • 字符串是一种常见的数据类型,通常会被大量地使用
    • 作用
      • 为了提高性能,PHP 引擎会将相同的字符串对象合并为同一个实例,这个过程称为字符串的 intern
      • 对于一些频繁使用的字符串,可以使用 intern 函数手动将其加入到字符串池中,以提高它们的重用率
      • 默认情况下这个不可变的内存区域只会存在于单个 php-fpm 的进程中, 如果设置了这个选项, 那么它将会在所有的 php-fpm 进程中共享
    • 例如
      • 代码中使用了 1000 次字符串 “foo”, 在 PHP 内部只会在第一使用这个字符串的时候分配一个不可变的内存区域来存储这个字符串
      • 其他的 999 次使用都会直接指向这个内存区域. 这个选项则会把这个特性提升一个层次
  • opcache.interned_strings_buffer = 8 默认值
    • 默认 8MB ,建议 32MB 或者不超过 64MB

3.2.6 opcache.max_accelerated_files 设置 OPcache 可以缓存的 PHP 文件数量

  • opcache.max_accelerated_files = 4000 默认值
    • 控制内存中最多可以缓存多少个 PHP 文件, 这个选项必须得设置得足够大, 大于你的项目中的所有 PHP 文件的总和
    • 取值
      • 最好是在质数集合 {223, 463, 983, 1979, 3907, 7963, 16229, 32531, 65407, 130987, ...} 中找到的第一个大于等于设置值的质数, 最大值 1000000
      • 可以查看当前项目 PHP 文件个数 find . -type f -print | grep php | wc -l
    • 例如
      • 代码库大概有 6000 个 PHP 文件,可以把这个值设置为一个素数 7963

3.2.7 opcache.fast_shutdown 控制在 PHP 进程终止时是否要尽快清除 OpCache 内的缓存

  • opcache.fast_shutdown = 1
    • “允许更快速关闭”. 它的作用是在单个请求结束时提供一种更快速的机制来调用代码中的析构器, 从而加快 PHP 的响应速度和 PHP 进程资源的回收速度, 这样应用程序可以更快速地响应下一个请求
    • 生产环境强烈建议开启!
    • 在 PHP Request Shutdown 的时候会收内存的速度会提高

3.2.8 opcache.huge_code_pages 是否开启大页面 Huge Pages

  • opcache.huge_code_pages = 1
    • OpCache 将使用大页面 Huge Pages 来尽可能地减少内存的使用和地址转换的开销,从而提升 PHP 应用程序的性能
    • Huge Pages
      • Huge Pages 大页面是一种操作系统级别的优化技术,在内存管理方面有着很好的表现,在某些场景下,使用大页面可以明显地提升性能
      • 默认的内存是以4KB分页的,而虚拟地址和内存地址是需要转换的, 转换是要查表的,CPU为了加速这个查表过程都会内建TLB (Translation Lookaside Buffer)
      • 显而易见如果虚拟页越小,表里的条目数也就越多,而TLB大小是有限的,条目数越多TLB的Cache Miss也就会越高,如果启用 Huge Pages ,就能一定程度降低 Cache Miss
      • 相对于小页面,大页面可以将内存分配成更大的块,这样就会降低内存碎片的程度,减少页表项的数量,进而提高了内存管理的效率
      • 如果Huge pages 可用, 那么 Opcache 也会用 Huge pages 来存储 OPcodes 缓存
  • opcache.huge_code_pages = 0
    • 默认关闭
  • 查看 Huge
    • 默认一个 Hugepage 的 size 是 2MB
      // 修改 php.ini 
      [root@Centos7 ~]# vim php.ini
      opcache.huge_code_pages = 1
      // 重启 php-fpm
      [root@Centos7 ~]# systemctl restart php-fpm
      // 查看 Huge
      [root@Centos7 ~]# cat /proc/meminfo | grep Huge
      AnonHugePages:   2045952 kB
      HugePages_Total:       0
      HugePages_Free:        0
      HugePages_Rsvd:        0
      HugePages_Surp:        0
      Hugepagesize:       2048 kB
      // 操作系统 分配一些 Huge pages
      [root@Centos7 ~]# sysctl vm.nr_hugepages=128
      vm.nr_hugepages = 128
      [root@Centos7 ~]# cat /proc/meminfo | grep Huge
      AnonHugePages:    2275328 kB
      HugePages_Total:     128
      HugePages_Free:      128
      HugePages_Rsvd:        0
      HugePages_Surp:        0
      Hugepagesize:       2048 kB
      // 重启 php-fpm
      [root@Centos7 ~]# systemctl restart php-fpm
      [root@Centos7 ~]# cat /proc/meminfo | grep Huge
      AnonHugePages:   2275328 kB
      HugePages_Total:     128
      HugePages_Free:      121
      HugePages_Rsvd:        0
      HugePages_Surp:        0
      Hugepagesize:       2048 kB
      // 查看 php-fpm 的 text 大小
      [root@Centos7 ~]# size /usr/sbin/php-fpm
      text       data        bss        dec        hex    filename
      4583126     559622     120200    5262948     504e64    /usr/sbin/php-fpm
      // 修改 php.ini 
      [root@Centos7 ~]# vim php.ini
      opcache.huge_code_pages = 0
      // 再次查看
      [root@opensource02v ~]#  cat /proc/meminfo | grep Huge
      AnonHugePages:   2287616 kB
      HugePages_Total:     128
      HugePages_Free:      123
      HugePages_Rsvd:        0
      HugePages_Surp:        0
      Hugepagesize:       2048 kB
      // 发现 php-fpm 启动后多用了 2 个 pages ,4583126 字节大概 也就是 4MB 

3.2.9 opcache.max_wasted_percentage 限制 OPCache 中可以容忍的最大浪费内存百分比

在 OpCache 中,有一部分内存是被浪费掉的,例如已经过期但尚未被清理的脚本缓存、被废弃但依然存在的符号表等等

  • opcache.max_wasted_percentage = 5 默认值
    • 当 OPcache 中浪费内存的百分比超过 5% 时,就会尝试触发内存回收机制,清理掉那些已经不再需要的数据
    • 慎重考虑:应用程序有较高的并发量或者需要频繁重载 PHP 文件,那么适当增大这个值,从而减少内存回收的频率,提高系统的性能和稳定性
  • OPcache 基于先到先得的原则进行缓存
    • 每当达到最大内存消耗或最大加速文件时 , OPcache 就会尝试重新启动缓存,但是没有超过最大浪费内存,还是不会重启,并且每次依然会尝试缓存,不期望这样的事情方式
    • 避免内存占满

3.2.10 opcache.file_cache 设置 OPCache 编译后的脚本缓存文件保存的路径 - 文件作为二级缓存

OPCache 首先是根据共享内存的大小进行缓存,如果内存满了,就只能重置 OPCache 或者删除一些缓存,那么还有一种思路,利用文件缓存。

  • opcache.file_cache = 默认值 空字符串
    • 为空,表示未启用文件缓存
    • 设置,定义这个磁盘缓存的路径
      • 有效路径
      • 可写权限
  • opcache.file_cache_only=0
    • 仅仅使用文件做缓存 0否
  • opcache.file_cache_consistency_checks=1
    • 当从文件缓存中加载脚本的时候,对文件进行校验
  • opcache.file_cache_fallback=1
    • 在 Windows 系统无法用到共享内存,强制使用文件作为缓存

3.2.11 opcache.preload PHP7.4 引入的预加载机制进一步优化性能

  • opcache.preload = 默认值 空字符串
    • 为空,表示未开启预加载
    • 设置,定义一个脚本文件的路径,在服务器启动时期进行编译和缓存的 PHP 脚本文件
  • opcache.preload_user = 默认值 空字符串
    • 为空
    • 设置 禁止以 root 用户预加载代码。该指令方便以其他用户预加载

预加载 (OPcache Preloading)

从 PHP 7.4.0 开始,PHP 可以配置为在引擎启动时将脚本预加载到 OPcache 中。这些文件中的任何函数、类、接口或特征(但不是常量)随后将对所有请求全局可用,而无需显式包含。这样,在后续的请求中, PHP 脚本执行时,这些预加载的文件就可以直接从 OPcache 缓存中读取它们编译过的 OPcodes

  • 预加载,需要一个自定义 PHP 脚本 ,其中可以用 opcache_compile_file() 来预加载你需要缓存的文件
  • 脚本在服务器启动的时候执行一次
  • 预先加载文件,必须预先加载它们的依赖项,如接口,trait 和父类
  • 预加载只加载文件,不执行文件,因此动态生成的一切无法被预加载
  • 不支持热更新,需要重启 PHP
  • 实际应用中,应该对经常使用的类进行预加载,而不要全部加载,您可以决定只预加载“热类”

3.2.11 opcache.jit PHP8 引入的JIT可以让你的程序起飞吗

JIT 在 OPcache 优化之后的基础上,结合 Runtime 再次优化,直接生成机器码

  • opcache.jit=on

    • 经典用法
      • disable 完全禁用,无法在运行时启用
      • off 禁用,但可以在运行时启用
      • tracing/on 使用追踪 JIT。默认启用并推荐给大部分用户
        • 对应 1254
      • function 使用函数 JIT
        • 对应 1205
    • 高级用法 CRTO
      • 第一位 C [特定CPU优化]
        • 0 禁用特定 CPU 优化
        • 1 如果 CPU 则支持 AVX 指令
      • 第二位 R [寄存器分配策略]
        • 0 不执行
        • 1 执行局部域寄存器分配
        • 2 执行全局寄存器分配
      • 第三位 T [JIT触发策略]
        • 0 在脚本加载时编译所有函数 (脚本级别,推荐使用0)
        • 1 在第一次执行时编译函数
        • 2 第一次请求时分析函数,然后编译最热门函数 (JIT调用次数最多的百分之(opcache.prof_threshold * 100))
        • 3 动态分析和编译热门函数 (超过N(N和opcache.jit_hot_func相关)次)
        • 4 目前未使用
        • 5 使用追踪 JIT。动态分析和为热门代码段编译追踪 (opcache.jit_hot_loopopcache.jit_hot_return) (WEB级别,推荐3 or 5)
      • 第四位 O [JIT优化级别]
        • 0 不JIT
        • 1 最小 JIT(调用标准 VM 处理程序)
        • 2 内联 VM 处理程序
        • 3 使用类型推断,做函数级别的JIT
        • 4 使用类型推断,使用调用图做函数级别的JIT
        • 5 使用类型推断,优化整个脚本级别的JIT
  • opcache.jit_buffer_size=64M

    • 为编译 JIT 代码保留的共享内存量。值 0 表示禁用 JIT。

3.3 OPcache 缓存策略

在启用 OPcache 模块后,生成的 OPcodes 可以被缓存到内存中,以供下次使用

3.3.1 缓存内容

  • PHP Class
  • PHP Function
  • PHP FilePath
  • PHP OpArray
  • Interned String 缓存
    • 变量名称、类名、方法名、字符串、注释
    • OPcache 开启后,在 PHP-FPM 下,全部进程都将共享 Interned String 缓存的字符串 节省内存

一个Web页面查看OPcache状态

git clone https://github.com/rlerdorf/opcache-status.git --depth=1
cd opcache-status
php -S localhost:8000 opcache.php

3.3.2 缓存策略

是缓存就存在过期,过期那么就要更新,为了防止正式环境代码执行不一致问题,建议永远不要自动过期,即不设置过期时间

  • 禁止缓存过期
    • opcache.revalidate_freq=0
    • opcache.validate_timestamps=0
  • 判断缓存已满
    • opcache.memory_consumption=64
    • opcache.max_accelerated_files=4000
    • opcache.max_wasted_percentage=5
  • 禁止在流量高峰期部署代码

3.3.3 缓存更新

  • 主动刷新缓存
    • php-fpm reload 平滑重启 (切记平滑,不要直接 restart)
  • 主动调用系统函数
    • opcache_reset() 重置整个 OPcache
    • 调用方式(具体看项目的部署方式)
      • php-cli 命令方式
      • php-fpm 接口方式
  • 使用第三方库
  • 一个示例 shell 脚本 重置 OPcache
#!/bin/bash
WEBDIR=/www/html/
RANDOM_NAME=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 13)
echo "<?php opcache_reset(); ?>" > ${WEBDIR}${RANDOM_NAME}.php
curl http://127.0.0.1/${RANDOM_NAME}.php
rm ${WEBDIR}${RANDOM_NAME}.php

3.4 OPcache 最佳实践

3.4.1 OPcache 生产配置

; OPcache 开启
opcache.enable=1
opcache.cli_enable=1
; OPcache 禁止检查脚本更新
opcache.revalidate_freq=0
; OPcache 禁止检查脚本更新间隔时间
opcache.validate_timestamps=0 
; OPcache 共享内存大小 512MB (根据实际情况判断)
opcache.memory_consumption=512
; Opcache 最大内存浪费占比 (根据实际情况判断)
opcache.max_wasted_percentage=10
; OPcache 缓存文件个数上限 (根据实际情况判断)
opcache.max_accelerated_files=50000
; OPcache 常驻字符串 64MB (根据实际情况判断)
opcache.interned_strings_buffer=64
; OPcache 开启大页面缓存
opcache.huge_code_pages = 1
; OPcache 使用快速停止续发事件
opcache.fast_shutdown=1

3.4.2 OPcache 开发配置

opcache.enable=1
opcache.cli_enable=1
opcache.revalidate_freq=1
opcache.validate_timestamps=0 
opcache.max_accelerated_files=4000
opcache.memory_consumption=64
opcache.interned_strings_buffer=32
opcache.huge_code_pages = 1
opcache.fast_shutdown=1

3.5 OPcache 默认扩展配置

  • php7.4 的 OPcache 默认配置
[opcache]
# 确定是否启用 Zend OPCache
; Determines if Zend OPCache is enabled
;opcache.enable=1

# 确定是否为PHP的CLI版本启用Zend OPCache
; Determines if Zend OPCache is enabled for the CLI version of PHP
;opcache.enable_cli=0

# OPcache共享内存存储大小
; The OPcache shared memory storage size.
;opcache.memory_consumption=128

# 用于插入字符串的内存量(以兆字节为单位)
; The amount of memory for interned strings in Mbytes.
;opcache.interned_strings_buffer=8

# OPcache哈希表中键(脚本)的最大数量 [200,1000000]
; The maximum number of keys (scripts) in the OPcache hash table.
; Only numbers between 200 and 1000000 are allowed.
;opcache.max_accelerated_files=10000

# 计划重新启动之前“浪费”内存的最大百分比
; The maximum percentage of "wasted" memory until a restart is scheduled.
;opcache.max_wasted_percentage=5

; When this directive is enabled, the OPcache appends the current working
; directory to the script key, thus eliminating possible collisions between
; files with the same name (basename). Disabling the directive improves
; performance, but may break existing applications.
;opcache.use_cwd=1

# 禁用时,必须手动重置OPcache或重新启动Web服务器,以使对文件系统的更改生效。
; When disabled, you must reset the OPcache manually or restart the
; webserver for changes to the filesystem to take effect.
;opcache.validate_timestamps=1

# 检查文件时间戳以了解对共享的更改的频率(以秒为单位)内存存储分配。
# “1”表示每秒验证一次,但仅限于每次请求一次。“0”表示始终验证
; How often (in seconds) to check file timestamps for changes to the shared
; memory storage allocation. ("1" means validate once per second, but only
; once per request. "0" means always validate)
;opcache.revalidate_freq=2

# 启用或禁用include_path优化中的文件搜索
; Enables or disables file search in include_path optimization
;opcache.revalidate_path=0

# 如果禁用,所有PHPDoc注释都将从代码中删除,以减少优化代码的大小。
; If disabled, all PHPDoc comments are dropped from the code to reduce the
; size of the optimized code.
;opcache.save_comments=1

# 允许文件存在重写(file_exists等)性能功能。
; Allow file existence override (file_exists, etc.) performance feature.
;opcache.enable_file_override=0

# 位掩码,其中每个位启用或禁用适当的OPcache 通行证
; A bitmask, where each bit enables or disables the appropriate OPcache
; passes
;opcache.optimization_level=0x7FFFBFFF

;opcache.dups_fix=0

# OPcache黑名单文件的位置(允许使用通配符)。 每个OPcache黑名单文件都是一个包含文件名的文本文件
# 不应该加速。文件格式是添加每个文件名到一条新线路。文件名可以是完整路径,也可以只是文件前缀
; The location of the OPcache blacklist file (wildcards allowed).
; Each OPcache blacklist file is a text file that holds the names of files
; that should not be accelerated. The file format is to add each filename
; to a new line. The filename may be a full path or just a file prefix
; (i.e., /var/www/x  blacklists all the files and directories in /var/www
; that start with 'x'). Line starting with a ; are ignored (comments).
;opcache.blacklist_filename=

# 允许将大文件排除在缓存之外。默认情况下,所有文件进行缓存
; Allows exclusion of large files from being cached. By default all files
; are cached.
;opcache.max_file_size=0

# 检查每N个请求的缓存校验和。默认值“0”表示禁用检查。
; Check the cache checksum each N requests.
; The default value of "0" means that the checks are disabled.
;opcache.consistency_checks=0

# 如果缓存。未被访问
; How long to wait (in seconds) for a scheduled restart to begin if the cache
; is not being accessed.
;opcache.force_restart_timeout=180

# 缓存错误日志文件名
; OPcache error_log file name. Empty string assumes "stderr".
;opcache.error_log=

# 所有OPcache错误都会进入Web服务器日志
# 默认情况下,只记录致命错误(级别0)或错误(级别1)。
# 您还可以启用警告(级别2)、信息消息(级别3)或 调试消息(级别4)。
; All OPcache errors go to the Web server log.
; By default, only fatal errors (level 0) or errors (level 1) are logged.
; You can also enable warnings (level 2), info messages (level 3) or
; debug messages (level 4).
;opcache.log_verbosity_level=1

# 首选共享内存后端。留空,由系统决定
; Preferred Shared Memory back-end. Leave empty and let the system decide.
;opcache.preferred_memory_model=

# 在脚本执行过程中,保护共享内存免受意外写入。仅对内部调试有用。
; Protect the shared memory from unexpected writing during script execution.
; Useful for internal debugging only.
;opcache.protect_memory=0

# 只允许从路径为 从指定的字符串开始。默认的“”表示没有限制
; Allows calling OPcache API functions only from PHP scripts which path is
; started from specified string. The default "" means no restriction
;opcache.restrict_api=

# 共享内存段的映射基础(仅适用于Windows)
; Mapping base of shared memory segments (for Windows only). All the PHP
; processes have to map shared memory into the same address space. This
; directive allows to manually fix the "Unable to reattach to base address"
; errors.
;opcache.mmap_base=

# 方便每个用户使用多个OPcache实例(仅适用于Windows)
; Facilitates multiple OPcache instances per user (for Windows only). All PHP
; processes with the same cache ID and user share an OPcache instance.
;opcache.cache_id=

# 启用和设置二级缓存目录。当SHM内存已满、服务器重新启动或SHM重置。默认的“”将禁用基于文件的缓存。
; Enables and sets the second level cache directory.
; It should improve performance when SHM memory is full, at server restart or
; SHM reset. The default "" disables file based caching.
;opcache.file_cache=

# 启用或禁用共享内存中的操作码缓存
; Enables or disables opcode caching in shared memory.
;opcache.file_cache_only=0

# 从文件缓存加载脚本时启用或禁用校验和验证
; Enables or disables checksum validation when script loaded from file cache.
;opcache.file_cache_consistency_checks=1

; Implies opcache.file_cache_only=1 for a certain process that failed to
; reattach to the shared memory (for Windows only). Explicitly enabled file
; cache is required.
;opcache.file_cache_fallback=1

# 启用或禁用将PHP代码(文本段)复制到巨大的页面中
; Enables or disables copying of PHP code (text segment) into HUGE PAGES.
; This should improve performance, but requires appropriate OS configuration.
;opcache.huge_code_pages=0

# 验证缓存的文件权限权限
; Validate cached file permissions.
;opcache.validate_permission=0

# 防止环境中的名称冲突。
; Prevent name collisions in chroot'ed environment.
;opcache.validate_root=0

# 如果指定,它会生成操作码转储,用于调试的不同阶段
; If specified, it produces opcode dumps for debugging different stages of
; optimizations.
;opcache.opt_debug_level=0

; Specifies a PHP script that is going to be compiled and executed at server
; start-up.
; http://php.net/opcache.preload
;opcache.preload=

# 出于安全原因,不允许以root身份预加载代码
; Preloading code as root is not allowed for security reasons. This directive
; facilitates to let the preloading to be run as another user.
; http://php.net/opcache.preload_user
;opcache.preload_user=

# 阻止缓存小于此秒数的文件。它防止缓存未完全更新的文件。
# 以防所有文件更新在您的站点上是原子的,您可以通过将其设置为“0”来提高性能。
; Prevents caching files that are less than this number of seconds old. It
; protects from caching of incompletely updated files. In case all file updates
; on your site are atomic, you may increase performance by setting it to "0".
;opcache.file_update_protection=2

# 用于存储共享锁定文件的绝对路径
; Absolute path used to store shared lockfiles (for *nix only).
;opcache.lockfile_path=/tmp
本作品采用《CC 协议》,转载必须注明作者和本文链接
明天我们吃什么 悲哀藏在现实中 Tacks
本帖由系统于 4个月前 自动加精
讨论数量: 2

终于更新了,支持

4个月前 评论
JuferYu

难得的好文章,一定要多多支持

1个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!