天天看點

apc php擴充,PHP的apc擴充導緻引入檔案錯誤

最近遇到一個非常奇怪的bug,在主機PHP代碼版本回退的過程中,導緻備機服務不可用。

經過各種複現和文檔查詢,發現是PHP的apc擴充在和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

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目錄下的檔案,這時檔案自然不存在,是以報錯。那麼這是為什麼呢?

原因是我們使用了PHP的apc擴充。

PHP的服務過程

apc php擴充,PHP的apc擴充導緻引入檔案錯誤

學習過計算機原理的同學,都了解語言分為編譯型語言和解釋型語言,由于語言是人來編寫的,而機器無法直接執行,是以,在代碼被執行前需要經曆一個編譯成機器可以識别的操作碼的過程。

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

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

Nginx轉發請求給PHP主程序

主程序引入代碼檔案

PHP解釋器會先将代碼切分為Token

生成抽象文法樹

生成機器可以直接執行的操作碼

PHP虛拟機執行操作碼

如果檔案有引入其他檔案,循環執行上述2-6步驟

執行完成,傳回結果

可以看到每次請求過來,都會對檔案做一次編譯和緩存,那麼這樣會非常影響效率,為了保證PHP的靈活性,同時提升效率,我們需要對編譯好的操作碼進行緩存。這就是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擴充可能會導緻兩個問題:

rsync/svn配合使用時存在無法正确處理檔案緩存的問題

可能讀到殘缺檔案,導緻影響部分人的請求

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

# php.ini

[apc]

# 啟動ctime檢查

stat_ctime=1

# 預設值為2,變大這個值

file_update_protection=5

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

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

PHP代碼中執行apc_clear_cache()

重新開機php-fpm程序

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

apc php擴充,PHP的apc擴充導緻引入檔案錯誤

服務使用了rsync同步

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

apc php擴充,PHP的apc擴充導緻引入檔案錯誤

導緻這個問題的原因探究

具體為什麼在apc擴充跟rsync同時使用會産生這個bug,我沒有看源碼,不太了解,但我做了一些大膽的猜測,下面的内容不夠清楚和正确,希望大家能給我更精确的指導:

apc php擴充,PHP的apc擴充導緻引入檔案錯誤

這裡可以看出檔案是怎麼檢查是否有更新的,而問題也就出現在這一部分,沒有辦法判斷檔案是否被更新,同時正确讀取到緩存的檔案。

參考資料