使用Kubernetes中的Nginx來改善第三方服務的可靠性和延遲
譯自:How we improved third-party availability and latency with Nginx in Kubernetes
本文討論瞭如何在Kubernetes中通過配置Nginx緩存來提升第三方服務訪問的性能和穩定性。這種方式基於Nginx來實現,優點是不需要進行代碼開發即可實現緩存第三方服務的訪問,但同時也缺少一些定製化擴展。不支持緩存寫操作,多個pod之間由於使用了集中式共享方式,因而緩存缺乏高可用。
使用Nginx作爲網關來緩存到第三方服務的訪問
第三方依賴
技術公司越來越依賴第三方服務作爲其應用棧的一部分。對外部服務的依賴是一種快速拓展並讓內部開發者將精力集中在業務上的一種方式,但部分軟件的失控可能會導致可靠性和延遲降級。
在Back Market,我們已經將部分產品目錄劃給了一個第三方服務,我們的團隊需要確保能夠在自己的Kubernetes集羣中快速可靠地訪問該產品目錄數據。爲此,我們使用Nginx作爲網關代理來緩存所有第三方API的內部訪問。
多集羣環境中使用Nginx作爲網關來緩存第三方API的訪問
使用結果
在我們的場景下,使用網關來緩存第三方的效果很好。在運行幾天之後,發現內部服務只有1%的讀請求才需要等待第三方的響應。下面是使用網關一週以上的服務請求響應緩存狀態分佈圖:
HIT:緩存中的有效響應 ->使用緩存
STALE:緩存中過期的響應 ->使用緩存,後臺調用第三方
UPDATING:緩存中過期的響應(後臺已經更新) ->使用緩存
MISS:緩存中沒有響應 ->同步調用第三方
即使在第三方下線12小時的情況下,也能夠通過緩存保證96%的請求能夠得到響應,即保證大部分終端用戶不受影響。
內部網關的響應要遠快於直接調用第三方API的方式(第三方位於Europe,調用方位於US)。
以 ms 爲單位的緩存路徑的請求持續時間的 P90(1e3爲1秒)
下面看下如何配置和部署Nginx。
Nginx 緩存配置
可以參見官方文檔、Nginx緩存配置指南以及完整的配置示例
proxy_cache_path ... max_size=1g inactive=1w;
proxy_ignore_headers Cache-Control Expires Set-Cookie;
proxy_cache_valid 1m;
proxy_cache_use_stale error timeout updating
http_500 http_502 http_503 http_504; proxy_cache_background_update on;
配置的目標是最小化第三方服務的讀請求(如HTTP GET)。
如果緩存中不存在響應,則需要等待第三方響應,這也是我們需要儘可能避免的情況,這種現象可能發生在從未請求一個給定的URL或由於響應過期一週而被清除(inactive=1w
),或由於該響應是最新最少使用的,且達到了全局的緩存大小的上限(max_size=1g
)而被清除。
如果響應位於緩存中,當設置proxy_cache_background_update on
時,即使緩存的響應超過1分鐘,也會將其直接返回給客戶端。如果緩存的響應超過1分鐘(proxy_cache_valid 1m
),則後臺會調用第三方來刷新緩存。這意味着緩存內容可能並不與第三方同步,但最終是一致的。
當第三方在線且經常使用URLs時,可以認爲緩存的TTL是1分鐘(加上後臺緩存刷新時間)。這種方式非常適用於不經常變更的產品數據。
假設全局緩存大小沒有達到上限,如果一週內第三方不可達或出現錯誤,此時就可以使用緩存的響應。當一週內某個URL完全沒有被調用時也會發生這種情況。
爲了進一步降低第三方的負載,取消了URL的後臺並行刷新功能:
proxy_cache_lock on;
第三方API可能會在其響應中返回自引用絕對鏈接(如分頁鏈接),因此必須重寫URLs來保證這些鏈接指向正確的網關:
sub_filter 'https:\/\/$proxy_host' '$scheme://$http_host'; sub_filter_last_modified on;
sub_filter_once off;
sub_filter_types application/json;
由於sub_filter
不支持gzip
響應,因此在重寫URLs的時候需要禁用gzip
響應:
# Required because sub_filter is incompatible with gzip reponses: proxy_set_header Accept-Encoding "";
回到一開始的配置,可以看到啓用了proxy_cache_background_update
,該標誌會啓用後臺更新緩存功能,這種方式聽起來不錯,但也存在一些限制。
當一個客戶端請求觸發後臺緩存更新(由於緩存狀態爲STALE
)時,無需等待後臺更新響應就會返回緩存的響應(設置proxy_cache_use_stale updating
),但當Nginx後續接收到來自相同客戶端連接上的請求時,需要在後臺更新響應之後纔會處理這些請求(參見ticket)。下面配置可以保證爲每個請求都創建一條客戶端連接,以此保證所有的請求都可以接收到過期緩存中的響應,不必再等待後臺完成緩存更新。
# Required to ensure no request waits for background cache updates:
keepalive_timeout 0;
缺電是客戶端需要爲每個請求創建一個新的連接。在我們的場景中,成本要低得多,而且這種行爲也比讓一些客戶端隨機等待緩存刷新要可預測得多。
Kubernetes部署
上述Nginx配置被打包在了Nginx的非特權容器鏡像中,並跟其他web應用一樣部署在了Kubernetes集羣中。Nginx配置中硬編碼的值會通過Nginx容器鏡像中的環境變量進行替換(參見Nginx容器鏡像文檔)。
集羣中的網關通過Kubernetes Service進行訪問,網關pod的數量是可變的。由於Nginx 緩存依賴本地文件系統,這給緩存持久化帶來了問題。
非固定pod的緩存持久化
正如上面的配置中看到的,我們使用了一個非常長的緩存保留時間和一個非常短的緩存有效期來刷新數據(第三方可用的情況下),同時能夠在第三方關閉或返回錯誤時繼續使用舊數據提供服務。
我們需要不丟失緩存數據,並在Kubernetes pod擴容啓動時能夠使用緩存的數據。下面介紹了一種在所有Nginx實例之間共享持久化緩存的方式--通過在pod的本地緩存目錄和S3 bucket之間進行同步來實現該功能。每個Nginx pod上除Nginx容器外還部署了兩個容器,這兩個容器共享了掛載在/mnt/cache
路徑下的本地卷emptyDir,兩個容器都使用了AWS CLI容器鏡像,並依賴內部Vault來獲得與AWS通信的憑據。
init容器會在Nginx啓動前啓動,負責在啓動時將S3 bucket中保存的緩存拉取到本地。
aws s3 sync s3://thirdparty-gateway-cache /mnt/cache/complete
除此之外還會啓動一個sidecar容器,用於將本地存儲中的緩存數據保存到S3 bucket:
while true
do
sleep 600
aws s3 sync /mnt/cache/complete s3://thirdparty-gateway-cache
done
爲了避免上傳部分寫緩存條目到bucket,使用了Nginx的use_temp_path 選項(使用該選項可以將):
proxy_cache_path /mnt/cache/complete ... use_temp_path=on;
proxy_temp_path /mnt/cache/tmp;
默認的aws s3 sync
不會清理bucket中的數據,可以配置bucket回收策略:
<LifecycleConfiguration>
<Rule>
<ID>delete-old-entries</ID>
<Status>Enabled</Status>
<Expiration>
<Days>8</Days>
</Expiration>
</Rule>
</LifecycleConfiguration>
限制
如果可以接受最終一致性且請求是讀密集的,那麼這種解決方式是一個不錯的選項。但它無法爲很少訪問的後端提供同等的價值,也不支持寫請求(POST、DELETE等)。
鑑於使用了純代理方式,因此它不支持在第三方的基礎上提供抽象或自定義。
除非某種類型的客戶端服務認證(如通過服務網格頭)作爲緩存密鑰的一部分,否則會在所有客戶端服務之間共享緩存結果。這種方式可以提高性能,但也會給需要多級認證來訪問第三方數據的內部服務帶來問題。我們的場景中不存在這種問題,因爲生產數據對內部服務是公開的,且緩存帶來的"認證共享"只會影響讀請求。
在安全方面,還需要注意,任何可以訪問bucket的人都可以讀取甚至修改網關的響應。因此需要確保bucket是私有的,只有特定人員才能訪問。
集中式的緩存存儲會導致緩存共享(即所有pod會共享S3 bucket中的緩存,並在網關擴展時將緩存複製到pod中),因此這不是Nginx推薦的高可用共享緩存。未來我們會嘗試實現Nginx緩存的主/備架構。