天天看點

Nginx 失敗重試機制(詳細)

https://zhuanlan.zhihu.com/p/127959800

重試機制解析

Nginx 的失敗重試,就是為了實作對用戶端透明的伺服器高可用。然而這部分失敗重試機制比較複雜且官方文檔沒詳細介紹,本文将對其解析,并配合實際場景例子使之更容易被了解。

基礎失敗重試

這部分介紹最常見、最基礎的失敗重試場景。

為了友善了解,使用了以下配置進行分析(

proxy_next_upstream

 沒有特殊配置):

upstream test {
    server 127.0.0.1:8001 fail_timeout=60s max_fails=2; # Server A
    server 127.0.0.1:8002 fail_timeout=60s max_fails=2; # Server B
} 
           

模拟後端異常的方式是直接将對應服務關閉,造成 connect refused 的情況,對應 

error

 錯誤。

在最初始階段,所有伺服器都正常,請求會按照輪詢方式依次轉發給 AB 兩個 Server 處理。假設這時 A 節點服務崩潰,端口不通,則會出現這種情況:

  1. 請求 1 轉到 A 異常,再重試到 B 正常處理,A fails +1
  2. 請求 2 轉到 B 正常處理
  3. 請求 3 轉到 A 異常,再重試到 B 正常處理,A fails +1 達到 max_fails 将被屏蔽 60s
  4. 屏蔽 A 的期間請求都隻轉給 B 處理,直到屏蔽到期後将 A 恢複重新加入存活清單,再按照這個邏輯執行

如果在 A 的屏蔽期還沒結束時,B 節點的服務也崩潰,端口不通,則會出現:

  1. 請求 1 轉到 B 異常,此時所有線上節點異常,會出現:
  • AB 節點一次性恢複,都重新加入存活清單
  • 請求轉到 A 處理異常,再轉到 B 處理異常
  • 觸發 no live upstreams 報錯,傳回 502 錯誤
  • 所有節點再次一次性恢複,加入存活清單
  • 請求 2 依次經過 AB 均無法正常處理, 觸發 

    no live upstreams

     報錯,傳回 502 錯誤

重試限制方式

預設配置是沒有做重試機制進行限制的,也就是會盡可能去重試直至失敗。

Nginx 提供了以下兩個參數來控制重試次數以及重試逾時時間:

  • proxy_next_upstream_tries

    :設定重試次數,預設   表示無限制,該參數包含所有請求 upstream server 的次數,包括第一次後之後所有重試之和;
  • proxy_next_upstream_timeout

    :設定重試最大逾時時間,預設   表示不限制,該參數指的是第一次連接配接時間加上後續重試連接配接時間,不包含連接配接上節點之後的處理時間

為了友善了解,使用以下配置進行說明(隻列出關鍵配置):

proxy_connect_timeout 3s;
proxy_next_upstream_timeout 6s;
proxy_next_upstream_tries 3;

upstream test {
    server 127.0.0.1:8001 fail_timeout=60s max_fails=2; # Server A
    server 127.0.0.1:8002 fail_timeout=60s max_fails=2; # Server B
    server 127.0.0.1:8003 fail_timeout=60s max_fails=2; # Server C
}
           

第 2~3 行表示在 6 秒内允許重試 3 次,隻要超過其中任意一個設定,Nginx 會結束重試并傳回用戶端響應(可能是錯誤碼)。我們通過 iptables DROP 掉對 8001、8002 端口的請求來模拟 connect timeout 的情況:

iptables -I INPUT  -p tcp -m tcp --dport 8001 -j DROP
iptables -I INPUT  -p tcp -m tcp --dport 8002 -j DROP
           

