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 轉到 A 異常,再重試到 B 正常處理,A fails +1
- 請求 2 轉到 B 正常處理
- 請求 3 轉到 A 異常,再重試到 B 正常處理,A fails +1 達到 max_fails 将被屏蔽 60s
- 屏蔽 A 的期間請求都隻轉給 B 處理,直到屏蔽到期後将 A 恢複重新加入存活清單,再按照這個邏輯執行
如果在 A 的屏蔽期還沒結束時,B 節點的服務也崩潰,端口不通,則會出現:
- 請求 1 轉到 B 異常,此時所有線上節點異常,會出現:
- AB 節點一次性恢複,都重新加入存活清單
- 請求轉到 A 處理異常,再轉到 B 處理異常
- 觸發 no live upstreams 報錯,傳回 502 錯誤
- 所有節點再次一次性恢複,加入存活清單
- 請求 2 依次經過 AB 均無法正常處理, 觸發
報錯,傳回 502 錯誤no live upstreams
重試限制方式
預設配置是沒有做重試機制進行限制的,也就是會盡可能去重試直至失敗。
Nginx 提供了以下兩個參數來控制重試次數以及重試逾時時間:
-
:設定重試次數,預設 表示無限制,該參數包含所有請求 upstream server 的次數,包括第一次後之後所有重試之和;proxy_next_upstream_tries
-
:設定重試最大逾時時間,預設 表示不限制,該參數指的是第一次連接配接時間加上後續重試連接配接時間,不包含連接配接上節點之後的處理時間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 到達 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
指令來定義備用伺服器,其含義如下:
- 正常情況下,請求不會轉到到 backup 伺服器,包括失敗重試的場景
- 當所有正常節點全部不可用時,backup 伺服器生效,開始處理請求
- 一旦有正常節點恢複,就使用已經恢複的正常節點
- backup 伺服器生效期間,不會存在所有正常節點一次性恢複的邏輯
- 如果全部 backup 伺服器也異常,則會将所有節點一次性恢複,加入存活清單
- 如果全部節點(包括 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 轉到 B 處理,異常,此時所有線上節點異常,會出現:
- AB 節點一次性恢複,都重新加入存活清單
- 請求轉到 A 處理異常,再重試到 B 處理異常,兩者 fails 都 +1
- 因 AB 都異常,啟用 backup 節點正常處理,并且 AB 節點一次性恢複,加入存活清單
- 請求 2 再依次經過 A、B 節點異常,轉到 backup 處理,兩者 fails 都達到 max_fails:
- AB 節點都将會被屏蔽 60s,并且不會一次性恢複
- backup 節點正式生效,接下來所有請求直接轉到 backup 處理
- 直到 AB 節點的屏蔽到期後,重新加入存活清單
假設 AB 的屏蔽期都還沒結束時,C 節點的服務也崩潰,端口不通,則會出現
- 請求 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;
該配置需要注意的點:
- 添加非幂等請求重試是追加參數值,不要把原來預設的 error/timeout 參數值去掉
- 必須明确自己的業務允許非幂等請求重試以避免業務異常
禁止重試的場景
一些場景不希望請求在多個上遊進行重試,即使上遊伺服器完全挂掉。
正常情況下,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 為上遊伺服器數量)。是以建議:
- 用戶端設定請求逾時時間
- 配置合理的
proxy_connect_timeout
- 配合
、proxy_next_upstream_timeout
來避免重試導緻更長逾時proxy_next_upstream_tries