2020年3月16日 星期一

在 httpd 上使用 PHP-FPM

在撰寫本文時,使用的是利用 homebrew 安裝的 PHP 7.4.3 (含內建的 PHP-FPM) 以及 httpd 2.4.1。PHP-FPM 適用於重負載 (Heavy-Loaded) 的網站,對於一般在本機上開發 PHP 的開發者,並不一定需要使用 PHP-FPM。通常 PHP-FPM 會配合對於多執行緒處理較佳的 Nginx,而非 httpd。在閱讀本文前,建議先閱讀 PHP 與伺服器之間的應用程式介面以及在 Apache HTTP Server 的多程序處理模組


基礎常識


PHP-FPM (FastCGI Process Manager) 是 PHP 實作快速共用閘道介面 (Fast Common Gateway Interface, FastCGI) 的管理套件。FastCGI 是一個加強版的 CGI 協定,它將 CGI 應用程式包裝起來,利用 FastCGI 伺服器來建立程序以及管理從網頁伺服器傳送來的請求。它不僅保留了 CGI 的好處,而且還提升了 CGI 的性能,適合應用於重負載的網站。


根據 PHP 手冊,PHP-FPM 具有下列的特性

1. 能夠優雅起動或停止的先進程序管理;
2. 能夠根據不同的使用者 ID (uid) / 群組 ID (gid) / 根目錄更改 (chroot) / 環境 (environment) 等啓動工作者 (Worker) 子程序,監聽不同的通訊埠以及使用不同的 php.ini;
3. 標準串流 stdout 以及 stderr 記錄;
4. 操作碼 (Opcode) 快取意外損壞時緊急重新啟動;
5. 支援加速上傳;
6. 慢速記錄 (slowlog):記錄執行異常緩慢的腳本,包含腳本名稱以及 PHP 回溯 (Backtraces),使用 ptrace 系統呼叫或是類似的機制去讀取遠端程序的執行資料;
7. 提供特殊的 fastcgi_finish_request() 函式,能夠在完成請求以及刷新全部資料時,繼續執行耗時的操作 (例如,影音轉檔、統計處理等);
8. 動態或靜態的子程序生成;
9. 和 Apache 模組狀態類似的基本 SAPI 狀態資訊;
10. 以 php.ini 為基礎的組態檔。


安裝


使用 Homebrew 安裝的 PHP 已經包含了 相同版本的 PHP-FPM。

設定 httpd.conf


PHP-FPM 大多使用在重負載 (Heavy-Loaded) 的網站上,因此建議 httpd 的多程序處理模組 (Multi-Processing Module, MPM) 使用 Event 模組 (停用預設的 Prefork 模組)。需要開啟 httpd.conf 的代理 (Proxy) 模組以及 FastCGI 代理模組,讓 httpd 能夠將請求轉傳到 FastCGI 伺服器處理。httpd.conf 需要進行下列的組態設定
LoadModule mpm_event_module lib/httpd/modules/mod_mpm_event.so
LoadModule proxy_module lib/httpd/modules/mod_proxy.so
LoadModule proxy_fcgi_module lib/httpd/modules/mod_proxy_fcgi.so


設定 PHP 檔案的處理器,讓副檔名為 php 的請求傳送到 PHP-FPM 伺服器:
<IfModule mpm_event_module>
    <FilesMatch \.php$>
        SetHandler "proxy:unix:/usr/local/var/run/php/php-fpm.sock|fcgi://localhost/"
    </FilesMatch>
    AddType text/html .php
</IfModule>


上面的組態設定使用了 FilesMatchsetHandler 兩個指令將 PHP 檔案傳送到指定的 Unix 插座 (Socket)。其中,FilesMatch 透過指定的檔案名稱來限制區塊內指令的適用範圍,它可以使用規則運算式 (Regular Expressions) 來表示檔案名稱;setHandler 指定代理要傳遞請求到那一種插座 (Socket),以及插座的識別碼。


PHP-FPM 和 httpd 之間的通訊可以使用網路插座或是 Unix 插座。Unix 插座是一種程序間的通訊機制,它允許在同一台電腦上的兩個程序可以同時開啟一個 Unix 插座來進行數據交換。它使用檔案的位址作為識別碼 (例如上面的 /usr/local/var/run/php/php-fpm.sock),通訊在系統核心進行處理。網路插座是另一種程序間的通訊機制,它允許兩個程序通過同一網路插座來進行數據交換。它使用網路位址加上通訊埠作為識別碼,通訊經由網路堆疊處理。使用網路插座的兩個程序可以是不同電腦上的程序,也可以是同一個電腦上經由 IP 繞回 (Loopback) 介面通訊的兩個不同程序。網路插座透過網際網路協定通訊,可以跨電腦通訊,使用上較為靈活但是額外的處理成本較高,Unix 插座則是相反。


上面的組態檔使用的是 Unix 插座。如果要用網路插座,則處理器的設定要改為:
setHandler "proxy:fcgi://127.0.0.1:9000"


其中,127.0.0.1 為本機的繞回位址,如果 PHP-FPM 跟 httpd 在不同的主機,則要改成 PHP-FPM 主機的 IP 位址;9000 為 PHP-FPM 的監聽埠。


