Nginx是一個免費開源且穩定高效的Web伺服器程式,擁有反向代理以及負載平衡的功能,經常作為最前端的伺服器。PHP是一種適合用於網站開發的腳本式程式語言,彈性度高,可以直接被嵌入HTML文件中。Nginx可以透過FastCGI去執行PHP程式,且內建FastCGI快取功能。
如果您不知道如何在Ubuntu Server架設Nginx伺服器,可以先參考這篇文章:
CGI是什麼?
CGI(Common Gateway Interface)是一個用於網頁伺服器的介面標準,支援CGI的網頁伺服器會將其所接收到的HTTP請求的內容(路徑、標頭、主體等)作為執行某支程式時的用環境變數以及從標準輸入(stdin)輸入的資料,而該程式輸出到標準輸出(stdout)的資料會被網頁伺服器拿來回應給客戶端。網頁伺服器每次使用CGI執行程式時,都會建立出新的行程(process),其實就像是我們在終端機直接用檔案路徑去執行某支程式那樣,可想而知,這樣的方式在應付多個HTTP請求時是很沒有效率的。
FastCGI是什麼?
FastCGI就是在網頁伺服器和用CGI所執行的程式(以下簡稱CGI程式)之間再加一個管理員。網頁伺服器要把HTTP請求交給管理員處理,而不是直接用CGI去執行程式。管理員會負責分配CGI程式的執行資源,使系統不會每次遇到請求就開一個行程去跑指定的CGI程式,以此來改善網路服務的運作效率。
PHP FastCGI
PHP FastCGI(php-fastcgi)是早期PHP的FastCGI實作。
FPM(FastCGI Process Manager)
FPM(php-fpm)是PHP目前的FastCGI實作。
安裝PHP
由於PHP是腳本式程式語言,因此需要有PHP的執行環境才可以被執行。執行以下指令可以安裝PHP的執行環境:
安裝完php-cli
套件後,就可以使用php
指令來執行PHP程式碼。如下圖:
輸入以下指令可以查看PHP的版本:
使用php
指令執行PHP程式,其設定檔的路徑是/etc/php/<PHP版本號碼>/cli/php.ini
。例如PHP 7.4,設定檔的路徑就是/etc/php/7.4/cli/php.ini
。有關於php.ini
檔案,會在這篇文章之後再提到。
安裝FPM
執行以下指令可以安裝FPM:
FPM預設只會監聽Unix Domain Socket(簡稱UDS,或稱IPC Socket),UDS檔案路徑為/run/php/php<PHP版本號碼>-fpm.sock
。例如PHP 7.4,UDS檔案路徑就是/run/php/php7.4-fpm.sock
。
如果我們要確認FPM有沒有安裝並啟用成功,可以利用socat
這個指令工具來進行。使用以下指令可以安裝socat
:
執行以下指令來判斷FPM有沒有正常工作:
echo /dev/null | sudo socat unix:/var/run/php/php-fpm.sock - && echo "Working!" || echo "Not working!"
以上的/var/run/php/php-fpm.sock
是/run/php/php<PHP版本號碼>-fpm.sock
的符號連結。
設定FPM
FPM的設定檔放置於/etc/php/<PHP版本號碼>/fpm/
目錄中。例如PHP 7.4,就是在/etc/php/7.4/fpm/
目錄中。
FPM的設定檔目錄中還有一個pool.d
目錄,用來放置每個「池」(Pool)的設定檔,一個池就是一個FastCGI的管理區域(或者說管理員)。預設有一個www
池,設定檔檔名為www.conf
,它預設的模樣長成這樣:
比較重要的設定項目如下:
user
、group
:設定這個池在執行CGI程式的時候,行程是屬於哪個擁有者和群組的。預設為www-data
,與Nginx預設的相同。listen
:設定這個池從哪個地方接收請求。預設是/run/php/php<PHP版本號碼>-fpm.sock
。如果FastCGI服務是跑在與Nginx伺服器不同的作業系統上,那這個設定就要改成網路介面的IP位址和連接埠,例如0.0.0.0:9000
。listen.owner
、listen.group
:設定當監聽UDS檔案時,要用哪個擁有者和群組來建立UDS檔案。不設定的話就會是照user
和group
的設定。listen.mode
:設定當監聽UDS檔案時,建立出來的UDS檔案的檔案權限。不設定的話是0660
。php_admin_value[error_log]
:設定錯誤日誌的儲存路徑。預設的儲存路徑是/var/log/php<PHP版本號碼>-fpm.log
。如果一個FastCGI服務提供了多個PHP網站,可以透過區分FPM池來順便區分出錯誤日誌檔(讓每個PHP網站都擁有一個獨立的錯誤日誌檔),這樣日後會比較好維護。不過設定了另外的錯誤日誌路徑後,記得也要去設定logrotate
,讓新的PHP錯誤日誌檔案也可以正常被「輪替(rotate)」。catch_workers_output
:設定是否要取得子行程輸出的訊息至錯誤日誌中。不設定的話是no
。建議先設定為yes
,啟用這個功能,讓PHP程式執行的時候出現的訊息都可以被放在錯誤日誌中。pm
:設定子行程的數量要固定還是浮動。static
是固定為最大數量(pm.max_children
);dynamic
是動態調整;ondemand
是行程開完就關。預設是dynamic
。pm.max_children
:設定子行程的最大數量。預設是5
。這個數量可以設為處理器(邏輯處理器)的數量再根據每個子行程的記憶體用量乘上某個倍數(例如記憶體剩得少就乘2,記憶體剩得多就乘4)。pm.start_servers
:設定當子行程數量是動態調整時,預先建立的子行程數量。pm.min_spare_servers
:設定當子行程數量是動態調整時,最少保留的閒置子行程的數量。pm.max_spare_servers
:設定當子行程數量是動態調整時,最大保留的閒置子行程的數量。pm.process_idle_timeout
:設定當子行程是開完就關時,子行程是要閒置多久(秒)後才會被關閉。pm.max_requests
:設定當子行程處理完幾次請求後,就重新建立。當有用到有記憶體洩漏(Memory Leak)的PHP模組時,可以設定這個項目。pm.status_path
:設定用來查看這個池的狀態的請求路徑。建議設定這個項目,挺方便的,例如設成pm.status_path = /status
,這樣只要發送路徑為/status
的請求就可以知道這個池的狀態(文章之後會再說明)。
套用FPM的新設定
改寫FPM設定檔並存檔後,新設定並不會立即套用給正在運行中的FPM服務。
要讓FPM服務重新讀取設定檔,最好先用以下指令檢查FPM設定檔是否無誤,避免FPM服務不會因為設定檔錯誤而中斷服務。
例如PHP 7.4,指令如下:
接著再執行如的下指令來重新載入FPM的設定檔:
例如PHP 7.4,指令如下:
如果有將FPM監聽的目標改為IP位址和連接埠的話,可以用以下指令來檢查是否有正常在監聽:
ss
指令可以顯示出Socket相關的資訊。-l
參數可以只顯示正在監聽中的連線。若使用ss
指令時都沒給任何參數的話,會忽略掉監聽中的連線。-t
參數可以只顯示TCP連線。-n
參數可以讓連接埠數字直接被輸出,而不是用一個名稱代替。-p
參數可以顯示佔用連線的行程。
設定PHP
透過FPM執行PHP程式的PHP設定檔放在FPM的設定檔目錄中,檔名為php.ini
。例如PHP 7.4,PHP設定檔路徑就是/etc/php/7.4/fpm/php.ini
。它預設的模樣長成這樣:
比較重要的設定項目如下:
display_errors
:設定是否將PHP的警告和錯誤訊息直接輸出到標準輸出(stdout)或是標準錯誤(stderr)。預設是Off
(不輸出)。設定為On
可以輸出到標準輸出(stdout);設定為stderr
可以輸出到標準錯誤。這個功能在PHP程式的開發階段還蠻方便的。default_mimetype
:設定PHP程式輸出的內容之預設的MIME類型。預設是text/html
。max_execution_time
:設定每個PHP程式的最長執行時間(秒)。預設是30
。max_input_time
:設定每個PHP程式的最長讀取請求的時間(秒)。預設是60
。memory_limit
:設定每個PHP程式的最大記憶體使用量(位元組,可用K
、M
等單位)。預設是128M
。upload_max_filesize
:設定每個上傳的檔案的最大大小(位元組,可用K
、M
等單位)。預設是2M
。post_max_size
:設定每個POST請求主體的最大大小(位元組,可用K
、M
等單位)。預設是2M
。可以把這個看作是一次上傳的所有檔案的最大總大小。opcache.enable
:設定是否開啟Zend OPCache。預設是1
(開啟)。設成0
可以關閉,PHP程式還在開發階段時將Zend OPCache關閉會比較方便。opcache.memory_consumption
:設定Zend OPCache使用的共享記憶體大小(MB)。預設是128
。
套用PHP的新設定
改寫PHP設定檔並存檔後,新設定並不會立即套用給正在運行中的FPM服務。
要讓FPM服務重新讀取PHP設定檔,最好先用如下的指令檢查PHP設定檔是否無誤,避免FPM服務不會因為設定檔錯誤而中斷服務或PHP程式執行異常。
例如PHP 7.4,指令如下:
接著再執行如的下指令來重新啟動FPM:
例如PHP 7.4,指令如下:
設定Nginx
PHP的相關設定都搞定後,再來總算要來處理Nginx的部份啦!
在Nginx設定檔的location
區塊中,可以使用fastcgi_pass
命令,將PHP請求交給FastCGI服務來處理。fastcgi_pass
命令的用法類似proxy_pass
命令,如下:
server {
index index.html index.php;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
include fastcgi.conf;
}
}
以上設定,可以讓路徑為.php
結尾的請求,透過FastCGI服務來處理。
這邊有個十分令人疑惑的地方,那就是/etc/nginx
目錄下其實有兩個預設的FastCGI設定檔,第一個是上面用的fastcgi.conf
,第二個是fastcgi_params
,這兩個檔案只差在前者比後者多了:
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
所以引用fastcgi.conf
,表示要把PHP檔案放在Nginx虛擬主機的根目錄底下。而引用fastcgi_params
,表示要自行另外決定PHP檔案的位置,例如:
server {
root /var/www/html;
index index.html index.php;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
fastcgi_param SCRIPT_FILENAME /var/www/php$fastcgi_script_name;
include fastcgi_params;
}
}
如果FPM有啟用pm.status_path
設定項目,而設定值是/status
的話,可以再加上如下的location
區塊。
server {
index index.html index.php;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
include fastcgi.conf;
}
location = /status {
allow 127.0.0.1;
deny all;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
include fastcgi.conf;
}
}
如此一來就可以在同一台主機透過HTTP協定發送路徑為/status
的請求來取得FPM服務的狀態。如下圖:
如果要在別台主機查看FPM服務的狀態,可以加上更多的allow
命令,或者乾脆改用HTTP基本認證(帳密認證)。
關於UDS檔案的URI,可以在server
區塊內使用set
命令,將URI存進一個變數內,這樣在使用多次fastcgi_pass
命令時也不用一直去重複填寫URI。如下:
server {
index index.html index.php;
set $php_uds unix:/run/php/php7.4-fpm.sock;
location ~ \.php$ {
fastcgi_pass $php_uds;
include fastcgi.conf;
}
location = /status {
allow 127.0.0.1;
deny all;
fastcgi_pass $php_uds;
include fastcgi.conf;
}
}
另外有些特定的PHP程式可能會需要大量的執行時間,在PHP腳本中,可以呼叫set_time_limit
函數來單獨改變該PHP腳本的執行時間。當然,如果要讓該PHP腳本也能夠正常把結果回傳給Nginx,就要在Nginx設定檔中使用fastcgi_read_timeout
和fastcgi_send_timeout
命令來提高PHP程式讀取和回應資料的間隔之逾時的時間。fastcgi_read_timeout
和fastcgi_send_timeout
命令可以被用在http
、server
和location
區塊中,不設定的話是60s
(60秒)。
設定FastCGI快取
Nginx在接收FastCGI服務回應的資料時,可以將其快取成檔案,這樣下次如果又有一樣的請求,Nginx就可以直接從檔案系統中撈出來回應,不必再轉送給FastCGI服務處理。
在http
區塊中使用fastcgi_cache_path
命令,可以建立檔案快取存放的目錄,以及用以儲存鍵值(Key)的「Zone」。例如:
http {
fastcgi_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
...
}
以上設定,可以建立出一個名為my_cache
的「Zone」,擁有10MiB的大小的共享記憶體空間,用以儲存被快取的資源的雜湊值以及請求次數。而被快取的資源則會儲存在檔案系統中的/path/to/cache
目錄底下,另外擁有兩層子目錄。/path/to/cache
目錄不需要事先被建立出來,因為它會在Nginx載入設定檔時被建立,但/path/to
目錄要事先被建立。
levels
最多可以擁有三層,值的格式為x[:y[:z]]
,x
表示第一層子目錄的檔名長度,y
表示第二層子目錄的檔名長度,z
表示第三層子目錄的檔名長度。檔名是從快取的鍵值經過MD5雜湊值的十六進制(HEX)字串中,於尾端開始取固定長度的子字串來使用。舉例來說,若levels
設為1:2
,快取的鍵值經MD5雜湊後的十六進制字串為b7f54b2df7773722d382f4809d65029c
,則這個快取檔案的存放路徑為c/29/b7f54b2df7773722d382f4809d65029c
。
max_size=10g
用來限制/path/to/cache
目錄底下的快取總大小最多只能有10GiB,超過的話就會從最久沒被使用的快取開始刪除。
inactive=60m
用來設定快取在60分鐘內如果都沒被使用,就將其刪除。use_temp_path=off
用來設定要快取的檔案不要事先存到fastcgi_temp_path
再搬進快取(在無開啟FastCGI快取時,FastCGI服務所回應的資料會先被暫存至預設的或是指定的fastcgi_temp_path
,在有啟用快取的情況下,實在沒必要再做這個動作)。
接著還要用fastcgi_cache_key
命令來設定快取用的鍵值,基本的設定方式如下:
http {
fastcgi_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
fastcgi_cache_key $scheme$request_uri;
...
}
在想要啟用FastCGI快取的location
區塊,使用fastcgi_cache
命令,就可以讓Nginx進行FastCGI快取了。fastcgi_cache
命令的參數要傳入一個「Zone」的名稱,例如:
http {
fastcgi_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
fastcgi_cache_key $scheme$request_uri;
server {
index index.html index.php;
location ~ \.php$ {
fastcgi_cache_revalidate on;
fastcgi_cache my_cache;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
include fastcgi.conf;
}
}
}
以上設定另外還啟用了fastcgi_cache_revalidate
,可以讓Nginx在處理過期快取的時候去嘗試向FastCGI服務確認快取的有效性,而不是直接讓FastCGI服務重新處理請求。
還有一個地方要注意的是,這邊用的fastcgi_cache_key
,由於沒有區分是哪個Host,所以在不同的server
區塊下,最好使用不同的「Zone」來快取。另外也不建議用$host
變數來組成fastcgi_cache_key
,因為$host
變數在server
區塊不是因server_name
成功匹配而被選擇到時,$host
變數的值是由請求標頭的Host
欄位來決定的,因此會有被注入(injection)攻擊的可能性。
利用fastcgi_cache_use_stale
命令,當被FastCGI服務無法正常處理請求(有錯誤或是逾時),或是回應了如500 (Internal Server Error)的錯誤狀態碼時,Nginx還可以把上一次快取到但是過期的結果回應給客戶端。例如:
http {
fastcgi_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
fastcgi_cache_key $scheme$request_uri;
server {
index index.html index.php;
location ~ \.php$ {
fastcgi_cache_use_stale error timeout invalid_header http_500 http_503;
fastcgi_cache_revalidate on;
fastcgi_cache my_cache;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
include fastcgi.conf;
}
}
}
fastcgi_cache_use_stale
命令也可以在參數加上updating
,讓快取正在被更新時,也依然去使用過期的快取。而若加了updating
,則應該要再啟用fastcgi_cache_background_update
,讓Nginx可以在背景更新過期的快取。例如:
http {
fastcgi_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
fastcgi_cache_key $scheme$request_uri;
server {
index index.html index.php;
location ~ \.php$ {
fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503;
fastcgi_cache_background_update on;
fastcgi_cache_revalidate on;
fastcgi_cache my_cache;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
include fastcgi.conf;
}
}
}
另外還有fastcgi_cache_min_uses
命令可以設定資源至少要被請求幾次才會真的快取到檔案系統中。不設定的話是1
。增加這個值表示要讓更頻繁被存取的資源更能留在快取中。例如:
http {
fastcgi_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
fastcgi_cache_key $scheme$request_uri;
server {
index index.html index.php;
location ~ \.php$ {
fastcgi_cache_min_uses 3;
fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503;
fastcgi_cache_background_update on;
fastcgi_cache_revalidate on;
fastcgi_cache my_cache;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
include fastcgi.conf;
}
}
}
對於沒有在HTTP標頭內設定快取的回應,我們也可以利用fastcgi_cache_valid
命令來快取它,不過不是很建議這樣做。fastcgi_cache_valid
命令的前幾個參數是HTTP狀態碼,最後一個參數是要快取的時間(秒),即表示要對HTTP狀態碼為前幾個參數的回應做快取。如果只有傳入一個時間給fastcgi_cache_valid
命令,則會套用給200、301和302狀態碼;如果狀態碼是傳入any
,即代表所有狀態碼。例如:
http {
fastcgi_cache_path /path/to/cache levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off;
fastcgi_cache_key $scheme$request_uri;
server {
index index.html index.php;
location ~ \.php$ {
fastcgi_cache_valid 200 60m;
fastcgi_cache_min_uses 3;
fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503;
fastcgi_cache_background_update on;
fastcgi_cache_revalidate on;
fastcgi_cache my_cache;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
include fastcgi.conf;
}
}
}
若想控制啟用快取的時機以及重新整理(refresh)快取的時機,可以利用fastcgi_cache_bypass
和fastcgi_no_cache
命令。這兩個命令都可以傳入多個字串,以空格隔開。若傳入fastcgi_cache_bypass
命令的字串有至少一個不是空字串""
或是"0"
,則Nginx就不會使用快取中的資料來回應,但還是會把新拿到的回應存入快取中;若傳入fastcgi_no_cache
命令的字串有至少一個不是空字串""
或是"0"
,Nginx還是可以使用快取中的資料來回應,但如果需要去向FastCGI服務拿新的回應,該回應就不會被存入快取中。
如果要刪除快取,可以直接刪除檔案系統中,存放快取資料的目錄下的檔案以及目錄。舉例來說,如果想刪除所有快取,可以執行以下指令(請小心使用,有誤刪到其它檔案的危險性):
如果是用HTTP標頭欄位的訊息來辨識不同的客戶端,就可能要改用如下的快取鍵值設定(以Authentication
標頭來舉例):
fastcgi_cache_key $scheme$request_uri$http_authentication;
套用Nginx的新設定
改寫Nginx設定檔並存檔後,新設定並不會立即套用給正在運行中的Nginx伺服器。
要讓Nginx伺服器重新讀取設定檔,最好先用以下指令檢查設定檔是否無誤,避免Nginx伺服器因為設定檔錯誤而中斷服務。
接著再執行以下指令來重新載入Nginx的設定檔: