避免 10 大 NGINX 配置錯誤(下)

語言: CN / TW / HK

原文作者:Timo Stark of F5Sergey Budnevich of F5

原文連結:避免 10 大 NGINX 配置錯誤

轉載來源:NGINX 官方網站


在幫助 NGINX 使用者解決問題時,我們經常會發現配置錯誤,這種配置錯誤也屢屢出現在其他使用者的配置中,甚至有時還會出現在我們的 NGINX 工程師同事編寫的配置中!本文介紹了 10 個最常見的錯誤,並解釋了問題所在以及相應的解決方法。

  1. 每個 worker 的檔案描述符不足
  2. error_log off 指令
  3. 未啟用與上游伺服器的 keepalive 連線
  4. 忘記指令繼承的工作機制
  5. proxy_buffering off 指令
  6. if 指令使用不當
  7. 過多的健康檢查
  8. 不安全地訪問指標
  9. 當所有流量都來自同一個 /24 CIDR 塊時使用 ip_hash
  10. 不採用上游組

錯誤 6:if 指令使用不當

if 指令使用起來很棘手,尤其是在 location{}塊中。它通常不會按照預期執行,甚至還會導致出現段錯誤。

通常,在 if{} 塊中,您可以一直安全使用的指令只有 return  rewrite。以下示例使用 if 來檢測包含 X‑Test http訊息頭的請求(可以是您想要測試的任何條件)。NGINX 返回 430 (Request Header Fields Too Large) 錯誤,在指定的位置 @error_430 進行攔截並將請求代理到名為 b 的上游 group。

location / {
    error_page 430 = @error_430;
    if ($http_x_test) {
        return 430; 
    }

    proxy_pass http://a;
}

location @error_430 {
    proxy_pass b;
}

對於 if 的這個用途及許多其他用途,通常可以完全避免使用該指令。在以下示例中,當請求包含 X‑Test 標頭時,map{} 塊將 $upstream_name 變數設定為 b ,並且請求被代理到以b 命名的上游 group。

map $http_x_test $upstream_name {
    default "b";
    ""      "a";
}

# ...

location / {
    proxy_pass http://$upstream_name;
}

錯誤 7:過多的健康檢查

配置多個虛擬伺服器將請求代理到同一個上游組十分常見(換句話說,在多個 server{} 塊中包含相同的 proxy_pass 指令)。這裡的錯誤是在每個 server{} 塊中都新增一個 health_check 指令。這樣做只會增加上游伺服器的負載,而不會產生任何額外資訊。

顯然,解決方法是每個 upstream{} 塊只定義一個健康檢查。此處,我們在一個指定位置為名為 b 的上游 group 定義了健康檢查,並進行了適當的超時和http訊息頭設定。

location / {
    proxy_set_header Host $host;
    proxy_set_header "Connection" "";
    proxy_http_version 1.1;
    proxy_pass http://b;
}

location @health_check {
    health_check;
    proxy_connect_timeout 2s;
    proxy_read_timeout 3s;
    proxy_set_header Host example.com;
    proxy_pass http://b;
}

在複雜的配置中,它可以進一步簡化管理,將所有健康檢查位置以及 NGINX Plus API 儀表盤分組到單個虛擬伺服器中,如本例所示。

server {
	listen 8080;
 
	location / {
	    # …
 	}
 
	location @health_check_b {
	    health_check;
	    proxy_connect_timeout 2s;
	    proxy_read_timeout 3s;
	    proxy_set_header Host example.com;
	    proxy_pass http://b;
	}
 
	location @health_check_c {
	    health_check;
	    proxy_connect_timeout 2s;
	    proxy_read_timeout 3s;
	    proxy_set_header Host api.example.com;
	    proxy_pass http://c;
	}
 
	location /api {
	    api write=on;
	    # directives limiting access to the API (see 'Mistake 8' below)
	}
 
	location = /dashboard.html {
	    root   /usr/share/nginx/html;
	}
}

錯誤 8:不安全訪問指標

Stub Status 模組提供了有關 NGINX 操作的基本指標。對於 NGINX Plus,您還可以使用 NGINX Plus API 收集更廣泛的指標集。通過在 server{}  location{} 塊中分別包含 stub_status  api 指令來啟用指標收集,您隨後可以通過訪問 URL 來檢視這些指標。

其中一些指標是敏感資訊,可被用來攻擊您的網站或 NGINX 代理的應用,我們有時在使用者配置中看到的錯誤是未限制對相應 URL 的訪問。此處我們將介紹一些可以保護指標的方法。在第一個示例中我們將使用 stub_status

