OPcache 設定指南——記憶體配置與命中率調校

網站明明裝了快取外掛,後台還是卡、TTFB 還是高,問題往往不在頁面快取那一層,而在更底層的 PHP 編譯。每一個沒被頁面快取攔下的請求,包含登入後的後台操作、REST API 呼叫、結帳流程,都得讓 PHP 把原始碼重新編譯成 bytecode 才能執行。OPcache 設定就是決定這件事要做幾次的開關,設對了,編譯成本一個 PHP-FPM 工作程序只付一次,之後上萬次請求都直接吃記憶體裡的編譯結果。

問題是 PHP 出廠的 OPcache 預設值是為「保守省資源」設計的,不是為一個掛了三十個外掛的 WooCommerce 商店設計的。預設的 128 MB 記憶體與一萬個檔案上限,在外掛一多的站台很快就撞牆。這篇會逐項拆解真正影響 WordPress 與 WooCommerce 的 OPcache 設定,告訴你記憶體與檔案數該配多少、怎麼讀 opcache_get_status() 判斷命中率是否健康,以及為什麼很多人把參數寫進 PHP-FPM 設定檔卻完全沒生效。

OPcache 到底在快取什麼,跟頁面快取差在哪

OPcache 只快取一樣東西,就是編譯後的 PHP bytecode,它不存 HTML、不存資料庫查詢結果、也不碰任何應用層資料。PHP 執行一支腳本時,會先把原始碼斷詞、解析、再編譯成 Zend 引擎看得懂的 opcode;沒有 OPcache 的話,這一整套流程每個請求都要重跑一次。OPcache 把編譯結果放進共享記憶體,下一個請求就能跳過編譯,直接執行。

這跟頁面快取、物件快取處在完全不同的層級。頁面快取(例如 WP Super Cache、Nginx FastCGI Cache)是直接把整頁 HTML 存起來,請求進來連 PHP 都不用跑;物件快取(Redis、Memcached)省的是重複的資料庫查詢。OPcache 省的則是「PHP 解析與編譯」這一段 CPU 時間,它墊在所有快取的最底下。一個外掛繁多的 WordPress 請求動輒載入數百個 PHP 檔,沒有 OPcache 時這幾百個檔每次都要重新編譯。

關鍵在於,這三層彼此不能互相取代。頁面快取救得了匿名訪客,但救不了登入後台的編輯;OPcache 則是只要請求最終跑到了 PHP,就一律受益,包含那些繞過頁面快取的動態請求。在中型 WooCommerce 站台上,正確配置 OPcache 後,未命中頁面快取的請求路徑普遍能省下數百毫秒的 TTFB,流量尖峰時的 CPU 用量也會明顯下降。要強調的是,OPcache 解的是編譯成本,解不了慢查詢、同步的外部 API 呼叫、或佈景主題裡的效能熱點,那些得靠其他層處理。

一個版本差異值得先記下來:從 PHP 8.5 起,OPcache 被編進 PHP 核心,不再是可選的擴充套件,新環境不會再出現「整個 build 漏掉 OPcache」的意外。PHP 8.4 以前它技術上仍是選用擴充,用 Dockerfile 或自行編譯時有機會漏載,所以調校前務必先確認它真的存在且啟用了。

哪六個 OPcache 設定真正影響 WordPress 效能

OPcache 有數十個 ini 指令,但對 WordPress 站台來說,真正決定它「有沒有在運作」或「白白吃記憶體」的,其實就下面六個。其餘指令多半維持預設即可。

指令 作用 WordPress 建議值
opcache.memory_consumption 配給 OPcache 的共享記憶體總量(MB) 標準站 256,外掛繁多或 WooCommerce 384–512
opcache.interned_strings_buffer 字串去重緩衝區大小(MB),從上面的總量裡切出 16 起跳,外掛多用 32
opcache.max_accelerated_files 最多追蹤幾支 PHP 檔 32531,WooCommerce/多站用 65407
opcache.validate_timestamps 是否週期性檢查檔案有沒有被改過 自架站維持 1,有部署管線才改 0
opcache.revalidate_freq 多久檢查一次檔案時間戳(秒) 正式環境 60,開發環境 0
opcache.save_comments 是否保留 docblock 註解 維持 1,不要關

