最近遇到一個非常奇怪的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的服務過程

學習過計算機原理的同學,都了解語言分為編譯型語言和解釋型語言,由于語言是人來編寫的,而機器無法直接執行,是以,在代碼被執行前需要經曆一個編譯成機器可以識别的操作碼的過程。
編譯型語言在執行前提前編譯好,然後釋出;解釋型語言先釋出,在執行時即時編譯。是以我們常說編譯型語言的性能好,主要就是快在這個地方。
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擴充的緩存情況。
服務使用了rsync同步
這次故障的一個關鍵因素是使用了rsync同步,我的服務架構是:
導緻這個問題的原因探究
具體為什麼在apc擴充跟rsync同時使用會産生這個bug,我沒有看源碼,不太了解,但我做了一些大膽的猜測,下面的内容不夠清楚和正确,希望大家能給我更精确的指導:
這裡可以看出檔案是怎麼檢查是否有更新的,而問題也就出現在這一部分,沒有辦法判斷檔案是否被更新,同時正确讀取到緩存的檔案。
參考資料