通過以下配置,網際網路上的任何人都可以訪問 http://example.com/basic_status 上的指標。

server {
    listen 80;
    server_name example.com;

    location = /basic_status {
        stub_status;
    }
}

使用HTTP 基本身份驗證保護指標

採用 HTTP 基本身份驗證相關的方式為指標新增密碼保護,包含 auth_basic  auth_basic_user_file 指令。檔案(此處為 .htpasswd)列出了可以登入檢視指標的客戶端的使用者名稱和密碼:

server {
    listen 80;
    server_name example.com;

    location = /basic_status {
        auth_basic “closed site”;
        auth_basic_user_file conf.d/.htpasswd;
        stub_status;
    }
}

使用 allow 和 deny 指令保護指標

如果您不希望強制授權使用者登入,並且您知道他們用於訪問指標的 IP 地址,另一個選項是使用allow 指令。您可以指定單獨的 IPv4 和 IPv6 地址以及 CIDR 範圍。deny all 指令將阻止來自任何其他地址的訪問。

server {
    listen 80;
    server_name example.com;

    location = /basic_status {
        allow 192.168.1.0/24;
        allow 10.1.1.0/16;
        allow 2001:0db8::/32;
        allow 96.1.2.23/32;
        deny  all;
        stub_status;
    }
}

兩種方法相結合

如果我們想結合使用這兩種方法怎麼辦?我們可以允許客戶端在沒有密碼的情況下從特定地址訪問指標,但來自不同地址的客戶端仍然需要登入。為此,我們使用 satisfy any 指令。它告訴 NGINX 允許使用 HTTP 基本身份驗證憑證登入或使用預批准的 IP 地址登入的客戶端訪問。為了提高安全性,您可以將 satisfy 設定為 all,要求來自特定地址的人登入。

server {
    listen 80;
    server_name monitor.example.com;

    location = /basic_status {
        satisfy any;

        auth_basic “closed site”;
        auth_basic_user_file conf.d/.htpasswd;
        allow 192.168.1.0/24;
        allow 10.1.1.0/16;
        allow 2001:0db8::/32;
        allow 96.1.2.23/32;
        deny  all;
        stub_status;
    }
}

對於 NGINX Plus,您可以使用相同的技術來限制訪問 NGINX Plus API 端點(在以下示例中為 http://monitor.example.com:8080/api/)以及 http://monitor.example.com/dashboard.html 上的實時活動監控儀表盤。

在沒有密碼的情況下,此配置只允許來自 96.1.2.23/32 網路或本地主機的客戶端訪問。由於指令是在 server{} 級別定義的,因此相同的限制同時應用於 API 和儀表板。附帶說明一下,apiwrite=on 引數意味著這些客戶端也可以使用 API 進行配置更改。

有關配置 API 和儀表盤的更多資訊,請參閱《NGINX Plus 管理員指南》

server {
    listen 8080;
    server_name monitor.example.com;
 
    satisfy any;
    auth_basic “closed site”;
    auth_basic_user_file conf.d/.htpasswd;
    allow 127.0.0.1/32;
    allow 96.1.2.23/32;
    deny  all;

    location = /api/ {    
        api write=on;
    }

    location = /dashboard.html {
        root /usr/share/nginx/html;
    }
}

錯誤 9:當所有流量都來自同一個 /24 CIDR 塊時使用 ip_hash

ip_hash 演算法基於客戶端 IP 地址的雜湊值,在 upstream{} 塊中的伺服器間進行流量負載均衡。雜湊鍵是 IPv4 地址或整個 IPv6 地址的前三個八位位元組。該方法建立會話永續性,這意味著來自客戶端的請求始終傳遞到同一伺服器,除非該伺服器不可用。

假設我們已將 NGINX 部署為虛擬專用網路中的反向代理(按高可用性配置)。我們在 NGINX 前端放置了各種防火牆、路由器、四層負載均衡器和閘道器,以接受來自不同來源(內部網路、合作伙伴網路和 Internet 等)的流量,並將其傳遞給 NGINX 以反向代理到上游伺服器。以下是 NGINX 的初始配置:

http {

    upstream {
        ip_hash;
        server 10.10.20.105:8080;
        server 10.10.20.106:8080;
        server 10.10.20.108:8080;
    }
 
    server {# …}
}

但事實證明存在一個問題:所有“攔截”裝置都位於同一個 10.10.0.0/24 網路上,因此對於 NGINX 來說,看起來所有流量都來自該 CIDR 範圍內的地址。請記住,ip_hash 演算法會雜湊 IPv4 地址的前三個八位位元組。在我們的部署中,每個客戶端的前三個八位位元組都是相同的(都為 10.10.0),因此它們的雜湊值也都是相同的,沒法將流量分配到不同伺服器。

解決方法是在雜湊演算法中使用 $binary_remote_addr 變數作為雜湊鍵。該變數捕獲完整的客戶端地址,將其轉換為二進位制表示,IPv4 地址為 4 個位元組,IPv6 地址為 16 個位元組。現在,每個攔截裝置的雜湊值都不同,負載均衡可正常進行。

我們還添加了 consistent 引數以使用 ketama 雜湊方法而不是預設值。這大大減少了在伺服器集更改時重新對映到不同上游伺服器的鍵的數量,為快取伺服器帶來了更高的快取命中率。

http {
    upstream {
        hash $binary_remote_addr consistent;
        server 10.10.20.105:8080;
        server 10.10.20.106:8080;
        server 10.10.20.108:8080;
    }

    server {# …}
}

錯誤 10:未採用上游組

假設您在最簡單的用例中使用 NGINX,作為監聽埠 3000的單個基於 NodeJS 的後端應用的反向代理。常見的配置可能如下所示:

http {

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_set_header Host $host;
            proxy_pass http://localhost:3000/;
        }
    }
}

非常簡單,對吧?proxy_pass 指令告訴 NGINX 客戶端向哪裡傳送請求。NGINX 需要做的就是將主機名解析為 IPv4 或 IPv6 地址。建立連線後,NGINX 將請求轉發給該伺服器。

這裡的錯誤是,假定只有一臺伺服器(因此沒有理由配置負載均衡),因此不需要建立 upstream{} 塊。事實上,一個 upstream{} 塊會解鎖幾項有助於提高效能的特性,如以下配置所示:

http {

    upstream node_backend {
        zone upstreams 64K;
        server 127.0.0.1:3000 max_fails=1 fail_timeout=2s;
        keepalive 2;
    }

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_set_header Host $host;
            proxy_pass http://node_backend/;
            proxy_next_upstream error timeout http_500;

        }
    }
}

