[php] 如何正確發佈 PHP 代碼

如何正確發佈PHP代碼

幾乎每一個 PHP 程序員都發布過代碼,可能是通過 FTP 或者 **rsync ** 同步的,也可能是通過 svn 或者 git 更新的。一個活躍的項目可能每天都要發佈若干次代碼,但是現實卻是很少有人注意其中的細節,實際上這裏面有好多坑,很可能你就在坑中卻渾然不知。

一個正確實現的發佈系統至少應該支持原子發佈。如果說每一個版本都表示一個獨立的狀態的話,那麼在發佈期間,任何一次請求只能在單一狀態下被執行。如此稱之爲支持原子發佈;反之如果在發佈期間,一次請求跨越不同的狀態,那麼就不能稱之爲原子發佈。我們不妨舉個例子來說明一下:假設一次請求需要 include 兩個 PHP 文件,分別是 a.phpb.php,當 include a.php 完成後,發佈代碼,接着 include b.php,如果處理不當的話,那麼就可能會導致舊版本的 a.php 和新版本的 b.php 同時存在於同一個請求之中,換句話說就是沒有實現原子發佈。

開源世界裏有很多不錯的發佈代碼工具,比如 ruby 社區的 capistrano,其流程大致就是發佈代碼到一個全新的目錄,然後再軟鏈接到真正的發佈目錄。

├── current -> releases/v1
└── releases
    ├── v1
    │   ├── foo.php
    │   └── bar.php
    └── v2
        ├── foo.php
        └── bar.php

不過鑑於 PHP 本身的特殊性,如果只是簡單套用上面的流程,那麼將很難實現真正的原子發佈。要理清箇中緣由,還需要了解一下 PHP 中的兩個 Cache 的概念:

  • opcode cache
  • realpath cache

先聊聊 opcode cache,基本就是 apc 或者 zend opcode,關於它的作用,大家都已經很熟悉,不必多言,需要注意的是 apc 的 bug 很多,比如開啓了 apc.enable_cli 配置後就會有很多靈異問題,所以說 opcode cache 還是儘可能使用 zend opcache 吧,如果需要緩存數據,可以用 apcu。此外 apczend opcode 對緩存鍵的選擇有所差異:apc 選擇的是文件的 inodezend opcode 選擇的是文件的 path

再聊聊 realpath cache,它的作用是緩衝獲取文件信息的 IO 操作,大多數時候它對我們而言是透明的,以至於很多人都不知道它的存在,需要注意的是 realpath cache 是進程級別的,也就是說,每一個 php-fpm 進程都有自己獨立的 realpath cache

假設在發佈代碼期間,opcode cache 或者 realpath cache 裏的數據出現過期,那麼就會出現一部分緩存是舊文件,一部分緩存是新文件的非原子發佈的情況,爲了避免出現這種情況,我們應該保證緩存過期時間足夠長,最好是除非我們手動刷新,否則永遠不過期,對應到配置上就是:關閉 apc.statopcache.validate_timestamps 配置,設置足夠大的 realpath_cache_sizerealpath_cache_ttl 配置,必要的監控總是有好處的。

相關的技術細節特別瑣碎,建議大家仔細閱讀如下資料:

在採用軟鏈接發佈代碼的時候,通常遇到的第一個問題多半是新代碼不生效!即便調用了 apc_clear_cache 或者 opcache_reset 方法也無效,重啓 php-fpm 自然是能夠解決問題,不過對腳本語言來說重啓太重了!難道除了重啓就沒有別的辦法了麼?

事實上之所以會出現這樣的問題,主要是因爲 opcode cache 是通過 realpath cache 獲取文件信息,即便軟鏈接已經指向了新位置,但是如果 realpath cache 裏還保存着舊數據的話,opcode cache 依然無法知道新代碼的存在,缺省情況下,realpath_cache_ttl 緩存有效期是兩分鐘,這意味着發佈代碼後,可能要兩分鐘才能生效。爲了讓發佈儘快生效,需要以進程爲單位清除 realpath cache

<?php

	$key = 'php.pid_' . getmypid();
	
	if (($rev = apc_fetch($key)) != DEPLOY_VERSION) {
	    if($rev < DEPLOY_VERSION) {
	        apc_store($key, DEPLOY_VERSION);
	    }
	    
	    clearstatcache(true);
	}