則具體的請求處理情況如下:

  1. 請求 1 到達 Nginx,按照以下邏輯處理
  • 先轉到 A 處理,3s 後連接配接逾時,A fails +1
  • 重試到 B 處理,3s 後連接配接逾時,B fails +1
  • 到達設定的 6s 重試逾時,直接傳回 `504 Gateway Time-out` 到用戶端,不會重試到 C
  • 請求 2 轉到 C 正常處理
  • 請求 3 到達 Nginx
  • 先轉到 B 處理,3s 後連接配接逾時,B 達到 max_fails 将被屏蔽 60s
  • 轉到 C 正常處理
  • 請求 4 達到 Nginx:
  • 先轉到 A 處理,3s 後連接配接逾時,A 達到 max_fails 将被屏蔽 60s
  • 轉到 C 正常處理
  • 後續的請求将全部轉到 C 處理直到 AB 屏蔽到期後重新加入伺服器存活清單

從上面的例子,可以看出 

proxy_next_upstream_timeout

 配置項對重試機制的限制,重試次數的情況也是類似,這裡就不展開細講了。

關于 backup 伺服器

Nginx 支援設定備用節點,當所有線上節點都異常時啟用備用節點,同時備用節點也會影響到失敗重試的邏輯,是以單獨列出來介紹。

upstream 的配置中,可以通過 

backup

 指令來定義備用伺服器,其含義如下:

  1. 正常情況下,請求不會轉到到 backup 伺服器,包括失敗重試的場景
  2. 當所有正常節點全部不可用時,backup 伺服器生效,開始處理請求
  3. 一旦有正常節點恢複,就使用已經恢複的正常節點
  4. backup 伺服器生效期間,不會存在所有正常節點一次性恢複的邏輯
  5. 如果全部 backup 伺服器也異常,則會将所有節點一次性恢複,加入存活清單
  6. 如果全部節點(包括 backup)都異常了,則 Nginx 傳回 502 錯誤

為了友善了解,使用了以下配置進行說明:

upstream test {
    server 127.0.0.1:8001 fail_timeout=60s max_fails=2; # Server A
    server 127.0.0.1:8002 fail_timeout=60s max_fails=2; # Server B
    server 127.0.0.1:8003 backup; # Server C
} 
           

在最初始階段,所有伺服器都正常,請求會按照輪詢方式依次轉發給 AB 兩個節點處理。當隻有 A 異常的情況下,與上文沒有 backup 伺服器場景處理方式一緻,這裡就不重複介紹了。

假設在 A 的屏蔽期還沒結束時,B 節點的服務也崩潰,端口不通,則會出現:

  1. 請求 1 轉到 B 處理,異常,此時所有線上節點異常,會出現:
  • AB 節點一次性恢複,都重新加入存活清單
  • 請求轉到 A 處理異常,再重試到 B 處理異常,兩者 fails 都 +1
  • 因 AB 都異常,啟用 backup 節點正常處理,并且 AB 節點一次性恢複,加入存活清單
  • 請求 2 再依次經過 A、B 節點異常,轉到 backup 處理,兩者 fails 都達到 max_fails:
  • AB 節點都将會被屏蔽 60s,并且不會一次性恢複
  • backup 節點正式生效,接下來所有請求直接轉到 backup 處理
  • 直到 AB 節點的屏蔽到期後,重新加入存活清單

假設 AB 的屏蔽期都還沒結束時,C 節點的服務也崩潰,端口不通,則會出現

  1. 請求 1 轉到 C 異常,此時所有節點(包括 backup)都異常,會出現:
  • ABC 三個節點一次性恢複,加入存活清單
  • 請求轉到 A 處理異常,重試到 B 處理異常,最後重試到 C 處理異常
  • 觸發 `no live upstreams` 報錯,傳回 502 錯誤
  • 所有節點再次一次性恢複,加入存活清單
  • 請求 2 依次經過 AB 節點異常,重試到 C 異常,最終結果如上個步驟,傳回 502 錯誤

踩坑集錦

