《讓PHP擴充套件開拓程式設計前路》 之 效能最佳化利器 OPcache

Tacks發表於2023-10-31
  • 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

相關文章