如此在 apc 環境下基本就能工作了,但是在 zend opcode 環境下還可能有問題。因爲在缺省情況下 opcache.revalidate_path 是關閉的,此時會緩存未解析的符號鏈接的值,這會導致即便軟鏈接指向修改了,也無法生效,所以在使用 zend opcode 的時候,如果使用了軟鏈接,視情況可能需要把 opcache.revalidate_path 激活。

詳細介紹參考:PHP’s OPCache extension review

BTW:如果需要手動重置 opcode cache,需要注意的是因爲它是基於 SAPI 的概念,所以不能直接在命令行下調用 apc_clear_cache 或者 opcache_reset 方法來重置緩存,當然辦法總是有的,那就是使用 CacheTool 在命令行下模擬 fastcgi 請求。

分析到這裏,我們不妨反思一下:在 PHP 中原子發佈之所以是一個棘手的問題,歸根結底是因爲軟鏈接和緩存之間的的矛盾。不管是 opcode cache 還是 realpath cache,都是 PHP 固有的緩存特性,基於客觀需要無法繞開,如此說來是否有辦法繞開軟鏈接,使其成爲馬奇諾防線呢?答案是 NGINX$realpath_root

	fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
	fastcgi_param DOCUMENT_ROOT $realpath_root;

有了 $realpath_root,即便 DOCUMENT_ROOT 目錄中含有軟鏈接,NGINX 也會把軟鏈接指向的真正的路徑發給 PHP,也就是說,對 PHP 而言,軟鏈接已經不存在了!不過作爲代價,每一次請求,NGINX 都要通過相對昂貴的 IO 操作獲取 $realpath_root 的值,通過 strace 命令我們能監控這一過程,下圖從 currentfoo 的過程:

realpath

在本例中,壓測發現使用 $realpath_root 後,性能下降了大約 **5% **左右,不過明眼人一下就能發現,雖然 $realpath_root 導致了 lstatreadlink 操作,但是 lstat 操作的次數是和目錄深度成正比的,也就是說目錄越深,執行的 lstat 次數越多,性能下降也就越大。如果能夠降低發佈目錄的深度,那麼可以預計還能降低一些性能損耗。

結尾介紹一下 Deployer,它是 PHP 中做得比較好的工具,有很多特色,比如支持並行發佈,具體演示如下圖,左邊是串行,右邊是並行,使用「vvv」能得到更詳細信息:

deploy

不過 Deployer 在原子發佈上有一點瑕疵,具體見 release/symlink 代碼:

<?php

// deploy:release
run("cd {{deploy_path}} && if [ -h release ]; then rm release; fi");
run("ln -s $releasePath {{deploy_path}}/release");
// deploy:symlink
run("cd {{deploy_path}} && ln -sfn {{release_path}} current");
run("cd {{deploy_path}} && rm release");

?>

release 的時候,它是先刪除再創建,是一個兩步的非原子操作,在 symlink 的時候,看上去「ln -sfn」是單步原子操作,實際上也是錯誤的:

shell> strace ln -sfn releases/foo current
symlink("releases/foo", "current")      = -1 EEXIST (File exists)
unlink("current")                       = 0
symlink("releases/foo", "current")      = 0

通過 strace 我們能清晰的看到,雖然表面上使用「ln -sfn」是一步操作,但是內部依然是按照先刪除再創建的邏輯執行的,實際上這裏應該搭配使用「ln & mv」

shell> ln -sfn releases/foo current.tmp
shell> mv -fT current.tmp current

先通過 ln 創建一個臨時的軟鏈接,再通過 mv 實現原子操作,此時如果使用 strace 監控,會發現 mv「T」 選項實際上僅僅執行了一個 rename 操作,所以是原子的。

BTW:在使用「ln -sfn」前後,如果使用 stat 查看新舊文件的 inode 的話,可能會發現它們擁有一樣的 inode 值,看上去和我們的結論相悖,其實不然,實際上只是複用刪除值而已(如果想驗證,注意 Linux 會複用,Mac 不會複用)。

據說一千個人的心中就有一千個哈姆雷特,不過我希望所有的 PHP 程序員在發佈 PHP 代碼的時候都能採用一種方法,那就是本文介紹的方法,正確的方法。

原文轉自老王的火丁筆記,原文地址:如何正確發佈PHP代碼 ;如有侵權請告知刪除。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章