這六個之中,前三個負責「容量」,配不夠就會掉快取;中間兩個負責「新鮮度」,也就是改了程式碼多久會生效;最後一個是相容性安全閥。後面會逐項說明數值怎麼抓,先把整體骨架放在這裡,方便對照。

opcache.save_comments 這個常被忽略的安全閥要特別點出來。它預設是 1,意思是保留 PHP 的 docblock 註解。有人為了省一點記憶體把它關成 0,結果踩到地雷,因為不少 WordPress 外掛與框架會在執行期讀取 docblock 來做註解解析(annotation)、註冊 REST API 路由或探索 CLI 指令,Doctrine、PHPUnit 這類工具也依賴它。關掉省下的記憶體通常只有幾個百分比,換來的卻可能是難以追查的外掛故障,不值得。維持 1 就對了。

WordPress 與 WooCommerce 的記憶體該配多少

直接給結論,標準 WordPress 站從 256 MB 起跳,WooCommerce、多站台、或外掛超過四十個的站從 384 MB 或 512 MB 起跳。opcache.memory_consumption 設定的是 OPcache 能用多少共享記憶體來存編譯後的程式碼,這個值若太小,是自架 WordPress 最常見的 OPcache 設定錯誤。

問題的連鎖反應是這樣的:預設的 128 MB 在一個掛二、三十個外掛的站很快就被塞滿,OPcache 開始清掉舊的快取項目騰空間,命中率隨之崩跌,而每一次被清掉的檔案,下一個請求就得從原始碼重新編譯。原本該省下的編譯成本又全部回來了。判斷記憶體是否吃緊的訊號很明確,就是 opcache_get_status() 裡的 oom_restarts(記憶體不足重啟次數),這個計數器若不是零而且持續往上爬,代表 memory_consumption 配太小了,該往上加。

第二個常被遺漏的記憶體設定是 opcache.interned_strings_buffer,也就是「字串去重緩衝區」。PHP 會把程式碼裡重複出現的字串(hook 名稱、option 鍵值、類別名、函式名、翻譯字串)在記憶體裡只存一份共用,這能大幅降低記憶體開銷與字串比對成本。WordPress 與它的外掛生態用到的這類字串非常多,每個外掛都註冊一堆 hook 與 option,預設的 8 MB 很快就見底。緩衝區一滿,PHP 就退回去用一般的堆積配置,比較慢,也吃不到去重的好處。

這裡有個容易忽略的相依關係要記牢:interned strings 緩衝區是從 memory_consumption 的總量裡切出來的,不是額外加上去的。如果你設 memory_consumption=256 又設 interned_strings_buffer=32,實際拿來存 bytecode 的只剩 224 MB。所以調高字串緩衝區時,記得也把總記憶體一起往上抓,這兩個變數是綁在一起的。緩衝區是否快滿,看 opcache_get_status()interned_strings_usagefree_memory 是不是逼近零、number_of_strings 是不是逼近上限。

max_accelerated_files 要設多大才不會掉快取

先記一個原則,數一數站台實際的 PHP 檔案數,再把上限設到「舒服地超過」那個數字。opcache.max_accelerated_files 控制的是 OPcache 最多追蹤幾支 PHP 檔,每支檔案佔一個槽位,槽位用完後就算記憶體還有空間,新檔案也會被擠掉、停止快取。

預設值是 10000,聽起來很多,但一個乾淨的 WordPress 大約就有 3000 支 PHP 檔,加上二十個外掛與一個典型佈景主題會來到 8000 到 15000 支,光是 WooCommerce 自己就再添數千支。要查站台實際的檔案數,可以在伺服器上跑這行:

find /var/www/yoursite -name “*.php” -type f | wc -l

有一個反直覺的細節要注意:你填進去的數字,PHP 內部會自動進位到一串質數裡的下一個值,這串質數依序是 223、463、983、1979、3907、7963、16229、32531、65407、130987、262237、524521、1048793。換句話說,你填 20000,PHP 實際會用 32531。既然如此,乾脆直接填這串質數裡的值,避免猜不準。對多數正式環境的 WordPress 站,32531 是合理目標;WooCommerce 或外掛繁多的多站台,用 65407。