如果不熟悉 HTTP 協定,以及 Nginx 的重試機制,很可能在使用過程中踩了各種各樣的坑:

  • 部分上遊伺服器出現異常卻沒有重試
  • 一些訂單建立接口,用戶端隻發了一次請求,背景卻建立了多個訂單,等等…

以下整理了一些常見的坑,以及應對政策。

需要重試卻沒有生效

接口的 POST 請求允許重試,但實際使用中卻沒有出現重試,直接報錯。

從 1.9.13 版本,Nginx 不再會對一個非幂等的請求進行重試。如有需要,必須在 

proxy_next_upstream

 配置項中顯式指定 

non_idempotent

 配置。參考 RFC-2616 的定義:

  • 幂等 HTTP 方法:GET、HEAD、PUT、DELETE、OPTIONS、TRACE
  • 非幂等 HTTP 方法:POST、LOCK、PATCH

如需要允許非幂等請求重試,配置參考如下(追加 

non_idemponent

 參數項):

proxy_next_upstream error timeout non_idemponent;
           

該配置需要注意的點:

  1. 添加非幂等請求重試是追加參數值,不要把原來預設的 error/timeout 參數值去掉
  2. 必須明确自己的業務允許非幂等請求重試以避免業務異常

禁止重試的場景

一些場景不希望請求在多個上遊進行重試,即使上遊伺服器完全挂掉。

正常情況下,Nginx 會對 error、timeout 的失敗進行重試,對應預設配置如下:

proxy_next_upstream error timeout;
           

如希望完全禁止重試,需要顯式指定配置來關閉重試機制,配置如下:

proxy_next_upstream off;
           

重試導緻性能問題

錯誤配置了重試參數導緻 Nginx 代理性能出現異常

預設的 error/timeout 是不會出現這種問題的。在定義重試場景時,需要結合業務情況來确定是否啟用自定義錯誤重試,而不是單純去複制其他服務的配置。比如對于某個業務,沒有明确自己業務情況,去網上複制了 Nginx 配置,其中包括了:

proxy_next_upstream error timeout invalid_header http_500 http_503 http_404;
           

那麼隻需要随便定義一個不存在的 URI 去通路該服務頻繁去請求該服務,就可以重複觸發 Nginx 

no live upstreams

 報錯,這在業務高峰情況下,性能将受到極大影響。同樣,如果使用的代碼架構存在不标準 HTTP 處理響應情況,惡意構造的請求同樣也會造成類似效果。

是以在配置重試機制時,必須先對業務的實際情況進行分析,嚴謹選擇重試場景。

異常的響應逾時重試

某幂等接口處理請求耗時較長,出現非預期的重試導緻一個請求被多次響應處理。

假設該接口處理請求平均需要 30s,而對應的代理逾時為:

proxy_read_timeout 30s;
           

預設的重試包含了 timeout 場景,在這個場景下,可能會有不到一半的請求出現逾時情況,同時又因為是幂等請求,所有會進行重試,最終導緻一個的逾時請求會被發到所有節點處理的請求放大情況。

是以在進行逾時設定時,也必須要跟進業務實際情況來調整。可以适當調大逾時設定,并收集請求相關耗時情況進行統計分析來确定合理的逾時時間。

異常的連接配接逾時重試

因上遊伺服器異常導緻連接配接問題,用戶端無逾時機制,導緻請求耗時非常久之後才失敗。

已知所有上遊伺服器異常,無法連接配接或需要非常久(超過 10s)才能連接配接上,假設配置了連接配接逾時為:

proxy_connect_timeout 10;
           

在這種情況下,因用戶端無逾時設定,幂等請求将卡住 10*n 秒後逾時(n 為上遊伺服器數量)。是以建議:

  1. 用戶端設定請求逾時時間
  2. 配置合理的 

    proxy_connect_timeout

  3. 配合 

    proxy_next_upstream_timeout

    proxy_next_upstream_tries

     來避免重試導緻更長逾時

繼續閱讀