轉自:https://n3xtchen.github.io/n3xtchen/nginx/2016/02/19/nginx-port-forwording
介紹
這裏,我們將介紹 Nginx 的 Http 代理功能(請求(request) 通過 Nignx 傳遞到後端服務器,進行後續處理)。Nginx 經常設置爲 反向代理(Reverse Proxy) 幫助 橫向擴展(scale out:通過增加獨立服務器來增加運算能力) 基礎架構(infrastructure) 來提升負載能力或者傳遞請求給下一級代理服務器。
接下來,我們將討論如何使用 Nginx 的 負載均衡(load balance ) 功能來 橫向擴展(scale out) 服務器。我們同時還會探討使用 緩衝(buffering) 和 緩存(caching) 技術來提升代理性能。
常規的代理信息
如果你之前只是部署單臺 Web 服務器,那你可能會想知道爲什麼需要代理請求。
橫向擴展(scale out) 提升 基礎架構(infrastructure) 的能力是使用代理的原因之一。Nignx 的設計初衷就是被用來處理併發請求,是客戶端接觸點的理想選擇。代理服務器可以傳遞 request 給多個能處理大量任務的後端服務器,達到跨設備分散負載的目的。這樣的設計同樣也能幫助你更容易得添加服務器或者下線需要維護的服務器。
當應用服務器沒有直接處理 request 的能力的時候,代理服務器就可以發揮作用了。很多框架(包括 Web 服務器)不如專門設計成高性能服務器(如 Nignx)那樣健壯。這種場景下,把 Nginx 放在這些服務之前,可以提升用戶體驗和安全性。
Nigix 通過接收 request,把它轉發給其他服務器處理來完成代理過程的。request 的處理結果會返回 Nginx,然後轉發給客戶端。實例中的其他服務器可以是遠程機器,本地服務,甚至是由 Nginx 定義的其他虛擬服務。由 Nginx 代理的服務器,我們稱之爲 upstream(上游)服務。
Nginx 可以代理使用 http(s), FastCGI, SCGI 和 uwsgi 的請求,或者爲每種代理類型指定不同指令的 memcached 協議。在這個指南中,我們專注於 http 協議。 Nginx 實例負責傳遞 request,並把各個信息融合成一個 upstream 可理解的格式。
解構一個簡單的 HTTP 代理傳遞過程
最簡單的代理類型莫過於把一個 request 導向到單一使用 http 協議通信的服務器了。我們把這類代理統稱爲 proxy pass,由 proxy_pass
指令處理。
proxy_pass
指令主要在 location
的 context(中文含義:語境或上下文) 中使用。它還可以在 location
和 limit_except
的 context 的 if
語法塊中使用。當 request 匹配到一個包含 proxy_pass
的 location
中時, 該 request 將會被指令轉發(Forward)到這個鏈接去。
讓我們看一個例子:
# server context
location /match/here {
proxy_pass http://example.com;
}
. . .
在上面代碼片段中,proxy_pass
語句中的服務器地址沒有提供 URI。在該模式下, request 的 URI 會原封不動地直接傳遞給 upstream 服務器。來看個例子:
- Nginx 所接受的 request 的原始 URI:
/match/here/please
example.com
從 Nginx 接收到的形式:http://example.com/match/here/please
讓我們一起看看另外一個場景:
# server context
location /match/here {
proxy_pass http://example.com/new/prefix;
}
. . .
上述例子中,代理服務器在尾部定義了 URI。當 URI 放到 proxy_pass
定義中時, request 中匹配這個 location
的部分會在傳遞的過程中將會被這個 URI 直接覆蓋掉,再來看個例子:
- Nginx 所接受的 request 的原始 URI:
/match/here/please
- upstream 服務器 從 Nginx 接收到的形式:
http://example.com/new/prefix/please
,這裏/match/here
被替換成/new/prefix
有時,這樣的替換是失效。這時,定義在 proxy_pass
的尾部的 URI 會被忽略,Nginx 直接把來自客戶端或被其他 Nginx 的指令修改的 URI 傳遞給 upstream 服務器。
例如,使用正則表達式匹配 location
, URI 的匹配出現爭議時,Nginx 直接發送客戶端 upstream 的原始 URI。還有另一個例子,當一個 rewrite 指令在同一個地址中使用,會導致客戶端的 URI 被重寫,但是仍然在同一個 block 下處理。這時,傳遞的 URI 是重寫後的。
理解 Nginx 處理 Headers 的方式
如果你希望 upstream 能合理地處理 request ,那僅僅傳遞 URI 是不夠的。來自於 Nginx 的 request 和直接來源於客戶端的 request 之間還是有區別的。這裏最大的差異來自於 request 的 Headers(頭信息)。
當 Nginx 代理一個 request ,它會自動對 Headers 做一些調整:
- Nginx 會去除任何空的 Headers。轉發空值是沒有意義的;它只會讓 request 變得臃腫。
- Nginx 默認把名稱包含下劃線的 Headers 視爲無效,直接移除。如果你希望讓這類型的信息生效,那你要把
underscores_in_headers
指令設置成on
,否則這樣的頭信息將不會把他發送給後端服務器。 Host
會被重寫成由$proxy_host
定義的值。它可以是由proxy_pass
指令定義的 upstream 的IP
(或者名稱)和端口。- Headers 中的 Connection 改成
close
。這個 Headers 用在兩個服務器創建特定連接的信號信息。在這個實例中,Nginx把它設置成close
,一旦原始 request 被響應,upstream 的這個連接將被關閉。upstream 不應該期望這個連接被持久化。
從第一點看來,我們可以確定任何不希望被轉發的 Headers 都應該被設置成空字符串。帶空值的 Headers 會被完全刪除掉。
接下來一點用來設置如果你的後端應用想要接受非標準的 Headers,你應該確保它們不應該帶下劃線。如果你需要的 Headers 使用了下劃線,你你需要把 underscores_in_headers
指令設置成 on
(在 http
的 context 或者爲這個 IP 和端口組合聲明的默認服務器的 context 中有效)。如果你不想這麼做,Nginx 將會把這類 Headers 標記爲無效,並在傳遞給 upstream 之前把它丟棄。
Headers 的 Host 在大部分代理場景中都起着重要作用,它默認被設置成 $proxy_host
的值,一個由 proxy_pass
定義的包含 IP 或名稱和端口的值。這樣的默認設定是爲了讓 Nginx 確保 upsteam 可以響應的地址是唯一的(它直接從連接信息取出)。
Host 常見的值如下:
$proxy_host
:它把 Host 設置成從proxy_pass
定義的 IP 或名稱加上端口的組合。從 Nginx 的角度看,它是默認以及安全的,但是經常不是被代理服務器需要的來正確處理請求的值。$http_host
:它把 Host 設置成客戶端 request 的 Headers 中相關信息。這個 Headers 由客戶端發送,可以被 Nginx 使用。這個變量名前綴是$http_
,後面緊跟着 Headers 的名稱,以小寫命名,任何斜槓都會被下劃線替換。雖然$http_host
在大部分情況可用的,但是當客戶端的 request 沒有有效的 Host 信息的時候,會導致傳輸失敗。$host
:這個是偏好設置,它可以是來自 request 的主機名,請求中的 Host 或者匹配請求的服務器名。
在大部分情況,你將會把 Host 設置成 $host
變量。它是最靈活的,經常爲被代理的服務器提供儘可能精確的 Host 信息。
配置或者重置 Headers
爲了適配代理連接,我們可以使用 proxy_set_header
指令。例如,爲了改變我們之前討論的 Host 以及其它的 Headers 中的配置,我們可以這麼做:
# server context
location /match/here {
proxy_set_header HOST $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://example.com/new/prefix;
}
. . .
上述配置把 request 中 Headers 的 Host 設置成 $host
變量,它將包含 request 的原始主機名。Headers 的 X-Forwarded-Proto
提供了關於原始 request 的 Headers 中被代理服務器協議(http 還是 https)。
X-Real-IP
被設置成客戶端的 IP 地址,以便代理服務器做判定或者記錄基於該信息的日誌。X-Forwarded-For
是一個包含整個代理過程經過的所有服務器 IP 的地址列表。在上述例子中,我們把它設置成 $proxy_add_x_forwarded_for
變量。這個變量包含了從客戶端獲取的 X-Forwarded-For
和 Nginx 服務器的 IP(按照 request 的順序)。
當然,我們也可以把 proxy_set_header
指令移到 server
或者 http
的 context 中,讓它同時在該 context 的多個 location
中生效:
# server context
proxy_set_header HOST $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_Header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location /match/here {
proxy_pass http://example.com/new/prefix;
}
location /different/match {
proxy_pass http://example.com;
}
爲負載均衡代理服務器定義 Upstream 語境(Context)
在上一個例子中,我們演示瞭如何爲了一個單臺後端服務器實現簡單的 http 代理。Nginx 讓我們很容易通過指定一個後端服務器集羣池子來擴展這個配置。
我們使用 upstream
指令來定義服務器羣的池子(pool)。這個配置假設服務器列表中的每臺機子都可以處理來自客戶端的 request。我們可以通過它輕輕鬆鬆地 橫向擴展(scale out) 我們的 基礎架構(infrastructure)。upstream
指令必須定義在 Nginx 的 http
的 context 中。
讓我們一起看個簡單的例子:
# http context
upstream backend_hosts {
server host1.example.com;
server host2.example.com;
server host3.example.com;
}
server {
listen 80;
server_name example.com;
location /proxy-me {
proxy_pass http://backend_hosts;
}
}
上述例子,我們設置一個叫做 backend_hosts
的 upstream
context。一旦定義了,這個名稱可以直接在 proxy_pass
中使用,就和常規的域名一樣。如你所見,在我們的服務器塊內,所有指向 example.com/proxy-me/… 的 request 都會被傳遞到我們定義的池子中。在池子裏,會根據配置的算法選取一臺服務器。默認,它只是一個簡單的round-robin(循環選擇) 處理(即每一個請求都會按順序傳遞給不同的服務器)。
改變 Upstream 均衡算法
你可以通過指令修改 upstream 池子的 均衡算法(balancing algorithm):
- round-robin(循環選擇):默認的算法。在其它算法沒被指定的情況下,它會被使用。
upstream
context 定義的每一個服務器都會按順序接受 request。 - least_conn(最少連接): 指定新的 request 永遠只會傳遞給擁有最少連接的後端服務器。在後端連接需要被持久化的情況下,這個算法將很有效。
- ip_hash:這種 均衡算法(balancing algorithm) 是基於客戶端的 IP 來分發 request 到不同的服務器。把客戶端 IP 的前三位八進制數作爲鍵值來決定由哪臺服務器處理。這樣,同一 IP 的客戶端每次只會由同一個臺服務器處理;它能保證 session(會話) 的一致性。
- hash:這種 均衡算法(balancing algorithm) 主要運用於緩存代理。這是唯一一種需要用戶提供數據的算法;算法根據用戶所提供數據的哈希值來決定服務器的分配。它可以是文本,變量或者文本和變量的組合。
修改 均衡算法(balancing algorithm) ,應該像下面那樣:
# http context
upstream backend_hosts {
least_conn;
server host1.example.com;
server host2.example.com;
server host3.example.com;
}
. . .
上述例子中,擁有最少連接數的服務器將會優先選擇。ip_hash
指令也可以用同樣的方式設置,來保證 session(會話) 的一致性。
至於 hash
方法,你應該提供要哈希的鍵。可以是任何你想要的:
# http context
upstream backend_hosts {
hash $remote_addr$remote_port consistent;
server host1.example.com;
server host2.example.com;
server host3.example.com;
}
. . .
上述的例子,request 的分發是基於客戶端的 IP 和端口;我們還可以添加另外的參數 consistent
,它實現了 ketama consistent hashing 算法。基本上,它意味着如果你的 upstream 服務器改變了,可以保證對 cache(緩存) 的最小影響。
設置服務器權重
在後端服務器聲明中,每一臺的服務器默認是權重平分的。它假定每一臺服務器都能且應該處理同一量級的負載(考慮到 均衡算法(balancing algorithm) 的影響)。然而,你也可以爲服務器設置其它的權重。
# http context
upstream backend_hosts {
server host1.example.com weight=3;
server host2.example.com;
server host3.example.com;
}
. . .
上述例子中,host1.example.com
可以比其它服務器多接受 2 倍的流量。默認,每一臺服務器的權重都是 1。
使用 Buffer 緩解後端的負載
對於大部分使用代理的用戶來說,最關心的事情之一就是增加一臺服務器對性能的影響。在大部分場景下,利用 Nginx 的 buffer(緩衝) 和 cache(緩存) 能力,可以大大地減輕負擔。
在代理過程中,兩個連接速度不一會對客戶端的體驗帶來不良的影響:
- 從客戶端到代理服務器的連接
- 從代理服務器到後端服務器的連接
Nginx 可以根據你希望優化哪一個連接來調整它的行爲。
沒有 buffer(緩衝),數據將會直接從代理服務器傳輸到客戶端。如果客戶端的速度足夠快(假設),你可以直接把 buffer(緩衝) 關掉,讓數據儘可能快速地到達;如果使用 buffer(緩衝),Nginx 將會臨時存儲後端 response(響應),然後慢慢把數據推送給客戶端;如果客戶端很慢,Nginx 會提前關閉後端服務器的連接。它可以任意控制分發的節奏。
Nginx 默認的 buffer(緩衝) 設計的初衷就是因爲客戶端之間速度存在差異。我們可以使用如下指令來調整 buffer(緩衝) 速度。你可以把 buffer(緩衝) 配置在 http
,server
或者 location
的 context 中。必須注意指令爲每一個,request 配置的大小;在客戶端的 request 很多的情況下,如果把值調的過大,會很影響性能:
porxy_buffering
:這個指令控制所在 context 或者子 context 的 buffer(緩衝) 是否打開。默認是on
。proxy_buffers
:這個指令控制 buffer(緩衝) 的數量(第一個參數)和大小(第二個參數)。默認是8
個 buffer(緩衝),每個 buffer(緩衝) 大小是1
個內存頁(4k
或8k
)。增加 buffer 的數量可以緩衝更多的信息。proxy_buffer_size
:是來自後端服務器 response 信息的一部分,它包含 Headers,從 response 分離出來。這個指令設置 response 的緩衝。默認,它和proxy_buffers
一樣,但是因爲它僅用於 Headers,所有它的值一般設置得更低。proxy_busy_buffer_size
:這個指令設置忙時 buffer(緩衝) 的最大值。一個客戶端一次只能從一個 buffer(緩衝) 中讀取數據的同時,剩下的 buffer(緩衝) 會被放到隊列中,等待發送到客戶端。這個指令控制在這個狀態下 buffer(緩衝) 的空間大小proxy_max_temp_file_size
:當代理服務器的 response 太大超出配置的 buffer(緩衝) 的時候,它來控制 Nginx 單次可以寫入臨時文件的最大數據量。proxy_temp_path
:當代理服務器的 response 太大超出配置的 buffer(緩衝) 的時候,Nginx 寫臨時文件的路徑。
正如你看到的,Nginx 提供了這幾個指令來調整 buffer(緩衝) 行爲。大部分時間,你不需要關心這些指令中的大部分;但是它們中的一些會很有用,可能最有用的就是 proxy_buffer
和 proxy_buffer_size
這兩個指令。
下面這個例子中增加每個 upstream 可用代理 buffer(緩衝) 數,減少存儲 Headers 的 buffer(緩衝) 數:
# server context
proxy_buffering on;
proxy_buffer_size 1k;
proxy_buffers 24 4k;
proxy_busy_buffers_size 8k;
proxy_max_temp_file_size 2048m;
proxy_temp_file_write_size 32k;
location / {
proxy_pass http://example.com;
}
相反,如果你的客戶端足夠快到你可以直接傳輸數據,你就可以完全關掉 buffer(緩衝)。實際上,即使 upstream 比客戶端快很多,Nginx 還是會使用 buffer(緩衝) 的,但是它會直接清空客戶端的數據,不會讓它進入到 buffer(緩衝) 池子。如果客戶端很慢,這會導致 upstream 連接會一直開到客戶端處理完爲止。當 buffer(緩衝) 被設置爲 off
的時候,只有 proxy_buffer_size
指令定義的 buffer(緩衝) 會被使用。
# server context
proxy_buffering off;
proxy_buffer_size 4k;
location / {
proxy_pass http://example.com;
}
高可用性(可選)
你可以通過添加一個冗餘的負載均衡器來使 Nginx 代理更加健壯,創建一個高可用性基礎設施。
一個 高可用(HA) 的配置是一種容許單點錯誤(single point of failure)的基礎設施,你的負載均衡是這個配置的一部分。當你的負載均衡器不可用或者你需要下線維護,你可以通過配置多個負載均衡器防止潛在的停機風險。
這是基本高可用架構圖:
這個例子中,在靜態 IP (它可以映射到一臺或多臺服務器)背後配置多個負載均衡器(一個是激活的,其它的一或多個是被動激活的)。客戶端 request 從靜態 IP 路由到激活的負載均衡器,然後到後端服務器。想了解更多,請閱讀 this section of How To Use Floating IPs。
配置代理緩存來減少響應時間
buffer(緩衝) 幫助減輕後端服務器負擔達到處理更多 request 目的的同時,Nginx 還提供一個從後端服務器緩存內容的功能,減少要連接 upstream 的次數。
配置代理緩存
我們使用 proxy_cache_path
指令來爲代理的內容設置緩存。它會創建一個直接用於代理服務器返回的數據存儲區域。proxy_cache_path
指令必須在 http
的 context 中設置。
下面例子中,我們將會配置這個和相關指令來設置我們的緩存系統。
# http context
proxy_cache_path /var/lib/nginx/cache levels=1:2 keys_zone=backcache:8m max_size=50m;
proxy_cache_key "$scheme$request_method$host$request_uri$is_args$args";
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
我們可以使用 proxy_cache_path
指令來定義緩存的存儲路徑。在這個例子,我使用 /var/lib/nginx/cache
這個路徑。如果這個路徑不存在,你需要創建這個目錄,並賦予正確的權限:
sudo mkdir -p /var/lib/nginx/cache
sudo chown www-data /var/lib/nginx/cache
sudo chmod 700 /var/lib/nginx/cache
參數 levels=
用來指定緩存的組織形式。Nginx 將會通過哈希鍵值創建一個緩存 key(在下方配置)。上述我們選擇的 level 採用 2 級目錄結構,內存空間的大小是 8m,假設我們的哈希鍵值爲 b7f54b2df7773722d382f4809d65087c
,那存儲該內容的目錄結構是:/var/lib/nginx/cache/backcache/c/87/b7f54b2df7773722d382f4809d65087c
,注意到規律沒有?參數 keys_zone=
定義緩存區域(我們稱之爲 backzone
)的名稱。這個也是我們定義存儲多少元數據的地方。在這個場景中,我們存儲 8 MB 的鍵。Nginx 將每一兆會存儲 8000 個實體。參數 max_size
用來定義實際緩存數據的最大尺寸。
現在,我歸納下:
proxy_cache_path {cache_root:緩存路徑}
levels={n:從緩存鍵值倒數n個字符作爲一級目錄}:{m:從緩存鍵值倒數第 n 個字符開始 m 個字符作爲二級目錄}
keys_zone={cache_name:該緩存在緩存路徑的目錄名}:8m;
最終該鍵緩存的目錄結構是:
{cache_root}/{cache_name}/{數字:從緩存鍵值倒數n個字符作爲一級目錄}/{從緩存鍵值倒數n個字符}/{從緩存鍵值倒數第 n 個字符開始 m 個字符作爲二級目錄}
上面我們使用的另一個指令就是 proxy_cache_key
。它用來設置用來存儲緩存值的鍵。
proxy_cache_valid
指令可以被指定多次。它允許我們基於不同狀態碼存儲不同值。在我們的例子中,我們存儲 ** 200 狀態(成功)** 和 302 狀態(重定向) 緩存時間爲 10 分鐘,和 404 狀態 爲 1 分鐘後清除緩存。
現在,我們已經配置好緩存區域,但是我們仍然需要告訴 Nginx 在哪裏使用緩存。
在我們定義代理到後端的 location
中,我們配置緩存的使用:
# server context
location /proxy-me {
proxy_cache backcache;
proxy_cache_bypass $http_cache_control;
add_header X-Proxy-Cache $upstream_cache_status;
proxy_pass http://backend;
}
. . .
使用 proxy_cache
指令,我們可以指定所在 context 可以使用 backcache
的緩存區域。Nginx 將在傳遞到後端之前檢查可用的緩存實體。
proxy_cache_bypass
指令用來設置 $http_cache_control
變量。它告知代理服務器發請求的客戶端是否需要請求一個新鮮,未緩存版本的資源。
我們還可以增加一個多餘 Headers 信息(X-Proxy-Cache
)。我們把這個 Headers 設置成 $upstream_cache_status
。它設置 Headers 來告知用戶該 request 的緩存是否被命中,丟失,或者被繞過。在 debug 的時候,該配置特別有用;並且對客戶端來說也很重要。
緩存結果的注意事項
緩存可以極大地提高代理服務器的性能。然而,配置緩存的時候,還是要需要考慮挺多的東西的。
首先,任何用戶相關的數據都不應該被緩存。因爲這樣會導致一個用戶數據的結果被呈現到另一個用戶。如果你的站點是純靜態的,那這可能就不是問題了。
如果你的站點有些動態元素,那你就需要在後端服務器考慮到這一點。處理它的方式依賴於後端處理方式。對於隱私內容,你應該把 Cache-Control
設置成 no-cache
,no-store
,或者 private
,這個依賴於數據本身:
no-cache
:表示必須先與服務器確認返回的響應是否被更改,然後才能使用該響應來滿足後續對同一個網址的請求。因此,如果存在合適的驗證令牌 (ETag),no-cache 會發起往返通信來驗證緩存的響應,如果資源未被更改,可以避免下載。no-store
:直接禁止瀏覽器和所有中繼緩存存儲返回的任何版本的響應 - 例如:一個包含個人隱私數據或銀行數據的響應。每次用戶請求該資源時,都會向服務器發送一個請求,每次都會下載完整的響應。private
:瀏覽器可以緩存private響應,但是通常只爲單個用戶緩存,因此,不允許任何中繼緩存對其進行緩存 - 例如,用戶瀏覽器可以緩存包含用戶私人信息的 HTML 網頁,但是 CDN 不能緩存。這個在緩存用戶瀏覽器數據的時候很有用,但是代理服務器不會在後續的請求中承認數據的有效性。public
:說明請求的是公共信息,它可以被任意的緩存。
控制這個行爲相關的 Headers 還有 max-age
,它控制資源緩存的過期時間。
根據數據的敏感度,正確地設置這些 Headers,將會幫助你有效地利用緩存,既能保障你的隱私數據安全的同時,還能讓動態內容進行有效地刷新。
如果你的後端服務器也使用 Nginx,你可以像下面這樣使用 expires
指令,它將會爲 Cache-Control
設置 max-age
:
location / {
expires 60m; # 給請求的 Header 添加 Cache-Control:max-age=3660;
}
location /check-me {
expires -1; # 給請求的 Headers 添加 Cache-Control:no-cache;
}
在上面的例子中,第一個塊允許內容被緩存 60 分鐘。第二個塊則把 Cache-Control
頭設置成 no-cache
。還想設置其他值,你可以使用 add_header
指令:
location /private {
expires -1;
add_header Cache-Control "no-store";
# 給請求的 Headers 添加 Cache-Control:no-cache, no-store;
}
結語
Nginx 是第一個也是最重要的反向代理服務器,還可以作爲 Web 服務器使用。因爲這樣的設計決策,代理請求到另個服務器變得更簡單。Nginx 也足夠靈活,允許你根據需求對代理配置進行靈活的控制。
參考文獻: