TL;DR
使用 Nginx 當作 reverse proxy 、再遇上 byte-range request (206 Partial Content) 時, request 產生的流量會超過使用者實際接收到的。
如果 request 的資源剛好又沒有快取,或是有多層的 reverse proxy 時,這些額外的流量會以倍數成長。對機器、網路及效能都會有負面的影響。
起因
客戶甲使用我們的 CDN 服務,有放了一個 apk 檔案,發現有時候使用者會下載到只有 1 byte 的檔案,顯然是錯誤的。
初步觀察 Log ,從 Nginx 的 upstream_content_length 欄位發現是客戶的源站 ( Origin Server ) 曾經回傳了 1 byte,而這次 request 被我們的節點快取之後,才造成後來的 1 byte api 問題。
因為這是一個 byte-range request (client 端指定了想取得的部份,Nginx 回傳指定的範圍以及 206 partial content),一開始我們認為是單一使用者的狀況。
後來發現事情大條,有複數的使用者無法正確取得完整的檔案,問題是出在我們的服務上。
調查
Nginx 作為 reverse proxy ,預設的行為是這樣子的:
在有快取的情況下,Nginx 收到一個 byte-range request 時,Nginx 會向它的 upstream 發起 request 取得完整的內容,在需求的片段可以供應時就先行回傳,並在完整內容接收完成之後移進快取,之後的 request 就會直接從快取回覆,不再需要向 upstream 請求。
而造成這次 Bug 的原因是這樣子:
我們有另一個客戶乙,也使用 CDN 來傳遞 apk 檔案,但是它的設計是每一個使用者都使用獨特不共用的 apk 檔,也就是無法快取。
在沒有快取的情況下,Nginx 收到一個 byte-range request 時,仍然會向 upstream 取得完整的檔案,如果一個 10 byte 的檔案,每次取 1 byte 分成10次取得,雖然 CDN 節點傳給使用者的只有 1 * 10 = 10 bytes ,但是在 CDN 節點與它的 upstream 之間是產生了 10 * 10 = 100 bytes 的傳輸量,而我們的 CDN 使用不止一層 upstream ,於是產生了非常多不必要的網路流量,也對機器及效能有負面影響。
當時為了解決客戶乙的問題,我們在 Nginx Conf 內加上了如下的設定:
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
如此設定之後,在向 upstream 請求時會將 client 傳來的Byte-Range header 一併加上,如此 upstream 就不會將所有內容回傳,也就不會產生額外的流量,完美解決。
…才怪。
這是針對不快取的狀況設定的,但是對於合理使用 CDN 的客戶甲來說,就造成了這次的問題:Nginx 將 upstream 取得的部份內容當成了完整內容,回傳給 client ,造成了前述無法取得完整檔案的問題。
調整目標
在功能正確的基本前提下,盡可能的不在 upstream 產生不必要的流量。
思考
避免又像先前一樣處理了一個狀況反而又產生了另一個問題。先想看看有什麼狀況是需要注意的?
可以簡單粗暴的寫顧客特例嗎?如果是客戶乙就加上前述的 proxy_set_header?長遠來看顯然不是個好方法。
如果從後台設計一個開關,可以用手動指定的方式把 proxy_set_header 加在 Nginx Conf 上?
如果不使用 proxy_set_header Range… 來解決呢?有其他方法嗎?
快取/不快取的設定是不相同的,而且同一個 server directive 中是有可能同時需要兩種設定的。例如 apk 不快取,mp4 則照一般 CDN 模式使用。
既然有這樣的可能性,那要怎麼讓兩種設定並存?
解法 A
想法:「此次 request 不快取時,就關閉 Byte-Range Request」
Nginx 將 max_ranges 設為 0 ,就可以關閉 byte-range 功能。
以 apk 檔實測,client 發起 byte-range request 時,Nginx 會回傳 200 以及完整的檔案而非 206 及部份回應,看來可以解決我們的問題。
為了避免與先前一樣,解決一個問題卻產生了別的問題,我用也很常使用 byte-range request 的 mp4 影片檔來作測試。
各家瀏覽器有不同的影片播放行為,其中 Firefox 與預期的最接近:一開始下載完整的影片,下載完之後不管怎麼切換位置都不會再產生其他 request 。而最慘的是 Safari ,完全無法播放,找了文件才發現,對 Safari 來說,影片是「必須」支援 byte-range request 才能播放的。
看來這個作法行不通,實在想不出什麼理由可以告訴客戶「不開快取的話,影片會無法播放唷」
解法 B
綜合先前的解,「此次 request 不快取時,將 client 傳來的Byte-Range header 一併加上」
後來我們選用了這個解法,目前還沒有發現什麼問題。其中還有很多待改進的部份,待日後再一步步完善。
參考資料
/https://www.nginx.com/blog/smart-efficient-byte-range-caching-nginx/
https://www.ruby-forum.com/t/partial-requests-206-on-reverse-proxy-cache/198908/2
留言