Nginx失敗重試中的HTTP協議冪等問題: non_idempotent

Nginx通過反向代理做負載均衡時,如果被代理的其中一個服務發生錯誤或者超時的時候,通常希望Nginx自動重試其他的服務,從而實現服務的高可用性。實際上Nginx本身默認會有錯誤重試機制,並且可以通過proxy_next_upstream來自定義配置。

如果不瞭解HTTP協議以及Nginx的機制,就可能在使用過程中遇到各種各樣的坑。例如服務出現了錯誤或超時卻未重試,或者一些例如創建訂單或發送短信這類的HTTP接口,客戶端只發送一次請求,後臺卻由於Nginx重試導致創建了多個訂單,或者收到多條短信,導致一些業務上的問題。

proxy_next_upstream

在Nginx配置文件中,proxy_next_upstream用於指定在什麼情況下Nginx會將請求轉移到其他服務器上。其默認值是proxy_next_upstream error timeout,即發生網絡錯誤以及超時,纔會重試其他服務器。默認情況下服務返回500狀態碼是不會重試的,如果想在響應500狀態碼時也進行重試,可以配置:

proxy_next_upstream error timeout http_500;

當然還有http_502http_503http_404等可以指定在出現哪些狀態碼的情況下需要重試。具體配置項可以參考官方文檔: http://nginx.org/en/docs/http...

用一個最簡單的例子來測試一下該特性,例如下面是Spring Boot寫了一個簡單的HTTP接口,返回500狀態碼:

@SpringBootApplication
public class NginxRetryApplication {

    public static void main(String[] args) {
        SpringApplication.run(NginxRetryApplication.class, args);
    }
}

@RestController
class TestController {

    @RequestMapping("/")
    public String test() {
        System.out.println("收到一個請求"); // 打印日誌
        throw new RuntimeException(); // 拋出異常, 返回500狀態碼
    }
}

分別使用9030和9031兩個端口號啓動該Spring Boot服務,然後Nginx配置好負載均衡:

upstream nginxretry {
    server 127.0.0.1:9030 max_fails=0;
    server 127.0.0.1:9031 max_fails=0;
}
server {
    listen 9039;
    location / {
        proxy_pass http://nginxretry;
        proxy_next_upstream error timeout http_500;
    }
}

注意:以上配置中max_fails=0是爲了更方便的測試Nginx錯誤重試機制。max_fails默認值是1,用於指定一個server在一段時間內(默認10s)發生錯誤次數達到多少次,Nginx就會自動將該服務器下線。這裏設置爲0是禁用這個特性,防止在測試過程中服務器被踢下線不好測試。線上環境下一般不會設置max_fails=0

配置完成後重啓Nginx,使用GET方式請求 http://localhost:9039/ ,再分別查看9030和9031兩個端口號對應的服務日誌,可以發現兩個服務都收到請求,也就是Nginx在訪問其中一個服務收到500錯誤狀態碼後,又嘗試去訪問另一個服務。

再次使用POST方式請求 http://localhost:9039/ ,再分別查看9030和9031兩個端口號對應的服務日誌,可以發現只有一個服務收到請求。也就是當請求類型是POST時,Nginx默認不會失敗重試。如果想讓POST請求也會失敗重試,可以繼續向下閱讀。

non_idempotent

Nginx文檔中可以看到proxy_next_upstream有一個選項non_idempotent:

normally, requests with a non-idempotent method (POST, LOCK, PATCH) are not passed to the next server if a request has been sent to an upstream server (1.9.13); enabling this option explicitly allows retrying such requests;

通常情況下,如果請求使用非等冪方法(POST、LOCK、PATCH),請求失敗後不會再到其他服務器進行重試。加上non_idempotent選項後,即使是非冪等請求類型(例如POST請求),發生錯誤後也會重試。

如果想讓POST請求也會失敗重試,需要配置non_idempotent

upstream nginxretry {
    server 127.0.0.1:9030 max_fails=0;
    server 127.0.0.1:9031 max_fails=0;
}
server {
    listen 9039;
    location / {
        proxy_pass http://nginxretry;
        proxy_next_upstream error timeout http_500 non_idempotent;
    }
}

重啓Nginx後再次使用POST請求訪問 http://localhost:9039/ ,再分別查看9030和9031兩個端口號對應的服務日誌,可以看到兩個服務都收到請求,也就是POST請求也會重試了。不過實際上在生產環境中,不建議加上non_idempotent選項,具體原因可以繼續往下閱讀。

什麼是冪等方法

HTTP協議規範中,對冪等方法(Idempotent Method)做了以下定義:

A request method is considered "idempotent" if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request.

如果使用該方法的多個相同請求對服務器的預期效果與單個請求的效果相同,則認爲請求方法是冪等的。常見的HTTP請求方法中,GET是冪等的,而POST是非冪等的。如果在回答面試題"GET和POST區別"時能答出這一點,才能說明對HTTP協議有一定的理解。

在做業務開發是如何理解冪等性,舉個最簡單的例子:GET方法一般用於獲取數據,如果獲取的是數據庫數據,對應的是SELECT語句。同樣的SELECT語句執行一次還是多次,都不會影響數據。而POST一般對應INSERT,如果執行多次後,可能會造成數據重複插入的問題。所以不要使用GET方法做一些INSERT操作,在業務開發時要遵循HTTP協議規範。

生產環境中爲什麼不建議加上non_idempotent選項?因爲無論是發生500錯誤還是timeout,服務器上的業務可能都已經執行過了,而重試會導致非冪等方法重複執行,從而導致業務問題,例如一個請求會創建了多個訂單,或者收到多條短信的問題。

參考文檔

關注我

圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章