zone 指令建立一個共享記憶體區,主機上的所有 NGINX worker 程序都可以訪問有關上游伺服器的配置和狀態資訊。幾個上游組 可以共享該記憶體區。對於 NGINX Plus,該區域還支援您使用 NGINX Plus API 更改上游組中的伺服器和單個伺服器的設定,而無需重啟 NGINX。

server 指令有幾個引數可用來調整伺服器行為。在本示例中,我們改變了 NGINX 用以確定伺服器不健康,因此沒有資格接受請求的條件。此處,只要通訊嘗試在每個 2 秒期間失敗一次(而不是預設的在 10 秒期間失敗一次),就會認為伺服器不健康。

我們結合使用此設定與 proxy_next_upstream 指令,配置在什麼情況下 NGINX 會認為通訊嘗試失敗,在這種情況下,它將請求傳遞到上游組中的下一個伺服器。在預設錯誤和超時條件中,我們添加了 http_500,以便 NGINX 認為來自上游伺服器的 HTTP 500 (Internal Server Error)程式碼表示嘗試失敗。

keepalive 指令設定每個 worker 程序快取中保留的上游伺服器的空閒 keepalive 連線的數量。我們已經在“錯誤 3:未啟用與上游伺服器的 keepalive 連線”中討論了這樣做的好處。

在 NGINX Plus 中,您還可以配置與上游組 有關的其他功能:

  • 上文提到了 NGINX 開源版僅在啟動時將伺服器主機名解析為 IP 地址一次。server 指令的 resolve引數能夠支援 NGINX Plus 監控與上游伺服器的域名對應的 IP 地址的變化,並自動修改上游配置而無需重新啟動。

    service 引數進一步支援 NGINX Plus 使用 DNS SRV 記錄,其中包括有關埠號、權重和優先順序的資訊。這對於通常動態分配服務埠號的微服務環境非常重要。

    有關解析伺服器地址的更多資訊,請參閱我們的博文“NGINX 和 NGINX Plus 使用 DNS 進行服務發現”。

  • server 指令的 slow_start 引數支援 NGINX Plus 逐漸增加發送到新近被認為是健康且可用於接受請求的伺服器的請求量。這可以防止請求激增,避免伺服器不堪重負,進而導致再次失敗。

  • queue 指令允許 NGINX Plus 在無法選擇上游伺服器來處理請求時將請求放入佇列中,而不是立即向客戶端返回錯誤。


更多資源

想要更及時全面地獲取 NGINX 相關的技術乾貨、互動問答、系列課程、活動資源?

請前往 NGINX 開源社群: