當前位置:才華齋>計算機>php語言>

如何正確釋出PHP程式碼

php語言 閱讀(3.21W)

幾乎每一個 PHP 程式設計師都發布過程式碼,可能是通過 ftp 或者 rsync 同步的,也可能是通過 svn 或者 git 更新的。一個活躍的專案可能每天都要釋出若干次程式碼,但是現實卻是很少有人注意其中的細節,實際上這裡面有好多坑,很可能你就在坑中卻渾然不知。下面是小編為大家帶來的關於如何正確釋出PHP程式碼的知識,歡迎閱讀。

如何正確釋出PHP程式碼
  如何正確釋出PHP程式碼

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

開源世界裡有很多不錯的釋出程式碼工具,比如 ruby 社群的 capistrano,其流程大致就是釋出程式碼到一個全新的目錄,然後再軟連結到真正的釋出目錄。

.

├── current -> releases/v1

└── releases

├── v1

│ ├──

│ └──

└── v2

├──

└──

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

opcode cache

  realpath cache

先聊聊 opcode cache,基本就是 apc 或者 zend opcode,關於它的作用,大家都已經很熟悉,不必多言,需要注意的是 apc 的 bug 很多,比如開啟了 le_cli 配置後就會有很多靈異問題,所以說 opcode cache 還是儘可能使用 zend opcache 吧,如果需要快取資料,可以用 apcu。此外 apc 和 zend opcode 對快取鍵的選擇有所差異:apc 選擇的是檔案的 inode,zend opcode 選擇的是檔案的 path。

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

假設在釋出程式碼期間,opcode cache 或者 realpath cache 裡的資料出現過期,那麼就會出現一部分快取是舊檔案,一部分快取是新檔案的非原子釋出的情況,為了避免出現這種情況,我們應該保證快取過期時間足夠長,最 好是除非我們手動重新整理,否則永遠不過期,對應到配置上就是:關閉 、date_timestamps 配置,設定足夠大的 realpath_cache_size、realpath_cache_ttl 配置,必要的監控總是有好處的。

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

realpath_cache

PHP’s OPCache extension review

Atomic deploys at Etsy

Cache invalidation for scripts in symlinked folders

在採用軟連結釋出程式碼的時候,通常遇到的第一個問題多半是新程式碼不生效!即便呼叫了 apc_clear_cache 或者 opcache_reset 方法也無效,重啟 php-fpm 自然是能夠解決問題,不過對指令碼語言來說重啟太重了!難道除了重啟就沒有別的辦法了麼?

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

事實上之所以會出現這樣的問題,主要是因為 opcode cache 是通過 realpath cache 獲取檔案資訊,即便軟連結已經指向了新位置,但是如果 realpath cache 裡還儲存著舊資料的話,opcode cache 依然無法知道新程式碼的存在,預設情況下,realpath_cache_ttl 快取有效期是兩分鐘,這意味著釋出程式碼後,可能要兩分鐘才能生效。為了讓釋出儘快生效,需要以程序為單位清除 realpath cache:

<?php

$key = '_' . getmypid();

if (($rev = apc_fetch($key)) != DEPLOY_VERSION) {

if($rev < DEPLOY_VERSION) {

apc_store($key, DEPLOY_VERSION);

}

clearstatcache(true);

}

?>

如此在 apc 環境下基本就能工作了,但是在 zend opcode 環境下還可能有問題。因為在預設情況下 lidate_path 是關閉的,此時會快取符號連結的值,這會導致即便軟連結指向修改了,也永遠無法生效,所以在使用 zend opcode 的'時候,如果使用了軟連結,視情況可能需要把 lidate_path 啟用。

分析到這裡,我們不妨反思一下:在 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 命令我們能監控這一過程,下圖從 current 到 foo 的過程:

 realpath

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

結尾介紹一下 Deployer,它是 PHP 中做得比較好的工具,有很多特色,比如支援並行釋出,具體演示如下圖,左邊是序列,右邊是並行,使用「vvv」能得到更詳細資訊:

 deploy

不過 Deployer 在原子釋出上有一點瑕疵,具體見 deploy:release 程式碼:

<?php

run("cd {{deploy_path}} && if [ -h release ]; then rm release; fi");

run("ln -s $releasePath {{deploy_path}}/release");

?>

也就是說,在切換軟連結的時候,它是先刪除再建立,是一個兩步操作,理論上如果有請求在這兩步中間進入的話,那麼將會出現找不到檔案的錯誤。

問題到這裡,大部分人會覺得使用「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

shell> mv -fT current

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

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