PHP的apc擴展導致引入文件錯誤

最近遇到一個非常奇怪的bug,在主機PHP代碼版本回退的過程中,導致備機服務不可用。
經過各種復現和文檔查詢,發現是PHPapc擴展在和rsync同時使用時,會導致無法正確的處理緩存文件,最終影響服務。解決方案官方也有提供,加上一行配置:

# php.ini
[apc]
apc.stat_ctime=1

下面我們來說明下這個問題出現的機制。

關鍵點:使用了PHP+apc擴展+rsync主從同步機制

故障表現:引入時找不到文件

平臺服務上線更新後,訪問平臺服務時報錯信息:

Warning: include(Yii.php): failed to open stream: No such file or directory in /home/disk4/htdocs/oss_debug/protected/lib/Yii/framework/YiiBase.php on line 421

Warning: include(): Failed opening 'Yii.php' for inclusion (include_path='.:/home/work/lnmp/weblib/phplib:/home/work/lnmp/lib/php') in /home/disk4/htdocs/oss_debug/protected/lib/Yii/framework/YiiBase.php on line 421

Fatal error: Class 'Yii' not found in /home/disk4/htdocs/oss_debug/index.php on line 42

這裏的提示信息表明,問題出現在YiiBase.php文件中,在421行引入Yii.php時找不到該文件,而這裏的include爲相對路徑,當前的引入路徑爲.:/home/work/lnmp/weblib/phplib:/home/work/lnmp/lib/php,多個引入路徑以:分割,所以這裏會在./,/home/work/lnmp/weblib/phplib,/home/work/lnmp/lib/php三個目錄下查找該文件,分別檢索了一下,發現確實均不存在該文件。

但是在正常的服務下,卻並不會查找該文件。具體爲什麼會去查找該文件,我猜測是先判斷Yii類是否存在,不存在就去引入Yii.php,而Yii類在yii.php文件中有定義,因此猜測是沒有正確引入yii.php導致。

# yii.php
<?php
require(dirname(__FILE__).'/YiiBase.php');
class Yii extends YiiBase
{
}

這個問題沒有深究,因爲最後發現故障跟這個點無關。

復現一個小問題:改變目錄後無法服務

你只需要將你的服務目錄換個名字即可復現,如你當前的服務目錄是/home/work/lnmp/htdocs/oss/,你將它重名爲/home/work/lnmp/htdocs/oss2,這個時候你就會發現服務受到了影響:

# 訪問 domain.com/oss2/index.php
Warning: file_get_contents(/home/work/lnmp/htdocs/oss/version): failed to open stream: No such file or directory in /home/work/lnmp/htdocs/oss/index.php on line 26
Warning: require_once(/home/work/lnmp/htdocs/oss/protected/lib/Yii/framework/yii.php): failed to open stream: No such file or directory in /home/work/lnmp/htdocs/oss/index.php on line 38
Fatal error: require_once(): Failed opening required '/home/work/lnmp/htdocs/oss6/protected/lib/Yii/framework/yii.php' (include_path='.:/home/work/lnmp/weblib/phplib:/home/work/lnmp/lib/php') in /home/work/lnmp/htdocs/oss6/index.php on line 38

可以看到當我們訪問oss2目錄時,程序卻依然在嘗試讀取oss目錄下的文件,這時文件自然不存在,因此報錯。那麼這是爲什麼呢?

原因是我們使用了PHPapc擴展。

PHP的服務過程

圖片描述
學習過計算機原理的同學,都瞭解語言分爲編譯型語言和解釋型語言,由於語言是人來編寫的,而機器無法直接執行,因此,在代碼被執行前需要經歷一個編譯成機器可以識別的操作碼的過程。

編譯型語言在執行前提前編譯好,然後發佈;解釋型語言先發布,在執行時即時編譯。因此我們常說編譯型語言的性能好,主要就是快在這個地方。

PHP屬於解釋型語言,常規的執行流程是:

  1. Nginx轉發請求給PHP主進程
  2. 主進程引入代碼文件
  3. PHP解釋器會先將代碼切分爲Token
  4. 生成抽象語法樹
  5. 生成機器可以直接執行的操作碼
  6. PHP虛擬機執行操作碼
  7. 如果文件有引入其他文件,循環執行上述2-6步驟
  8. 執行完成,返回結果

可以看到每次請求過來,都會對文件做一次編譯和緩存,那麼這樣會非常影響效率,爲了保證PHP的靈活性,同時提升效率,我們需要對編譯好的操作碼進行緩存。這就是apc擴展做的事情:

  1. 判斷文件是否有更新
  2. 如果更新,重新編譯並緩存
  3. 否則,直接讀取緩存的操作碼

