使用 NGINX 和 NGINX Plus 實現智慧高效的位元組範圍快取

Yujiaao發表於2022-06-30

作者: F5的歐文加勒特 產品管理高階總監2016 年 1 月 21 日

正確部署後,快取是加速 Web 內容的最快捷方式之一。快取不僅使內容更靠近終端使用者(從而減少延遲),還減少了對上游源伺服器的請求數量,從而提高了容量並降低了頻寬成本。

AWS 等全球分散式雲平臺和 Route 53 等基於 DNS 的全球負載平衡系統的可用性使您可以建立自己的全球內容交付網路 (CDN)。

在本文中,我們將瞭解NGINX 開源和NGINX Plus如何快取和交付使用位元組範圍請求訪問的流量。一個常見的用例是 HTML5 MP4 視訊,其中請求使用位元組範圍來實現特技播放(跳過和搜尋)視訊播放。我們的目標是實現支援位元組範圍的視訊傳輸快取解決方案,並最大限度地減少使用者延遲和上游網路流量。

編者:在 NGINX Plus R8 中引入了逐個切片填充快取切片中討論的快取切片方法。有關該版本中所有新功能的概述,請參閱我們部落格: NGINX Plus R8 發版。

我們的測試框架

我們需要一個簡單的、可重現的測試框架來研究使用 NGINX 進行快取的替代策略。

一個簡單、可重複的測試平臺,用於研究 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 快取切片詳解

啟用切版快取

啟用 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 會中止事務,因為底層快取版本現在已損壞。我們建議您僅對釋出後不會更改的大檔案(例如視訊檔案)使用快取切片。

概括

如果您使用位元組範圍交付大量資源,快取鎖定和快取切片技術都可以最大限度地減少網路流量併為您的使用者提供出色的內容交付效能。

如果快取填充操作可以快速執行,並且您可以在填充過程中接受到源伺服器的流量峰值,請使用快取鎖定技術。

如果快取填充操作非常慢且內容穩定(不更改),請使用新的快取切片技術。

相關文章