如果這個值設太低,OPcache 撞到上限後就不再快取新檔。判斷的訊號是 opcache_get_status()num_cached_keys 逼近 max_cached_keys,或者 hash_restarts 計數器在往上跳,後者明確對應到「檔案數上限太小」。還有一個容易被遺忘的來源是程式碼產生的檔案,像 Symfony、Magento 這類框架,或是某些快取機制會動態產生大量不在版控裡的 PHP 檔;同一台伺服器若跑多個應用共用同一個 FPM 工作池,這些檔案數都要一起算進去。

validate_timestamps 與 revalidate_freq 怎麼設才不會放出舊程式碼

這兩個設定一起決定「你改了 PHP 檔之後,多久會真的生效」。opcache.validate_timestamps 控制 OPcache 要不要去檢查磁碟上的檔案有沒有被改過,預設 1(會檢查);opcache.revalidate_freq 則是檢查的間隔秒數,預設 2 秒。對多數自架 WordPress 站,最穩的組合是 validate_timestamps=1 搭配 revalidate_freq=60

維持 validate_timestamps=1 的理由是,正是這個檢查讓外掛更新、佈景主題改動、WP-CLI 編輯能在下一個請求就生效。把 revalidate_freq 從預設的 2 秒拉長到 60 秒,是因為 2 秒等於幾乎每個請求都要去 stat 一次成千上萬支檔案的修改時間,在繁忙的站台上很浪費。設成 60 秒代表一支被改過的腳本最多一分鐘後生效,對任何不是「即時改即時看」的開發迴圈來說都夠用。把它設成 0 會強制每個請求都檢查,那只適合本機開發,正式站不要這樣設。

validate_timestamps=0 是效能最高的選項,因為 PHP-FPM 連每個被 include 檔案的 stat() 系統呼叫都省了,實測大約能再快 1% 到 3%。但這個選項有前提,也有風險。前提是你必須有一套受控的部署管線,每次部署都會跑 opcache_reset() 或重新載入 PHP-FPM;風險是漏了這一步,你的外掛更新、甚至一個安全性修補,會「看起來部署成功了卻完全沒生效」,舊的 bytecode 會一直活著,直到工作程序因為別的原因被回收。

如果決定走 validate_timestamps=0 這條路,重置快取要用對方法。直接呼叫 opcache_reset() 只清掉「當前這個 FPM 工作程序」的快取,不會清掉整個池裡其他工作程序的快取;當 PHP-FPM 跑多個工作程序時,正確做法是對 PHP-FPM 送出 SIGHUP 訊號讓它優雅重載。另一個常見坑是用「每次發布都 checkout 一份新目錄」的部署策略,OPcache 會把同一支腳本在不同檔案路徑下各快取一份,舊版本的快取沒清掉就會把新版本擠出記憶體,最後變成快取爆滿又沒在加速。所以這種部署一定要搭配對舊版目錄逐檔呼叫 opcache_invalidate($file, true),或乾脆整個 reset。

怎麼用 opcache_get_status 判讀命中率與記憶體壓力

判斷 OPcache 健不健康,最直接的方式就是讀它的狀態。要注意一個前提,OPcache 每個 SAPI 用的是各自獨立的共享記憶體池,所以你在命令列跑 opcache_get_status() 拿到的數字,跟網站實際吃的(PHP-FPM 那個池)是不同的,狀態一定要透過 PHP-FPM 或 Apache 來讀才準。常見做法是放一支受權杖保護的小腳本在站內,呼叫 opcache_get_status(false) 把狀態以 JSON 輸出,看完務必刪掉,因為它會洩漏路徑與環境資訊。

