502bad gateway尋因——php-fpm關鍵引數:max_children和backlog

仙人掌發表於2021-02-08

背景分析

在高併發或者壓測的情況下,網站請求容易出現一種錯誤——502 Bad Gateway。
出現這個錯誤的原因有很多,但在高併發的情況下,主要原因就在於php-cgi的程式數不夠用。
對於一般的站點,簡化版的請求處理流程圖如下——
file
假如流程1的請求數在一瞬間達到一個很高的數值(比如2000),由於nginx的非同步非阻塞架構,能從容應對這麼多請求同時到達,並將這些請求都轉發到了fpm主程式。但fpm並不一定能同時處理這麼多請求,假如fpm只配置了能併發處理1000個請求,那麼就會導致接近1000個請求會被異常返回502 bad gateway。
此時,就需要調整fpm的兩個配置引數——max_children和listen.backlog的值。

max_children——

由於我們開啟的是static模式,所以這個參數列示的就是fpm子程式的數量。這個數量不是越多越好,空閒的子程式太多會增加程式管理的開銷以及上下文切換的開銷。網上推薦的子程式數量計算公式為:

n = M / (m * 1.2)

其中,M 是PHP 能利用的記憶體數量。m 是每個 PHP 子程式平均使用的記憶體數量。
一般來講,我們的m在20Mb到30Mb之間,假設取最大值30Mb,M取8G,那麼可以計算得到子程式數量n約等於228。由於我們是按m取較大值算的,所以一般我們可以設定n為256左右。也就是說,此時,fpm可以同時處理256個請求(這裡的“同時”其實並不是真實的併發,受機器CPU核數的限制,其實真正的併發處理請求個數也只有最高也只能等於CPU核數,但忽略這個影響的話,可以認為機器的fpm可以併發處理256個請求)。

listen.backlog——

假如我們的max_children配置了256,那如果此時併發數達到300,會不會出現40多個請求返回502錯誤呢?實際上不會。
首先TCP 建立連線時要經過 3 次握手,在客戶端向伺服器發起連線時,對於伺服器而言,一個完整的連線建立過程,伺服器會經歷 2 種 TCP 狀態:SYN_ RECEIVED,ESTABELLISHED。對應也會維護兩個佇列:1. 一個存放 SYN 的佇列(半連線佇列);2. 一個存放已經完成連線的佇列(全連線佇列)。當一個連線的狀態是 SYN RECEIVED 時,它會被放在 SYN 佇列中。當它的狀態變為 ESTABLISHED 時,它會被轉移到另一個佇列。所以後端的應用程式只從已完成的連線的佇列中獲取請求。如果一個伺服器要處理大量網路連線,且併發性比較高,那麼這兩個佇列長度就非常重要了。因為,即使伺服器的硬體配置非常高,伺服器端程式效能很好,但是這兩個佇列非常小,那麼經常會出現客戶端連線不上的現象,因為這兩個佇列一旦滿了後,很容易丟包,或者連線被複位。所以,如果伺服器併發訪問量非常高,那麼這兩個佇列的設定就非常重要了。
對於以上說到的例子,實際上這300個請求會先依次到達全連線佇列,然後fpm的子程式再依次去消費這個佇列中的請求。由於這個過程耗時很短,所以幾乎可以認為是此時有256個請求被fpm子程式進行處理,然後剩餘的40多個請求就還是在這個全連線佇列中等待被取出消費。所以此時不會出現502的錯誤返回。
但是如果此時併發請求量不止300,已經超過了(全連線佇列的長度+max_children)的總和,就會有一部分已連線的請求沒辦法被及時處理,此時就會出現502 bad gateway的錯誤。
所以高併發情況下fpm是否會返回502錯誤,除了max_children的大小限制,另外一個限制就是全連線佇列的長度,而這個長度配置就是在fpm.conf裡的listen.backlog這個值。這個值一般預設是-1,也就是沒限制,但其實由於linux系統本身也有一個對應的全連線佇列長度的限制,而且fpm配置的backlog大小不能大於系統配置的大小。所以假設系統配置的全連線佇列大小為128時,當fpm配置裡的值為-1或者是一個大於128的值時,此時backlog的最大值還是隻有128。也就是說,如果我們要修改fpm全連線佇列長度,首先要保證系統的全連線佇列值足夠大。目前還沒發現這個值設定為多少是最合適的,但最好設定為大於1024的值。同時這個值也不是越大越好,因為設定大了之後,雖然可以一定程度上避免出現502錯誤,但由於nginx有響應超時時間的設定,如果fpm處理不過來,nginx那邊等待超時,斷開連線,就會報504 gateway timeout的錯。

優化操作

一、max_children與backlog的合適大小

1、對於8G的機器,max_children取256應該是比較合適的,如果記憶體較大,這個值可以對應的增大一些。
2、至於backlog的值,一般來講,大於1024即可。後續如果要改的話,可以把當前值加倍增大,保持數值是2的指數最好,因為系統最終也會將這個值轉為2的指數倍。

二、如何改max_children大小

1、開啟fpm配置檔案
2、找到pm.max_children這個配置,將等於號後面的值改為需要的值之後,儲存配置。
3、重啟fpm,執行 ps -ef|grep fpm|grep -v master|grep -v grep |wc -l 可以檢視當前fpm子程式的數量,確認是不是已經改過來了

三、如何改backlog大小

1、cat /proc/sys/net/core/somaxconn (先檢視系統的連線佇列長度限制,如果太小,需要先通過下面的步驟23修改系統的這個值,否則就跳過這兩步)    
2、vim /etc/sysctl.conf(在檔案中新增一行net.core.somaxconn=x,x為希望改到的佇列長度值,如果裡面已經有這一行配置,則直接改這行配置的值)
3、sysctl -p(重新載入系統配置)
4、開啟fpm配置檔案
5、找到listen.backlog這個配置,將等於號後面的值改為需要的值。
6、重啟fpm。

四、避免被同一個ip頻繁請求攻擊導致無法正常訪問的優化建議

當大量非法請求通過nginx湧入到fpm時,正常使用者的訪問請求就會因為無法被fpm消費導致返回502或者介面一直處於載入中。所以,為了避免這種情況,最好的方式就是在nginx層就直接拒絕掉這個非法ip的請求,不讓其到達fpm,浪費正常的fpm子程式資源。
Nginx有兩個配置,可以用於限制每一個IP地址在一段時間內的訪問次數:limit_req_zone和limit_req。具體的配置方法如下:

1、在nginx.conf的http模組裡,新增 limit_req_zone$binary_remote_addr zone=allips:10m rate=30r/s;
2、在nginx.conf的server模組裡,新增 limit_req zone=allips burst=5;
3、nginx -s reload 平滑重啟nginx。

這樣,就實現了每個ip每秒最多能發起35個請求的限制,超過的請求,nginx會直接返回503Service Temporarily Unavailable(服務暫時不可用)錯誤。這樣就既保證了fpm不會在短時間內接收到同一個ip的高併發請求,也可以保證正常ip的使用者可以訪問介面。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章