作者: F5的歐文加勒特 產品管理高級總監2016 年 1 月 21 日
正確部署後,緩存是加速 Web 內容的最快捷方式之一。緩存不僅使內容更靠近最終用户(從而減少延遲),還減少了對上游源服務器的請求數量,從而提高了容量並降低了帶寬成本。
AWS 等全球分佈式雲平台和 Route 53 等基於 DNS 的全球負載平衡系統的可用性使您可以創建自己的全球內容交付網絡 (CDN)。
在本文中,我們將瞭解NGINX 開源和NGINX Plus如何緩存和交付使用字節範圍請求訪問的流量。一個常見的用例是 HTML5 MP4 視頻,其中請求使用字節範圍來實現特技播放(跳過和搜索)視頻播放。我們的目標是實現支持字節範圍的視頻傳輸緩存解決方案,並最大限度地減少用户延遲和上游網絡流量。
編者:在 NGINX Plus R8 中引入了逐個切片填充緩存切片中討論的緩存切片方法。有關該版本中所有新功能的概述,請參閲我們博客: NGINX Plus R8 發版。
我們的測試框架
我們需要一個簡單的、可重現的測試框架來研究使用 NGINX 進行緩存的替代策略。
一個簡單、可重複的測試平台,用於研究 NGINX 中的緩存策略
我們從一個 10-MB 的測試文件開始,其中包含每 10 個字節的字節偏移量,以便我們可以驗證字節範圍請求是否正常工作:
origin$ perl -e 'foreach $i ( 0 ... 1024*1024-1 ) { printf "%09d\n",
$i*10 }' > 10Mb.txt
文件中的第一行如下:
origin$ head 10Mb.txt
000000000
000000010
000000020
000000030
000000040
000000050
000000060
000000070
000000080
000000090
對文件中的中間字節範圍(500,000 到 500,009)的 curl 請求會返回預期的字節範圍:
client$ curl -r 500000-500009 http://origin/10Mb.txt
000500000
現在讓我們為源服務器和 NGINX 代理緩存之間的單個連接添加 1MB/s 的帶寬限制:
origin# tc qdisc add dev eth1 handle 1: root htb default 11
origin# tc class add dev eth1 parent 1: classid 1:1 htb rate 1000Mbps
origin# tc class add dev eth1 parent 1:1 classid 1:11 htb rate 1Mbps
為了檢查延遲是否按預期工作,我們直接從源服務器檢索整個文件:
cache$ time curl -o /tmp/foo http://origin/10Mb.txt
% Total % Received % Xferd Average Speed Time ...
Dload Upload Total ...
100 10.0M 100 10.0M 0 0 933k 0 0:00:10 ...
... Time Time Current
... Spent Left Speed
... 0:00:10 --:--:-- 933k
real 0m10.993s
user 0m0.460s
sys 0m0.127s
交付文件需要將近 11 秒,這是對邊緣緩存性能的合理模擬,邊緣緩存通過帶寬有限的 WAN 網絡從源服務器拉取大文件。
NGINX 的默認字節範圍緩存行為
一旦 NGINX 緩存了整個資源,它會直接從磁盤上的緩存副本中為字節範圍的請求提供服務。
當內容沒有被緩存時會發生什麼?當 NGINX 收到對未緩存內容的字節範圍請求時,它會從源服務器請求整個文件(不是字節範圍),並開始將響應流式傳輸到臨時存儲。
一旦 NGINX 收到滿足客户端原始字節範圍請求所需的數據,NGINX 就會將數據發送給客户端。在後台,NGINX 繼續將完整響應流式傳輸到臨時存儲中的文件。傳輸完成後,NGINX 將文件移動到緩存中。
我們可以通過以下簡單的 NGINX 配置很容易地演示默認行為:
proxy_cache_path /tmp/mycache keys_zone=mycache:10m;
server {
listen 80;
proxy_cache mycache;
location / {
proxy_pass http://origin:80;
}
}
我們首先清空緩存:
cache # rm –rf /tmp/mycache/*
然後我們請求10Mb.txt的中間十個字節:
client$ time curl -r 5000000-5000009 http://cache/10Mb.txt
005000000
real 0m5.352s
user 0m0.007s
sys 0m0.002s
NGINX 向源服務器發送整個 10Mb.txt 文件的請求,並開始將其加載到緩存中。一旦請求的字節範圍被緩存,NGINX 就會將其交付給客户端。正如time命令所報告的,這發生在 5 秒多一點的時間內。
在我們之前的測試中,傳送整個文件只需要 10 多秒,這意味着在將中間字節範圍傳送到客户端之後,檢索和緩存 10Mb.txt 的全部內容還需要大約 5 秒。虛擬服務器的訪問日誌記錄了完整文件中 10,486,039 字節 (10 MB) 的傳輸,狀態碼為 200:
192.168.56.10 - - [08/Dec/2015:12:04:02 -0800] "GET /10Mb.txt HTTP/1.0" 200 10486039 "-" "-" "curl/7.35.0"
如果我們 curl 在整個文件被緩存後重復請求,響應是立即的,因為 NGINX 從緩存中提供請求的字節範圍。
但是,這種基本配置(以及由此產生的默認行為)存在問題。如果我們第二次請求相同的字節範圍,在它被緩存之後但在整個文件被添加到緩存之前,NGINX 向源服務器發送一個對整個文件的新請求,並開始一個新的緩存填充操作。我們可以使用以下命令演示此行為:
client$ while true ; do time curl -r 5000000-5000009 http://dev/10Mb.txt ; done
對源服務器的每個新請求都會觸發一個新的緩存填充操作,並且緩存不會“穩定下來”,直到緩存填充操作完成而沒有其他操作正在進行。
想象一下用户在視頻文件發佈後立即開始觀看的場景。如果緩存填充操作需要 30 秒(例如),但額外請求之間的延遲小於此值,則緩存可能永遠不會填充,NGINX 將繼續向源服務器發送越來越多的整個文件請求。
NGINX 提供了兩種緩存配置,可以有效解決這個問題:
緩存鎖 ——使用此配置,在由第一個字節範圍請求觸發的緩存填充操作期間,NGINX 將任何後續字節範圍請求直接轉發到源服務器。緩存填充操作完成後,NGINX 為緩存中的字節範圍和整個文件的所有請求提供服務。
緩存切片 ——通過在 NGINX Plus R8 和 NGINX 開源 1.9.8 中引入的這種策略,NGINX 將文件分割成可以快速檢索的更小的子範圍,並根據需要從源服務器請求每個子範圍。
對單個緩存填充操作使用緩存鎖
以下配置在收到第一個字節範圍請求時立即觸發緩存填充,並在緩存填充操作正在進行時將所有其他請求轉發到源服務器:
proxy_cache_path /tmp/mycache keys_zone=mycache:10m;
server {
listen 80;
proxy_cache mycache;
proxy_cache_valid 200 600s;
proxy_cache_lock on;
# Immediately forward requests to the origin if we are filling the cache
proxy_cache_lock_timeout 0s;
# Set the 'age' to a value larger than the expected fill time
proxy_cache_lock_age 200s;
proxy_cache_use_stale updating;
location / {
proxy_pass http://origin:80;
}
}
proxy_cache_lock on – 設置緩存鎖。當 NGINX 收到文件的第一個字節範圍請求時,它會從源服務器請求整個文件並啓動緩存填充操作。NGINX 不會將後續的字節範圍請求轉換為對整個文件的請求或啓動新的緩存填充操作。相反,它將請求排隊,直到第一個緩存填充操作完成或鎖定超時。
proxy_cache_lock_timeout – 控制緩存鎖定多長時間(默認為 5 秒)。當超時到期時,NGINX 將每個排隊的請求未經修改地轉發到源服務器(作為保留標頭的字節範圍請求Range,而不是作為對整個文件的請求),並且不緩存源服務器返回的響應。
在我們使用10Mb.txt進行測試的情況下,緩存填充操作可能會花費大量時間,因此我們將鎖定超時設置為 0(零)秒,因為沒有必要將請求排隊。NGINX 會立即將文件的任何字節範圍請求轉發到源服務器,直到緩存填充操作完成。
proxy_cache_lock_age – 設置緩存填充操作的最後期限。如果操作沒有在指定時間內完成,NGINX 會再向源服務器轉發一個請求。它總是需要比預期的緩存填充時間更長,因此我們將其從默認的 5 秒增加到 200 秒。
proxy_cache_use_stale updating – 如果 NGINX 正在更新資源,則告訴 NGINX 立即使用資源的當前緩存版本。這對第一個請求(觸發緩存更新)沒有影響,但會加速對客户端後續請求的響應。
我們重複我們的測試,請求10Mb.txt的中間字節範圍。該文件沒有被緩存,並且與之前的測試一樣,time 表明 NGINX 需要 5 秒多一點的時間才能交付請求的字節範圍(回想一下,網絡的吞吐量限制為 1 Mb/s):
client # time curl -r 5000000-5000009 http://cache/10Mb.txt
005000000
real 0m5.422s
user 0m0.007s
sys 0m0.003s
由於緩存鎖定,在緩存被填充時,後續對字節範圍的請求幾乎立即得到滿足。NGINX 將這些請求轉發到源服務器,而不嘗試從緩存中滿足它們:
client # time curl -r 5000000-5000009 http://cache/10Mb.txt
005000000
real 0m0.042s
user 0m0.004s
sys 0m0.004s
在源服務器訪問日誌的以下摘錄中,帶有狀態代碼的條目206確認源服務器在緩存填充操作完成期間正在處理字節範圍請求。(我們使用該log_format指令將Range請求標頭包含在日誌條目中,以識別哪些請求已修改,哪些未修改。)
最後一行,帶有狀態碼200,對應於第一個字節範圍請求的完成。NGINX 將此修改為對整個文件的請求並觸發緩存填充操作。
192.168.56.10 - - [08/Dec/2015:12:18:51 -0800] "GET /10Mb.txt HTTP/1.0" 206 343 "-" "bytes=5000000-5000009" "curl/7.35.0"
192.168.56.10 - - [08/Dec/2015:12:18:52 -0800] "GET /10Mb.txt HTTP/1.0" 206 343 "-" "bytes=5000000-5000009" "curl/7.35.0"
192.168.56.10 - - [08/Dec/2015:12:18:53 -0800] "GET /10Mb.txt HTTP/1.0" 206 343 "-" "bytes=5000000-5000009" "curl/7.35.0"
192.168.56.10 - - [08/Dec/2015:12:18:54 -0800] "GET /10Mb.txt HTTP/1.0" 206 343 "-" "bytes=5000000-5000009" "curl/7.35.0"
192.168.56.10 - - [08/Dec/2015:12:18:55 -0800] "GET /10Mb.txt HTTP/1.0" 206 343 "-" "bytes=5000000-5000009" "curl/7.35.0"
192.168.56.10 - - [08/Dec/2015:12:18:46 -0800] "GET /10Mb.txt HTTP/1.0" 200 10486039 "-" "-" "curl/7.35.0"
當我們在整個文件被緩存後重複測試時,NGINX 會從緩存中提供任何進一步的字節範圍請求:
client # time curl -r 5000000-5000009 http://cache/10Mb.txt
005000000
real 0m0.012s
user 0m0.000s
sys 0m0.002s
使用緩存鎖可以優化緩存填充操作,但代價是在緩存填充期間將所有後續用户流量發送到源服務器。
逐片填充緩存
NGINX Plus R8 和 NGINX 開源 1.9.8 中引入的Cache Slice模塊提供了另一種填充緩存的方法,當帶寬受到嚴重限制並且緩存填充操作需要很長時間時,這種方法更有效。
編者:有關 NGINX Plus R8 中所有新功能的概述,請參閲我們博客上的 NGINX Plus R8 。
使用緩存切片方法,NGINX 將文件分成更小的段,並在需要時請求每個段。這些段在緩存中累積,並且通過將一個或多個段的適當部分傳遞給客户端來滿足對資源的請求。對大字節範圍(或者實際上是整個文件)的請求會觸發每個所需段的子請求,這些段在從源服務器到達時被緩存。一旦所有的段都被緩存了,NGINX 就會組裝來自它們的響應並將其發送給客户端。
NGINX 緩存切片詳解
在下面的配置片段中,該slice指令(在 NGINX Plus R8 和 NGINX 開源 1.9.8 中引入)告訴 NGINX 將每個文件分段為 1-MB 片段。
在使用 slice 指令時,我們還必須將 $slice_range 變量添加到 proxy_cache_key 指令中以區分文件的片段,並且我們必須替換 Range 請求中的標頭,以便 NGINX 從源服務器請求適當的字節範圍。我們將請求升級為 HTTP/1.1 因為 HTTP/1.0 不支持字節範圍請求。
proxy_cache_path /tmp/mycache keys_zone=mycache:10m;
server {
listen 80;
proxy_cache mycache;
slice 1m;
proxy_cache_key $host$uri$is_args$args$slice_range;
proxy_set_header Range $slice_range;
proxy_http_version 1.1;
proxy_cache_valid 200 206 1h;
location / {
proxy_pass http://origin:80;
}
}
和以前一樣,我們請求10Mb.txt中的中間字節範圍:
client$ time curl -r 5000000-5000009 http://cache/10Mb.txt
005000000
real 0m0.977s
user 0m0.000s
sys 0m0.007s
NGINX 通過請求單個 1-MB 文件段(字節範圍 4194304–5242879)來滿足請求,其中包含請求的字節範圍 5000000–5000009。
KEY: www.example.com/10Mb.txtbytes=4194304-5242879
HTTP/1.1 206 Partial Content
Date: Tue, 08 Dec 2015 19:30:33 GMT
Server: Apache/2.4.7 (Ubuntu)
Last-Modified: Tue, 14 Jul 2015 08:29:12 GMT
ETag: "a00000-51ad1a207accc"
Accept-Ranges: bytes
Content-Length: 1048576
Vary: Accept-Encoding
Content-Range: bytes 4194304-5242879/10485760
如果一個字節範圍請求跨越多個段,NGINX 會請求所有需要的段(尚未緩存),然後從緩存的段中組裝字節範圍響應。
Cache Slice 模塊是為交付 HTML5 視頻而開發的,它使用字節範圍請求將內容偽流到瀏覽器。它非常適用於初始緩存填充操作可能需要幾分鐘的視頻資源,因為帶寬受到限制,並且文件在發佈後不會更改。
選擇最佳切片大小
將切片大小設置為足夠小的值,以便可以快速傳輸每個段(例如,在一兩秒內)。這將減少多個請求觸發上述持續更新行為的可能性。
另一方面,切片大小可能太小。如果對整個文件的請求同時觸發數千個小請求,則開銷可能會很高,從而導致內存和文件描述符使用過多以及磁盤活動更多。
此外,由於緩存切片模塊將資源拆分為獨立的段,因此一旦資源被緩存,就無法更改資源。ETag每次從源端接收到一個段時,該模塊都會驗證資源的標頭,如果ETag發生更改,NGINX 會中止事務,因為底層緩存版本現在已損壞。我們建議您僅對發佈後不會更改的大文件(例如視頻文件)使用緩存切片。
總結
如果您使用字節範圍交付大量資源,緩存鎖定和緩存切片技術都可以最大限度地減少網絡流量併為您的用户提供出色的內容交付性能。
如果緩存填充操作可以快速執行,並且您可以在填充過程中接受到源服務器的流量峯值,請使用緩存鎖定技術。
如果緩存填充操作非常慢且內容穩定(不更改),請使用新的緩存切片技術。