apc擴展

apc擴展文檔

Alternative PHP Cache (APC 可選 PHP 緩存) 是一個開放自由的 PHP opcode 緩存。它的目標是提供一個自由、 開放,和健全的框架,用於緩存、優化 PHP 中間代碼。

該擴展也提供了一些內置的方法,可以用於手動設置或清空緩存。
清空緩存的方法:apc_clear_cache()。調用這個方法後可以解決因apc緩存過期文件導致的bug

另外,我們需要關注的幾個配置項:

apc.stat integer
是否啓用腳本更新檢查。 改變這個指令值要非常小心。 默認值 On 表示APC在每次請求腳本時都檢查腳本是否被更新, 如果被更新則自動重新編譯和緩存編譯後的內容。但這樣做對性能有不利影響。 如果設爲 Off 則表示不進行檢查,從而使性能得到大幅提高。 但是爲了使更新的內容生效,你必須重啓Web服務器(譯者注:如果採用cgi/fcgi類似的,需重啓cgi/fcgi進程)。 生產服務器上腳本文件很少更改, 可以通過禁用本選項獲得顯著的性能提升。

這個指令對於include/require的文件同樣有效。但是需要注意的是, 如果你使用的是相對路徑,APC就必須在每一次include/require時都進行檢查以定位文件。 而使用絕對路徑則可以跳過檢查,所以鼓勵你使用絕對路徑進行include/require操作。

apc.stat_ctime integer
驗證ctime(創建時間)可以避免SVN或者rsync帶來的問題,確保自上次緩存統計inode沒有改變。APC通常只檢查mtime(修改時間)。

apc.file_update_protection integer
當你在一個運行中的服務器上修改文件時,你應當執行原子操作。 也就是先寫進一個臨時文件,然後將該文件重命名(mv)到最終的名字。 文本編輯器以及 cp, tar 等程序卻並不是這樣操作的,從而導致有可能緩衝了殘缺的文件。 默認值 2 表示在訪問文件時如果發現修改時間距離訪問時間小於 2 秒則不做緩衝。 那個不幸的訪問者可能得到殘缺的內容,但是這種壞影響卻不會通過緩存擴大化。 如果你能確保所有的更新操作都是原子操作,那麼可以用 0 關閉此特性。 如果你的系統由於大量的IO操作導致更新緩慢,你就需要增大此值。

可以看到,apc擴展可能會導致兩個問題:

  1. rsync/svn配合使用時存在無法正確處理文件緩存的問題
  2. 可能讀到殘缺文件,導致影響部分人的請求

針對這兩個問題,也分別提供瞭解決方案:

# php.ini
[apc]
# 啓動ctime檢查
stat_ctime=1
# 默認值爲2,變大這個值
file_update_protection=5

雖然文檔中有說明,但還是有很多人會遇到這種問題,可以參考:

在遇到這個問題時,除了上面的配置解決問題,還可以:

  1. PHP代碼中執行apc_clear_cache()
  2. 重啓php-fpm進程

另外,我們可以將apc擴展安裝時包含的apc.php文件放到web服務目錄下,就可以可視化的觀察apc擴展的緩存情況。

圖片描述

服務使用了rsync同步

這次故障的一個關鍵因素是使用了rsync同步,我的服務架構是:
圖片描述

導致這個問題的原因探究

具體爲什麼在apc擴展跟rsync同時使用會產生這個bug,我沒有看源碼,不太瞭解,但我做了一些大膽的猜測,下面的內容不夠清楚和正確,希望大家能給我更精確的指導:
圖片描述
這裏可以看出文件是怎麼檢查是否有更新的,而問題也就出現在這一部分,沒有辦法判斷文件是否被更新,同時正確讀取到緩存的文件。

參考資料

  1. PHP手冊 - APC運行時配置:https://www.php.net/manual/zh...
  2. stack overflow - Problems with APC on publish:https://stackoverflow.com/que...
  3. PHP官方issue - apc.include_once_override turn on issue:https://bugs.php.net/bug.php?...
  4. php可選緩存APC:https://www.cnblogs.com/hf805...
  5. 關於上線系統的一些想法 (for php):http://bikong0411.github.io/2...
  6. 如何刷新APC類加載器緩存?:http://cn.voidcc.com/question...
  7. rsync文件同步服務:https://xdays.me/rsync%E6%96%...
  8. APC's Include Once Override breaks install:https://www.drupal.org/projec...
  9. 《PHP 7底層設計和源碼實現》
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章