使用Kubernetes中的Nginx來改善第三方服務的可靠性和延遲

charlieroro發表於2023-02-14

使用Kubernetes中的Nginx來改善第三方服務的可靠性和延遲

譯自:How we improved third-party availability and latency with Nginx in Kubernetes

本文討論瞭如何在Kubernetes中透過配置Nginx快取來提升第三方服務訪問的效能和穩定性。這種方式基於Nginx來實現,優點是不需要進行程式碼開發即可實現快取第三方服務的訪問,但同時也缺少一些定製化擴充套件。不支援快取寫操作,多個pod之間由於使用了集中式共享方式,因而快取缺乏高可用。

image

使用Nginx作為閘道器來快取到第三方服務的訪問

第三方依賴

技術公司越來越依賴第三方服務作為其應用棧的一部分。對外部服務的依賴是一種快速擴充並讓內部開發者將精力集中在業務上的一種方式,但部分軟體的失控可能會導致可靠性和延遲降級。

在Back Market,我們已經將部分產品目錄劃給了一個第三方服務,我們的團隊需要確保能夠在自己的Kubernetes叢集中快速可靠地訪問該產品目錄資料。為此,我們使用Nginx作為閘道器代理來快取所有第三方API的內部訪問。

image

多叢集環境中使用Nginx作為閘道器來快取第三方API的訪問

使用結果

在我們的場景下,使用閘道器來快取第三方的效果很好。在執行幾天之後,發現內部服務只有1%的讀請求才需要等待第三方的響應。下面是使用閘道器一週以上的服務請求響應快取狀態分佈圖:

image

HIT:快取中的有效響應 ->使用快取

STALE:快取中過期的響應 ->使用快取,後臺呼叫第三方

UPDATING:快取中過期的響應(後臺已經更新) ->使用快取

MISS:快取中沒有響應 ->同步呼叫第三方

即使在第三方下線12小時的情況下,也能夠透過快取保證96%的請求能夠得到響應,即保證大部分終端使用者不受影響。

內部閘道器的響應要遠快於直接呼叫第三方API的方式(第三方位於Europe,呼叫方位於US)。

image

以 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通訊的憑據。

image

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快取的主/備架構

相關文章