Nginx 動態 upstreams 實現

oschina發表於2015-08-20

我最近在工作中做一個設定,我有一個面向使用者的 Nginx 服務,它將訪問轉發到執行在AWS Elastic Load Balancer (如你所知. ELB)上的一個服務。這本身似乎不是一個困難的任務,你只需要找到 ELB 的主機名,將 ngin x指向它,這樣不就搞定了,對吧?

location / {
    proxy_pass http://service-1234567890.us-east-1.elb.amazonaws.com;
}

測試沒有問題,再正確設定一下防火牆/安全組配置,它就應該可以很好的工作了。幾個小時之後,你可能會發現,服務不再工作了,儘管沒有做任何改變。直接訪問 ELB 端點是可以工作的,但訪問 Nginx 卻總是超時提醒。

ELB端啟蒙

為了弄清楚為什麼服務突然中止,需要先了解一下 ELB 是如何工作的:

當你建立一個彈性負載均衡(Elastic Load Balancer),你將會得到 DNS 的返回記錄,AWS 會告訴你所有在使用的訪問服務。DNS 記錄是一個輪詢 DNS(round robin DNS)記錄,它指向兩個或更多的 IP 地址——這取決於你有多少可用的區域。DNS 記錄被設定成 60 秒的存活時間(time to live),這意味幾乎不會有記錄快取。

短 TTL 可以讓 AWS 快速改變機器的執行負載,在不中斷服務的情況下,不會有任何複雜的虛擬 IP 問題。這也是他們特別告訴你不要查詢主機名和傳送流量到其中某個 IP 地址的原因,那樣的話,你的服務可能會在未來某個未定義的時間,IP 地址可能會停止為負載均衡工作。

回到 Nginx

問題在於,對於 Nginx 來說,當它讀取到一個配置時,它就會立刻向 DNS 請求主機名,然後使用其結果,直到下次重新載入配置。在這段時間到來之前,ELB 可能改變 IP 地址,讓你的 Nginx 把請求轉發到一些不為你服務的地址。

Nginx Plus

解決這個問題的方式是為 Nginx Plus 付費,它新增 resolve 標記對在 upstream 分組上的伺服器進行指示。那就是讓 Nginx 驕傲的 DNS 對 TTL 的記錄,偶爾按順序重新處理記錄,並取得伺服器使用的更新列表。

為這個功能花費每年每伺服器 $1.500,看起來花費很多。當然這是你希望得到 Nginx Plus 帶來的其他功能,如果你不需要它們,這將會是一個昂貴的升級。

免費的選擇

一個更加實惠的選擇是寫這樣一個配置:

resolver 172.16.0.23;
set $upstream_endpoint http://service-1234567890.us-east-1.elb.amazonaws.com;
location / {
    proxy_pass $upstream_endpoint;
}

它將會生效並且 Nginx 會遵循記錄 DNS 記錄的 TTL,萬一一個請求進來,會重新解釋它而且快取的記錄會過期。為什麼會這樣?

答案可以在 proxy_pass 指令文件結尾找到,它宣告瞭:

伺服器名,埠以及傳遞的URI也可以使用變數被指定:

proxy_pass http://$host$uri;

甚至像這樣:

proxy_pass $request;

在這個案例中,伺服器名會在所描述的 server groups 中被查詢,如果沒找到,會使用 resolver 來決定.

當我們給 proxy_pass 提供一個變數的時候,我們基本上是利用其改變行為,但這樣確實需要我們在配置中指定一個 DNS resolver。例子裡邊用到的 DNS resolver應該能夠在 AWS 上面跑在預設 VPC或者 EC2 中的所有伺服器工作(適用)。你也可以隨時檢視 /etc/resolv.conf 找出哪些 AWS 為你的伺服器提供並使用了哪些 DNS 伺服器。

關於轉發 URI 的 Caveat(警告)

如果你在 Nginx 中設定的 Location 不只是 /,那麼你需要注意到當給定一個變數作為引數時,proxy_pass 細微的改變行為。

先說重要的,快速概括 proxy_pass 如何在正常在操作中工作:

正常的表現行為

設想我們有一個 Nginx 配置包括這些:

location /foo/ {
    proxy_pass http://127.0.0.1:8080;
}

當我們傳送一個 /foo/bar/baz 的請求到這個站點,Nginx 會轉發請求到 http://127.0.0.1:8000/foo/bar/baz。

location /foo/ {
    # Note the trailing slash       ↓
    proxy_pass http://127.0.0.1:8080/;
}

Nginx 會在 Location 記錄裡邊去掉部分指定的 URI,然後把剩下的部分傳給 upstream 伺服器。所以請求 /foo/bar/baz 會被轉發到 http://127.0.0.1:8080/bar/baz。

改變行為

當我們使用一個變數作為 proxy_pass 的引數的時候,上面帶有尾部斜槓的行為會改變。例如我們有這樣的配置。

resolver 172.16.0.23;
set $upstream_endpoint http://service-1234567890.us-east-1.elb.amazonaws.com/;
location /foo/ {
    proxy_pass $upstream_endpoint;
}

當我們向那個配置傳送請求 /foo/bar/baz,轉發請求將不會去到/並且不是預想中的 /bar/baz。

為此解決方案就是從 upstream 的 endpoint 去掉尾部斜槓,然後像這樣手動重寫:

resolver 172.16.0.23;
set $upstream_endpoint http://service-1234567890.us-east-1.elb.amazonaws.com;
location /foo/ {
    rewrite ^/foo/(.*) /$1 break;
    proxy_pass $upstream_endpoint;
}

然後當你傳送請求 /foo/bar/baz,upstream 會接受到我們想要的請求 /bar/baz。

結束語

要知道這不單單隻適用於設定用 elb 做 upstream 伺服器,它適用於配置所有在 nginx 做 upstream 伺服器的修改 DNS 配置的情況。

希望這對你有用,如果你有任何建議或者只是想單純聯絡我,用 twitter 聯絡吧 Tenzer

相關文章