健康的 OPcache 大致長這樣:opcache_enabled 為 true、cache_full 為 false、memory_usage.used_memory 成長到一個遠低於上限的穩定值、oom_restarts 為 0、num_cached_scripts 穩定且遠低於 max_accelerated_files。最關鍵的單一指標是 opcache_statistics.opcache_hit_rate,快取暖機後應該在 98% 以上,理想是越接近 100% 越好。命中率若在一小時的流量後還壓在 95% 以下,代表有東西在逼著它重新編譯,常見元兇是 validate_timestamps=1 搭配某些每次 rsync 都改寫檔案時間戳的部署工具,這時要確認部署有保留 mtime。

幾個計數器要對照著看,才能對症下藥:

  • oom_restarts:記憶體不足導致的重啟,對應 memory_consumption 太小,往上加記憶體。
  • hash_restarts:檔案數雜湊表滿了導致的重啟,對應 max_accelerated_files 太小,往上加檔案上限。
  • current_wasted_percentage:記憶體碎片比例,跟 opcache.max_wasted_percentage(預設 5)對照。

這裡有個反直覺、且 PHP 官方文件沒寫清楚的行為要特別提醒。OPcache 用的是先到先存、沒有 LRU 淘汰策略;當記憶體或檔案數達到上限時,它會「嘗試」重啟整個快取,但只有在浪費的記憶體比例超過 max_wasted_percentage 時才真的重啟。換句話說,如果 cache_full 是 true、浪費比例卻沒到門檻,OPcache 既不重啟、也存不下新腳本,每個沒被快取的腳本就會每次請求都重新編譯,效能等同沒裝 OPcache。這正是命中率掉到 99% 以下時要去查的根因,解法是看是記憶體不夠就加 memory_consumption、是檔案數不夠就加 max_accelerated_files。要避免的最壞情況,是流量高峰時 OPcache 清空重啟,大量請求同時湧入重建同一批快取,造成 cache slam。

PHP 8 的 JIT 與 preload 對 WordPress 有沒有用

先講 JIT 的結論,對絕大多數 WordPress 與 WooCommerce 站台,把 JIT 維持關閉就好。PHP 8.0 在 OPcache 之上加了 JIT 即時編譯器,它能加速的是 CPU 密集、反覆走相同路徑的程式碼,例如影像處理、加密、大量數學運算。但 WordPress 的請求是 I/O 密集型,時間絕大多數花在等資料庫查詢、Redis 查找、外部 HTTP 呼叫與檔案讀取,JIT 只快得了 PHP 程式碼執行那一小段。WordPress 上開 JIT 的實測效益通常只有 1% 到 5%,卻要付出額外的 JIT 緩衝區記憶體,偶爾還會在邊角案例引發穩定性問題。

設定上維持 opcache.jit=disableopcache.jit_buffer_size=0 即可。這裡有個版本差異要留意:PHP 8.3 及更早,opcache.jit 預設是 tracing、緩衝區預設 0;PHP 8.4 起預設改成 disable、緩衝區預設 64M。不論哪個版本,想明確關掉就把兩個指令都照上面寫死。真的要實驗 JIT,可以用 opcache.jit=tracing 搭配 128M 緩衝區,但一定要在跟正式環境一樣的真實流量下,比較開啟前後的 TTFB 與 CPU;量不出差別就關回去。

Preload 則是另一個進階選項,效益看場景。PHP 7.4 引進的 opcache.preload 能在 PHP-FPM 啟動時就把指定檔案載進 OPcache,而不是等第一個請求才載,這樣可以消除重啟後「前幾個請求因為快取未命中而比較慢」的暖機期。對 WordPress 來說,預載核心穩定檔案(例如 wp-includes/functions.phpwp-includes/plugin.php 這類)效益最明顯。但要注意一個硬限制,被預載的檔案在不重啟 PHP-FPM 的情況下無法更新,所以預載對象要限定在不常變動的核心檔,不要把會在更新時變動的外掛檔放進去。

至於 opcache.enable_cli,預設是關閉的,因為每次 CLI 執行都是獨立程序、無法共用 bytecode,反而每次都要重配一次記憶體。多數情況維持關閉即可。例外是會反覆執行大量 PHP 檔的長時間 WP-CLI 作業,例如內容匯入、批次處理、快取暖機,這類可以考慮為 CLI 開啟,因為連帶會啟用 bytecode 最佳化器。像 wp option get 這種短指令就不必開,載入 OPcache 的開銷比省下的還多。

