PHP7 Opcache是怎么工作的你知道嗎?
2019-11-08
1. 概述
在理解 OPCache 功能之前,我們有必要先理解PHP-FPM + Nginx 的工作機(jī)制,以及PHP腳本解釋執(zhí)行的機(jī)制。
1.1 PHP-FPM + Nginx 的工作機(jī)制
請求從Web瀏覽器到Nginx,再到PHP處理完成,一共要經(jīng)歷如下五個(gè)步驟:
第一步:啟動(dòng)服務(wù)
啟動(dòng)PHP-FPM。PHP-FPM 支持兩種通信模式:TCP socket和Unix socket;
PHP-FPM 會啟動(dòng)兩種類型的進(jìn)程:Master 進(jìn)程 和 Worker 進(jìn)程,前者負(fù)責(zé)監(jiān)控端口、分配任務(wù)、管理Worker進(jìn)程;后者就是PHP的cgi程序,負(fù)責(zé)解釋編譯執(zhí)行PHP腳本。
啟動(dòng)Nginx。首先會載入 ngx_http_fastcgi_module 模塊,初始化FastCGI執(zhí)行環(huán)境,實(shí)現(xiàn)FastCGI協(xié)議請求代理
這里要注意:fastcgi的worker進(jìn)程(cgi進(jìn)程),是由PHP-FPM來管理,不是Nginx。Nginx只是代理
第二步:Request => Nginx
Nginx 接收請求,并基于location配置,選擇一個(gè)合適handler
這里就是代理PHP的 handler
第三步:Nginx => PHP-FPM
Nginx 把請求翻譯成fastcgi請求
通過TCP socket/Unix Socket 發(fā)送給PHP-FPM 的master進(jìn)程
第四步:PHP-FPM Master => Worker
PHP-FPM master 進(jìn)程接收到請求
分配Worker進(jìn)程執(zhí)行PHP腳本,如果沒有空閑的Worker,返回502錯(cuò)誤
Worker(php-cgi)進(jìn)程執(zhí)行PHP腳本,如果超時(shí),返回504錯(cuò)誤
處理結(jié)束,返回結(jié)果
第五步:PHP-FPM Worker => Master => Nginx
PHP-FPM Worker 進(jìn)程返回處理結(jié)果,并關(guān)閉連接,等待下一個(gè)請求
PHP-FPM Master 進(jìn)程通過Socket 返回處理結(jié)果
Nginx Handler順序?qū)⒚恳粋€(gè)響應(yīng)buffer發(fā)送給第一個(gè)filter → 第二個(gè) → 以此類推 → 最終響應(yīng)發(fā)送給客戶端
1.2 PHP腳本解釋執(zhí)行的機(jī)制
了解了PHP + Nginx 整體的處理流程后,我們接下來看一下PHP腳本具體執(zhí)行流程,
首先我們看一個(gè)實(shí)例:
<?php if (!empty($_POST)) { echo "Response Body POST: ", json_encode($_POST), "\n"; } if (!empty($_GET)) { echo "Response Body GET: ", json_encode($_GET), "\n"; }
我們分析一下執(zhí)行過程:
php初始化執(zhí)行環(huán)節(jié),啟動(dòng)Zend引擎,加載注冊的擴(kuò)展模塊
初始化后讀取腳本文件,Zend引擎對腳本文件進(jìn)行詞法分析(lex),語法分析(bison),生成語法樹
Zend 引擎編譯語法樹,生成opcode,
Zend 引擎執(zhí)行opcode,返回執(zhí)行結(jié)果
在PHP cli模式下,每次執(zhí)行PHP腳本,四個(gè)步驟都會依次執(zhí)行一遍;
在PHP-FPM模式下,步驟1)在PHP-FPM啟動(dòng)時(shí)執(zhí)行一次,后續(xù)的請求中不再執(zhí)行;步驟2)~4)每個(gè)請求都要執(zhí)行一遍;
其實(shí)步驟2)、3)生成的語法樹和opcode,同一個(gè)PHP腳本每次運(yùn)行的結(jié)果都是一樣的,
在PHP-FPM模式下,每次請求都要處理一遍,是對系統(tǒng)資源極大的浪費(fèi),那么有沒有辦法優(yōu)化呢?
當(dāng)然有,如:
OPCache:前身是Zend Optimizer+ ,是 Zend Server 的一個(gè)開源組件;官方出品,強(qiáng)力推薦
APC:Alternative PHP Cache 是一個(gè)開放自由的 PHP opcode 緩存組件,用于緩存、優(yōu)化 PHP 中間代碼;已經(jīng)不更新了不推薦
APCu:是APC的一個(gè)分支,共享內(nèi)存,緩存用戶數(shù)據(jù),不能緩存opcode,可以配合Opcache 使用
eAccelerate:同樣是不更新了,不推薦
xCache:不再推薦使用了
2. OPCache 介紹
OPCache 是Zend官方出品的,開放自由的 opcode 緩存擴(kuò)展,還具有代碼優(yōu)化功能,省去了每次加載和解析 PHP 腳本的開銷。
PHP 5.5.0 及后續(xù)版本中已經(jīng)綁定了 OPcache 擴(kuò)展。
緩存兩類內(nèi)容:
OPCode
Interned String,如注釋、變量名等
3. OPCache 原理
OPCache緩存的機(jī)制主要是:將編譯好的操作碼放入共享內(nèi)存,提供給其他進(jìn)程訪問。
這里就涉及到內(nèi)存共享機(jī)制,另外所有內(nèi)存資源操作都有鎖的問題,我們一一解讀。
3.1 共享內(nèi)存
UNIX/Linux 系統(tǒng)提供很多種進(jìn)程間內(nèi)存共享的方式:
System-V shm API: System V共享內(nèi)存,
sysv shm是持久化的,除非被一個(gè)進(jìn)程明確的刪除,否則它始終存在于內(nèi)存里,直到系統(tǒng)關(guān)機(jī);
mmap API:
mmap映射的內(nèi)存在不是持久化的,如果進(jìn)程關(guān)閉,映射隨即失效,除非事先已經(jīng)映射到了一個(gè)文件上
內(nèi)存映射機(jī)制mmap是POSIX標(biāo)準(zhǔn)的系統(tǒng)調(diào)用,有匿名映射和文件映射兩種
mmap的一大優(yōu)點(diǎn)是把文件映射到進(jìn)程的地址空間
避免了數(shù)據(jù)從用戶緩沖區(qū)到內(nèi)核page cache緩沖區(qū)的復(fù)制過程;
當(dāng)然還有一個(gè)優(yōu)點(diǎn)就是不需要頻繁的read/write系統(tǒng)調(diào)用
POSIX API:System V 的共享內(nèi)存是過時(shí)的, POSIX共享內(nèi)存提供了使用更簡單、設(shè)計(jì)更合理的API.
Unix socket API
OPCache 使用了前三個(gè)共享內(nèi)存機(jī)制,根據(jù)配置或者默認(rèn)mmap 內(nèi)存共享模式。
依據(jù)PHP字節(jié)碼緩存的場景,OPCache的內(nèi)存管理設(shè)計(jì)非常簡單,快速讀寫,不釋放內(nèi)存,過期數(shù)據(jù)置為Wasted。
當(dāng)Wasted內(nèi)存大于設(shè)定值時(shí),自動(dòng)重啟OPCache機(jī)制,清空并重新生成緩存。
3.2 互斥鎖
任何內(nèi)存資源的操作,都涉及到鎖的機(jī)制。
共享內(nèi)存:一個(gè)單位時(shí)間內(nèi),只允許一個(gè)進(jìn)程執(zhí)行寫操作,允許多個(gè)進(jìn)程執(zhí)行讀操作;
寫操作同時(shí),不阻止讀操作,以至于很少有鎖死的情況。
這就引發(fā)另外一個(gè)問題:新代碼、大流量場景,進(jìn)程排隊(duì)執(zhí)行緩存opcode操作;重復(fù)寫入,導(dǎo)致資源浪費(fèi)。
4. OPCache 緩存解讀
OPCache 是官方的Opcode 緩存解決方案,在PHP5.5版本之后,已經(jīng)打包到PHP源碼中一起發(fā)布。
它將PHP編譯產(chǎn)生的字節(jié)碼以及數(shù)據(jù)緩存到共享內(nèi)存中, 在每次請求,從緩存中直接讀取編譯后的opcode,進(jìn)行執(zhí)行。
通過節(jié)省腳本的編譯過程,提高PHP的運(yùn)行效率。
如果正在使用APC擴(kuò)展,做同樣的工作,現(xiàn)在強(qiáng)烈推薦OPCache來代替,尤其是PHP7中。
4.1 OPCode 緩存
Opcache 會緩存OPCode以及如下內(nèi)容:
PHP腳本涉及到的函數(shù)
PHP腳本中定義的Class
PHP腳本文件路徑
PHP腳本OPArray
PHP腳本自身結(jié)構(gòu)/內(nèi)容
4.2 Interned String 緩存
首先我們需要理解,什么是 Interned String?
在PHP5.4的時(shí)候, 引入了Interned String機(jī)制, 用于優(yōu)化PHP對字符串的存儲和處理。
尤其是處理大塊的字符串,比如PHP doces時(shí),Interned String 可以優(yōu)化內(nèi)存。
Interned String 緩存的內(nèi)容包括: 變量名稱、類名、方法名、字符串、注釋等。
在PHP-FPM模式中,Interned String 緩存字符,僅限于Worker 進(jìn)程內(nèi)部。
而緩存到OPCache中,那么Worker進(jìn)程之間可以使用 Interned String 緩存的字符串,節(jié)省內(nèi)存。
我們需要注意一個(gè)事情,在PHP開發(fā)中,一般會有大段的注釋,也會被緩存到OPCache中。
可以通過php.ini的配置,關(guān)閉注釋的緩存。
但是,像Zend Framework等框架中,會引用注釋,所以,是否關(guān)閉注釋的緩存,需要區(qū)別對待。
5. OPCache 更新策略
是緩存,都存在過期,以及更新策略等。
而OPCache的更新策略非常簡單,到期數(shù)據(jù)置為Wasted,達(dá)到設(shè)定值,清空緩存,重建緩存。
這里需要注意:在高流量的場景下,重建緩存是一件非常耗費(fèi)資源的事兒。
OPCache 在創(chuàng)建緩存時(shí)并不會阻止其他進(jìn)程讀取。
這會導(dǎo)致大量進(jìn)程反復(fù)新建緩存。所以,不要設(shè)置OPCache過期時(shí)間
每次發(fā)布新代碼時(shí),都會出現(xiàn)反復(fù)新建緩存的情況。如何避免呢?
不要在高峰期發(fā)布代碼,這是任何情況下都要遵守的規(guī)則
代碼預(yù)熱,比如使用腳本批量調(diào)PHP 訪問URL,或者使用OPCache 暴露的API 如
opcache_compile_file()
進(jìn)行編譯緩存
6. OPCache 的配置
6.1 內(nèi)存配置
opcache.preferred_memory_model="mmap"
OPcache 首選的內(nèi)存模塊。如果留空,OPcache 會選擇適用的模塊, 通常情況下,自動(dòng)選擇就可以滿足需求??蛇x值包括:mmap
,shm
,posix
以及win32
。opcache.memory_consumption=64
OPcache 的共享內(nèi)存大小,以兆字節(jié)為單位,默認(rèn)64M
opcache.interned_strings_buffer=4
用來存儲臨時(shí)字符串的內(nèi)存大小,以兆字節(jié)為單位,默認(rèn)4M
opcache.max_wasted_percentage=5
浪費(fèi)內(nèi)存的上限,以百分比計(jì)。如果達(dá)到此上限,那么 OPcache 將產(chǎn)生重新啟動(dòng)續(xù)發(fā)事件。默認(rèn)5
6.2 允許緩存的文件數(shù)量以及大小
opcache.max_accelerated_files=2000
OPcache 哈希表中可存儲的腳本文件數(shù)量上限。真實(shí)的取值是在質(zhì)數(shù)集合{ 223, 463, 983, 1979, 3907, 7963, 16229, 32531, 65407, 130987 }
中找到的第一個(gè)大于等于設(shè)置值的質(zhì)數(shù)。設(shè)置值取值范圍最小值是200
,最大值在 PHP 5.5.6 之前是100000
,PHP 5.5.6 及之后是1000000
。默認(rèn)值2000
opcache.max_file_size=0
以字節(jié)為單位的緩存的文件大小上限。設(shè)置為 0 表示緩存全部文件。默認(rèn)值0
6.3 注釋相關(guān)的緩存
opcache.load_comments
boolean
如果禁用,則即使文件中包含注釋,也不會加載這些注釋內(nèi)容。本選項(xiàng)可以和opcache.save_comments
一起使用,以實(shí)現(xiàn)按需加載注釋內(nèi)容。opcache.fast_shutdown
boolean 如果啟用,則會使用快速停止續(xù)發(fā)事件。所謂快速停止續(xù)發(fā)事件是指依賴 Zend 引擎的內(nèi)存管理模塊 一次釋放全部請求變量的內(nèi)存,而不是依次釋放每一個(gè)已分配的內(nèi)存塊。
6.4 二級緩存的配置
opcache.file_cache
配置二級緩存目錄并啟用二級緩存。啟用二級緩存可以在 SHM 內(nèi)存滿了、服務(wù)器重啟或者重置 SHM 的時(shí)候提高性能。默認(rèn)值為空字符串""
,表示禁用基于文件的緩存。opcache.file_cache_only
boolean
啟用或禁用在共享內(nèi)存中的 opcode 緩存。opcache.file_cache_consistency_checks
boolean
當(dāng)從文件緩存中加載腳本的時(shí)候,是否對文件的校驗(yàn)和進(jìn)行驗(yàn)證。opcache.file_cache_fallback
boolean
在 Windows 平臺上,當(dāng)一個(gè)進(jìn)程無法附加到共享內(nèi)存的時(shí)候, 使用基于文件的緩存,也即:opcache.file_cache_only=1
。需要顯示的啟用文件緩存。
7 配置信息
zend_extension=opcache.so
; Zend Optimizer + 的開關(guān), 關(guān)閉時(shí)代碼不再優(yōu)化.
opcache.enable=1
; Determines if Zend OPCache is enabled for the CLI version of PHP
opcache.enable_cli=1
; Zend Optimizer + 共享內(nèi)存的大小, 總共能夠存儲多少預(yù)編譯的 PHP 代碼(單位:MB)
; 推薦 128
opcache.memory_consumption=64
; Zend Optimizer + 暫存池中字符串的占內(nèi)存總量.(單位:MB)
; 推薦 8
opcache.interned_strings_buffer=4
; 最大緩存的文件數(shù)目 200 到 100000 之間
; 推薦 4000
opcache.max_accelerated_files=2000
; 內(nèi)存“浪費(fèi)”達(dá)到此值對應(yīng)的百分比,就會發(fā)起一個(gè)重啟調(diào)度.
opcache.max_wasted_percentage=5
; 開啟這條指令, Zend Optimizer + 會自動(dòng)將當(dāng)前工作目錄的名字追加到腳本鍵上,
; 以此消除同名文件間的鍵值命名沖突.關(guān)閉這條指令會提升性能,
; 但是會對已存在的應(yīng)用造成破壞.
opcache.use_cwd=0
; 開啟文件時(shí)間戳驗(yàn)證
opcache.validate_timestamps=1
; 2s檢查一次文件更新 注意:0是一直檢查不是關(guān)閉
; 推薦 60
opcache.revalidate_freq=2
; 允許或禁止在 include_path 中進(jìn)行文件搜索的優(yōu)化
;opcache.revalidate_path=0
; 是否保存文件/函數(shù)的注釋 如果apigen、Doctrine、 ZF2、 PHPUnit需要文件注釋
; 推薦 0
opcache.save_comments=1
; 是否加載文件/函數(shù)的注釋
;opcache.load_comments=1
; 打開快速關(guān)閉, 打開這個(gè)在PHP Request Shutdown的時(shí)候會收內(nèi)存的速度會提高
; 推薦 1
opcache.fast_shutdown=1
;允許覆蓋文件存在(file_exists等)的優(yōu)化特性。
;opcache.enable_file_override=0
; 定義啟動(dòng)多少個(gè)優(yōu)化過程
;opcache.optimization_level=0xffffffff
; 啟用此Hack可以暫時(shí)性的解決”can’t redeclare class”錯(cuò)誤.
;opcache.inherited_hack=1
; 啟用此Hack可以暫時(shí)性的解決”can’t redeclare class”錯(cuò)誤.
;opcache.dups_fix=0
; 設(shè)置不緩存的黑名單
; 不緩存指定目錄下cache_開頭的PHP文件. /png/www/example.com/public_html/cache/cache_
;opcache.blacklist_filename=
; 通過文件大小屏除大文件的緩存.默認(rèn)情況下所有的文件都會被緩存.
;opcache.max_file_size=0
; 每 N 次請求檢查一次緩存校驗(yàn).默認(rèn)值0表示檢查被禁用了.
; 由于計(jì)算校驗(yàn)值有損性能,這個(gè)指令應(yīng)當(dāng)緊緊在開發(fā)調(diào)試的時(shí)候開啟.
;opcache.consistency_checks=0
; 從緩存不被訪問后,等待多久后(單位為秒)調(diào)度重啟
;opcache.force_restart_timeout=180
; 錯(cuò)誤日志文件名.留空表示使用標(biāo)準(zhǔn)錯(cuò)誤輸出(stderr).
;opcache.error_log=
; 將錯(cuò)誤信息寫入到服務(wù)器(Apache等)日志
;opcache.log_verbosity_level=1
; 內(nèi)存共享的首選后臺.留空則是讓系統(tǒng)選擇.
;opcache.preferred_memory_model=
; 防止共享內(nèi)存在腳本執(zhí)行期間被意外寫入, 僅用于內(nèi)部調(diào)試.
;opcache.protect_memory=0