Nginx是一個免費開源且穩定高效的Web伺服器程式,擁有反向代理以及負載平衡的功能,經常作為最前端的伺服器。PHP是一種適合用於網站開發的腳本式程式語言,彈性度高,可以直接被嵌入HTML文件中。Nginx可以透過FastCGI去執行PHP程式,且內建FastCGI快取功能。



如果您不知道如何在Ubuntu Server架設Nginx伺服器,可以先參考這篇文章:

https://magiclen.org/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的執行環境:

sudo apt install php-cli

ubuntu-server-nginx-php

安裝完php-cli套件後,就可以使用php指令來執行PHP程式碼。如下圖:

ubuntu-server-nginx-php

輸入以下指令可以查看PHP的版本:

php -v

ubuntu-server-nginx-php

使用php指令執行PHP程式,其設定檔的路徑是/etc/php/<PHP版本號碼>/cli/php.ini。例如PHP 7.4,設定檔的路徑就是/etc/php/7.4/cli/php.ini。有關於php.ini檔案,會在這篇文章之後再提到。

安裝FPM

執行以下指令可以安裝FPM:

sudo apt install php-fpm

ubuntu-server-nginx-php

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

sudo apt install 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的符號連結。

ubuntu-server-nginx-php

設定FPM

FPM的設定檔放置於/etc/php/<PHP版本號碼>/fpm/目錄中。例如PHP 7.4,就是在/etc/php/7.4/fpm/目錄中。

FPM的設定檔目錄中還有一個pool.d目錄,用來放置每個「池」(Pool)的設定檔,一個池就是一個FastCGI的管理區域(或者說管理員)。預設有一個www池,設定檔檔名為www.conf,它預設的模樣長成這樣:

ubuntu-server-nginx-php

比較重要的設定項目如下:

  • usergroup:設定這個池在執行CGI程式的時候,行程是屬於哪個擁有者和群組的。預設為www-data,與Nginx預設的相同。
  • listen:設定這個池從哪個地方接收請求。預設是/run/php/php<PHP版本號碼>-fpm.sock。如果FastCGI服務是跑在與Nginx伺服器不同的作業系統上,那這個設定就要改成網路介面的IP位址和連接埠,例如0.0.0.0:9000
  • listen.ownerlisten.group:設定當監聽UDS檔案時,要用哪個擁有者和群組來建立UDS檔案。不設定的話就會是照usergroup的設定。
  • 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服務不會因為設定檔錯誤而中斷服務。

sudo php-fpm<PHP版本號碼> -t

例如PHP 7.4,指令如下:

sudo php-fpm7.4 -t

ubuntu-server-nginx-php

接著再執行如的下指令來重新載入FPM的設定檔:

sudo systemctl reload php<PHP版本號碼>-fpm

例如PHP 7.4,指令如下:

sudo systemctl reload php7.4-fpm

ubuntu-server-nginx-php

如果有將FPM監聽的目標改為IP位址和連接埠的話,可以用以下指令來檢查是否有正常在監聽:

sudo netstat -tlnp | grep php-fpm

netstat指令可以顯示出網路連線、路由表、介面統計、偽裝連線和多播成員的資訊。-l參數可以只顯示正在監聽中的連線。若使用netstat指令時都沒給任何參數的話,會忽略掉監聽中的連線。-t參數可以只顯示TCP連線。-n參數可以讓IP可以直接被輸出,而不需透過DNS反查其對應的網域名稱。-p參數可以顯示佔用連線的行程。

ubuntu-server-nginx-php

設定PHP

透過FPM執行PHP程式的PHP設定檔放在FPM的設定檔目錄中,檔名為php.ini。例如PHP 7.4,PHP設定檔路徑就是/etc/php/7.4/fpm/php.ini。它預設的模樣長成這樣:

ubuntu-server-nginx-php

比較重要的設定項目如下:

  • 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程式的最大記憶體使用量(位元組,可用KM等單位)。預設是128M
  • upload_max_filesize:設定每個上傳的檔案的最大大小(位元組,可用KM等單位)。預設是2M
  • post_max_size:設定每個POST請求主體的最大大小(位元組,可用KM等單位)。預設是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 -e -c /etc/php/<PHP版本號碼>/fpm/php.ini -r 'echo "OK\n";'

例如PHP 7.4,指令如下:

php -e -c /etc/php/7.4/fpm/php.ini -r 'echo "OK\n";'

ubuntu-server-nginx-php

接著再執行如的下指令來重新啟動FPM:

sudo systemctl restart php<PHP版本號碼>-fpm

例如PHP 7.4,指令如下:

sudo systemctl restart php7.4-fpm

ubuntu-server-nginx-php

設定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服務的狀態。如下圖:

ubuntu-server-nginx-php

如果要在別台主機查看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_timeoutfastcgi_send_timeout命令來提高PHP程式讀取和回應資料的間隔之逾時的時間。fastcgi_read_timeoutfastcgi_send_timeout命令可以被用在httpserverlocation區塊中,不設定的話是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_bypassfastcgi_no_cache命令。這兩個命令都可以傳入多個字串,以空格隔開。若傳入fastcgi_cache_bypass命令的字串有至少一個不是空字串""或是"0",則Nginx就不會使用快取中的資料來回應,但還是會把新拿到的回應存入快取中;若傳入fastcgi_no_cache命令的字串有至少一個不是空字串""或是"0",Nginx還是可以使用快取中的資料來回應,但如果需要去向FastCGI服務拿新的回應,該回應就不會被存入快取中。

如果要刪除快取,可以直接刪除檔案系統中,存放快取資料的目錄下的檔案以及目錄。舉例來說,如果想刪除所有快取,可以執行以下指令(請小心使用,有誤刪到其它檔案的危險性):

sudo sh -c 'rm -rf /data/cache/*'

如果是用HTTP標頭欄位的訊息來辨識不同的客戶端,就可能要改用如下的快取鍵值設定(以Authentication標頭來舉例):

fastcgi_cache_key $scheme$request_uri$http_authentication;

套用Nginx的新設定

改寫Nginx設定檔並存檔後,新設定並不會立即套用給正在運行中的Nginx伺服器。

要讓Nginx伺服器重新讀取設定檔,最好先用以下指令檢查設定檔是否無誤,避免Nginx伺服器因為設定檔錯誤而中斷服務。

sudo nginx -t

ubuntu-server-nginx

接著再執行以下指令來重新載入Nginx的設定檔:

sudo nginx -s reload

ubuntu-server-nginx