哪些設定錯誤會讓 OPcache 完全失效

最容易讓人白忙一場的錯誤,是把 OPcache 的記憶體設定寫進 PHP-FPM 的 pool 設定檔。有人會在 pool 檔裡寫 php_admin_value[opcache.memory_consumption]=256,結果完全沒生效。原因是 OPcache 的記憶體是在 PHP 啟動的當下就配置好的,比 PHP-FPM pool 設定被套用的時間點還早。更麻煩的是 phpinfo() 可能顯示成你改的值,實際配置的記憶體卻還是預設的 128 MB,讓人誤以為設定有效。記憶體相關的 OPcache 設定一律要寫在 php.ini(或 PHP 設定目錄下的 opcache.ini 片段),不要寫在 pool 檔。從 PHP 8.5 起,遇到這種寫法 PHP 會發出警告,比較容易察覺。

第二個常見問題是改了記憶體類設定卻沒重啟。memory_consumptioninterned_strings_buffer 這類記憶體設定只在 PHP 程序啟動時讀取一次,不能動態調大;改完一定要重新載入 PHP-FPM 才會反映新值,否則你看設定檔以為改了,跑起來還是舊的。

第三類是部署造成的舊程式碼問題。當 WordPress 核心或外掛更新後,OPcache 在下一次重新驗證之前,可能繼續供應更新前的舊 bytecode。validate_timestamps=0 時這個狀態是無限期的,revalidate_freq=60 時最多撐 60 秒。穩健的做法是把 OPcache 重置納入更新流程,例如掛進 WordPress 的升級流程,在外掛或核心更新後對 PHP-FPM 送 SIGHUP 優雅重載整個池。共享主機上還可能遇到 OPcache 被設成 opcache.file_cache_only=1 這種以檔案為基礎、且每個使用者各自一份的模式,命中率會偏低、速度也較慢;若無法控制這個模式,至少實測一下開啟與關閉 OPcache 的回應時間差異,確認它真的有幫上忙。

最後是共享主機與自架伺服器的權限差異。在 VPS 或專屬主機上,你能直接透過 php.ini 或獨立的 opcache.ini 控制全部設定;共享主機通常由主機商統一配置,你頂多能透過站台根目錄的 .user.ini 覆寫少數設定,例如 revalidate_freqenable_file_override,而 memory_consumptionmax_accelerated_filesinterned_strings_buffer 這些往往需要伺服器層級的權限才改得動。若用的是 Kinsta、WP Engine、Cloudways 這類代管 WordPress 主機,OPcache 多半已經由主機商配到合理的值,本文這些參數主要是給自己掌控 PHP-FPM 環境的人參考。

從預設值到穩定命中率,調校的節奏怎麼抓

OPcache 是那種設定一次、之後每天默默回本的東西,但它的數值沒有萬用解,會跟你的外掛組合、檔案數與流量型態互動。合理的節奏是:先把六個關鍵指令設到本文建議的起跳值,記憶體類寫進 php.ini 而非 pool 檔,重新載入 PHP-FPM,讓站台在真實流量下跑至少半小時,再回頭讀 opcache_get_status(false)。看到 oom_restarts 就加記憶體,看到 hash_restarts 就加檔案上限,命中率壓在 95% 以下就回去查時間戳與部署流程。一次只動一層、量完再調下一層,比一口氣把所有數字開到最大更可靠。

把 OPcache 擺對位置也很重要,它是整個效能堆疊的底層,要跟 PHP-FPM 工作池、PHP 的 memory_limit、資料庫調校、以及 Redis 或 Memcached 物件快取搭在一起看。先確認 OPcache 沒在掉快取,再往上處理頁面快取與資料庫,效能瓶頸才會一層層被解開。先量測,再依狀態微調,這套節奏比任何單一參數的「最佳值」都更能讓站台穩定地快下去。

相關文章
標籤: WordPress 調校, WooCommerce, PHP 效能, PHP-FPM, OPcache