除了使用上述的 setHandler 來進行通訊之外,還可以使用 ProxyPassMatch 或是 ProxyPass。除非有特殊的需求,建議使用FilesMatch 和 setHandler 即可。

PHP-FPM 組態檔 php-fpm.conf


php-fpm.conf 設定組態選項的語法跟 php.ini 一樣。預設的路徑前綴為 /usr/local/var,需要注意的組態選項只有 error_log (其他建議使用預設值即可):
error_log = log/php-fpm.log


注意:
1. 這項設定僅會記錄 PHP-FPM 本身的錯誤訊息;PHP 腳本錯誤會記錄在 php.ini 的 error_log 或是程序池組態檔的 error_log。
2. 可以變更記錄檔的路徑;如果使用 brew services 來啟動 PHP-FPM,記得要修改 /usr/local/opt/php/homebrew.mxcl.php.plist 中 StandardErrorPath 下的字串值,不然每次 brew services 在重啟 PHP-FPM 時,預設的檔案夾內還是會再產生記錄檔。
3. 當啓動 PHP-FPM 時出現類似下列訊息,代表組態檔裡的錯誤記錄指令沒有設定好,或是啟動的是 macOS 內建的 PHP-FPM:
ERROR: failed to open error_log (/usr/var/log/php-fpm.log): No such file or directory (2)



在啟動 PHP-FPM 時需要至少建立一個主程序 (Master Process),每個主程序則會建立多個稱為工作者 (Worker) 的子程序來回應及處理請求;這些子程序會形成一個 PHP 程序池 (A Pool of PHP Processes)。為了方便管理,PHP-FPM 將建立程序池所使用的組態檔獨立出來。程序池組態檔通常會存放在 php-fpm.d 檔案夾裡。在 php-fpm.conf 的最後一行設定會將組態檔包含進來:
include=/usr/local/etc/php/7.4/php-fpm.d/*.conf


會使用 *.conf 是因為 PHP-FPM 的特性之一是可以有多個程序池,以用來處理不同的請求 (例如,當 httpd 有多個虛擬主機時,每個主機可以有自己的 PHP 程序池)。讓每個程序池有自己的組態檔,可以方便進行組態設定。

程序池組態檔 pool_name.conf


PHP-FPM 會依據程序池組態檔的設定值來建立 PHP 程序池。程序池組態檔的檔案名稱一般為程序池名稱加上 conf 副檔名。例如,在 php-fpm.d 檔案夾裡有一個  www.conf  程序池組態檔,它的程序池名稱就是 www。一般在建立程序池組態檔時都會藉由複製或是修改 www.conf 來完成。程序池組態檔需要設定的組態選項有下列幾項:
[userName]

user = userName
group = userGruop
listen = /usr/local/var/run/php/php-fpm.sock
php_admin_value[error_log] = /usr/local/var/log/php-fpm/$pool.error.log


首先,如果要根據不同的 uid 跟 gid 建立程序池,可以利用使用者名稱做為程序池的名稱。監聽 (listen) 的設定需要和 httpd.conf 配合,上例為使用 unix 插座時的檔案位置。如果是使用網路插座,則要更改為下列的設定:
listen = 127.0.0.1:9000


每個程序池可以有自己的錯誤記錄檔。在啟用程序池的錯誤記錄檔前,需要先啟動 log_errors 指令,php.ini 的 error_log 的組態設定將會被取代。最後,更改完後記得將檔名更改為 userName.conf。


在程序池設定檔中有 listen.* 指令用來設定監聽的權限,因為 macOS 是 BSD 衍生系統,所以不用進行設定。另外,程序管理員 (pm) 的相關組態選項,則需要依照使用情況進行調整,不在本文的範圍內。


PHP-FPM 的特性之一是能夠根據不同的 uid/gid/chroot/environment 等啓動工作者 (Worker) 子程序,監聽不同的通訊埠以及使用不同的 php.ini。要讓 PHP-FPM 同時執行多個 PHP 程序池,可以透過多個的程序池組態檔進行設定。


啓動 PHP-FPM


在使用 Homebrew 安裝完 PHP後,因為是伺服器的關係,會在 /usr/local/sbin 檔案夾建立 php-fpm 的符號連結 (Symbolic Link)。/usr/local/sbin 一般並不在 PATH 環境變數中,再加上 macOS 也有內建 php-fpm,如果直接執行 php-fpm 會出現下列的錯誤:
ERROR: failed to open configuration file '/private/etc/php-fpm.conf


透過下列的指令,可以知道執行的並不是 /usr/local/sbin/php-fpm
which php-fpm


因此執行 PHP-FPM 時,需要加上完整的路徑:
/usr/local/sbin/php-fpm


因為 PHP-FPM 預設會在前景執行 (使用的是 nodaemonize 模式),要停止 PHP-FPM 只要在終端機按下 control + c 即可。


比較方便的做法是使用 brew services,將 PHP-FPM 設為登入後自動執行的任務項:
brew services start php


當修改過相關的組態檔,需要重新啓動 PHP-FPM 時,只需要執行:
brew services restart php


沒有留言: