如何藉助Cloud Run和Redis將p95延遲從10秒降低至2秒

如何藉助Cloud Run和Redis將p95延遲從10秒降低至2秒

想象一下,在旅遊網站上搜尋航班,結果載入需要等待 10 秒。感覺像一個世紀那麼長,對吧?現代旅遊搜尋平臺即使在高負載下也必須幾乎即時返回結果。然而,不久前,我們旅遊搜尋引擎的 API 的 p95 延遲徘徊在 10 秒左右。這意味著 5% 的使用者搜尋(通常是在高峰流量期間)需要 10 秒或更長時間。結果——使用者失望、跳出率高,更糟的是——銷售額損失。因此,在這種情況下,降低延遲是不可妥協的。

本文是一個真實案例研究,介紹了我們如何改進雲基礎架構以消除延遲問題。透過利用 Google Cloud Run 進行可擴充套件計算並使用 Redis 進行智慧快取,我們將搜尋 API 的 p95 延遲從約 10 秒縮短至約 2 秒。在這裡,我們將介紹降低延遲的整個過程。其中包括效能瓶頸、最佳化以及它們帶來的顯著改進。

延遲瓶頸

極高的延遲是一個嚴重的問題。深入研究後,我們發現多個因素拖累了我們的響應時間。所有這些因素都有一個共同點——它們使我們的搜尋 API 在每個請求上都承擔了大量的繁重工作。在實現整體延遲降低之前,我們必須解決以下問題:

延遲瓶頸

  • 多次後端呼叫:對於每個使用者查詢,該服務都會按順序聯絡多個下游服務(機票價格提供商、輔助資訊資料庫等)。例如,搜尋航班有時會同時呼叫三個不同的 API,每個 API 大約需要 2 到 3 秒。某些搜尋的總延遲時間累積接近 10 秒。
  • 無快取層:由於沒有記憶體或 Redis 快取來快速返回最新結果,網站上的每個請求都從頭開始。即使是相同的搜尋也會在幾分鐘內重複。這意味著即使是熱門航線或靜態資料(如機場詳情),每次都需要從資料庫或第三方獲取。
  • Cloud Run 冷啟動:我們的服務在 Cloud Run(一個無伺服器容器平臺)上執行。在預設設定(最小例項數為 0)下,當流量空閒且有新請求傳入時,Cloud Run 必須啟動一個容器例項。這些冷啟動會增加明顯的延遲(通常會增加 1-2 秒的開銷)。這種“啟動稅”嚴重損害了我們的尾部延遲。
  • 單個請求:最初,我們將每個 Cloud Run 容器配置為一次僅處理一個請求(併發數 = 1)。這簡化了請求處理,但也意味著 10 個併發搜尋的突發情況會立即啟動 10 個獨立例項。由於冷啟動和每個例項的 CPU 資源有限,我們的系統難以高效處理峰值請求。

所有這些因素共同導致了 p95 延遲過慢。從根本上講,我們的架構並未針對速度進行最佳化。查詢執行了冗餘工作,而我們的基礎架構並未針對延遲敏感的工作負載進行調整。好訊息是?每個瓶頸都是降低延遲的機會。

我們為降低延遲所做的改進

我們主要從兩個方面來降低延遲:快取以避免重複工作;Cloud Run 最佳化以最大限度地減少冷啟動和處理開銷。後端的改進過程如下:

引入Redis快取層

我們部署了 Redis 快取,以縮短熱路徑上高開銷操作的執行時間。其理念非常簡單:儲存頻繁或近期查詢的結果,並直接將其提供給後續請求。例如,當使用者搜尋特定日期從紐約飛往倫敦的航班時,我們的 API 會獲取並編譯一次結果。然後,它會將該“票價響應”快取在 Redis 中一小段時間。

如果另一個使用者(或同一個使用者)隨後進行了相同的搜尋,後端可以在幾毫秒內返回快取的票價資料,從而避免重複呼叫外部 API 和資料庫查詢。透過避免在快取命中時進行高開銷的上游呼叫,我​​們顯著降低了熱查詢的延遲。

我們也對其他資料應用了快取,例如靜態或緩慢變化的參考資料。例如機場程式碼、城市後設資料、貨幣匯率,現在都使用快取。服務現在不再每次請求都訪問資料庫查詢機場資訊,而是從 Redis 中檢索(在啟動或首次使用時載入)。這減少了許多細微的查詢操作,這些查詢操作會不時增加幾毫秒的時間(在負載下這些時間會累積起來)。

快取必備

根據經驗,我們決定“快取熱門資料”。熱門航線、最近獲取的價格以及機場資訊等靜態參考資料都儲存在記憶體中,方便隨時訪問。為了保持快取資料的新鮮度(在價格變化時至關重要),我們設定了合理的 TTL(生存時間)到期時間和失效規則。例如,票價搜尋結果最多快取幾分鐘。

之後,它們就會過期,以便新的搜尋能夠獲得最新的價格。對於高度波動的資料,我們甚至可以在檢測到更改時主動使快取條目失效。正如 Redis 文件所述,機票價格通常“每隔幾個小時”才會更新一次。因此,較短的 TTL 與基於事件的失效機制相結合,可以在新鮮度和速度之間取得平衡。

結果如何?快取命中後,每個查詢的響應時間從幾秒縮短到幾百毫秒甚至更短。這全都歸功於 Redis,它能夠透過記憶體以極快的速度提供資料。事實上,行業報告顯示,使用記憶體中的“票價快取”可以將需要數秒的航班查詢轉化為僅需幾十毫秒的響應。雖然我們的結果並非完全即時,但這個快取層帶來了巨大的提升。顯著降低了延遲,尤其是在重複搜尋和熱門查詢方面。

最佳化Cloud Run設定以降低延遲

快取有助於處理重複性工作,但我們還需要最佳化首次查詢和擴充套件的效能。因此,我們對 Cloud Run 服務進行了微調,以實現低延遲。

始終只有一個熱例項

我們為 Cloud Run 服務啟用了最小例項數 = 1。這確保即使在空閒期間也至少有一個容器處於啟動狀態並準備好接收請求。第一個使用者請求不再會受到冷啟動懲罰。Google 工程師指出,保持最小例項數可以顯著提高對延遲敏感的應用的效能,因為它消除了從零到一的啟動延遲。

在我們的案例中,將最小例項數設定為 1(高峰時段甚至可以設定為 2 或 3),意味著使用者無需等待容器啟動。僅憑這一項最佳化,p95 延遲就顯著下降。

提高每個容器的併發數

我們重新審視了併發設定。在確保程式碼能夠安全地處理並行請求後,我們將 Cloud Run 的併發數從 1 提升到更高的數字。我們嘗試了 5、10 等值,最終將我們的工作負載設定為 5。這意味著每個容器在需要啟動新例項之前最多可以同時處理 5 個搜尋請求。

結果——流量高峰期間生成的新例項更少。這反過來又意味著更少的冷啟動和更低的開銷。本質上,我們讓每個容器並行執行更多工作,直到 CPU 使用率仍然保持健康。我們密切監控 CPU 和記憶體——我們的目標是高效利用每個例項,而不會使其過載。

這種調整有助於平滑突發流量下的延遲:如果同時有 10 個請求,而不是 10 個冷啟動(併發度 = 1),我們會使用 2 個熱例項,每個例項處理 5 個請求,從而保持快速響應。

更快的啟動和處理速度

我們還進行了一些應用級別的調整,以加快 Cloud Run 的啟動和執行速度。此外,我們還啟用了 Cloud Run 的啟動 CPU 提升功能,該功能可在啟動期間為新例項提供 CPU 的爆發式增長。我們還使用了精簡的基礎容器映象,並在啟動時僅載入必要的模組。

某些初始化步驟(例如載入大型配置檔案或預熱某些快取)也被移至容器啟動​​階段,而不是在請求時執行。得益於最小例項數,這種啟動過程的執行頻率很低。實際上,當請求到達時,例項已經完成引導(資料庫連線開啟、配置載入等),因此它可以立即開始處理查詢。

我們實際上只支付了一次啟動成本,然後在多個請求中重複使用它,而不是在每個請求上都支付少量成本。

實施這些最佳化後,效果立竿見影。我們監控了 API 的效能,對比了最佳化前後的對比。p95 延遲從大約 10 秒驟降至 2 秒左右。對於我們的使用者來說,這帶來了令人驚歎的 5 倍載入速度提升。平均延遲也得到了改善(對於快取命中查詢,延遲通常小於 500 毫秒)。

更重要的是,響應變得一致且可靠。使用者不再需要忍受痛苦且令人恐懼的 10 秒等待。系統能夠從容應對流量高峰:Cloud Run 可以根據需要擴充套件至更多例項。憑藉熱容器和更高的併發性,系統能夠輕鬆應對,避免冷啟動帶來的擁堵。

同時,Redis 快取吸收了重複查詢,並降低了下游 API 和資料庫的負載。這還透過防止這些系統成為瓶頸,間接降低了延遲。

最終結果是,搜尋 API 更加敏捷、更具可擴充套件性,滿足了客戶對快速響應和流暢體驗的期望。

關鍵要點

在我們為降低延遲而進行的一系列最佳化中,以下是一些關鍵要點和值得您考慮的要點。

  • 測量並控制尾部延遲:關注 p95 延遲(及以上)至關重要。它能凸顯真實使用者感受到的最壞延遲情況。將 95 百分位延遲從 10 秒降低到 2 秒,使我們最糟糕的體驗提升了 5 倍,速度也更快。這對使用者滿意度來說是一個巨大的提升!因此,請始終監控這些高百分位指標,而不僅僅是平均值。
  • 使用快取避免冗餘工作:事實證明,引入 Redis 快取對我們來說至關重要。透過從記憶體提供結果,快取頻繁請求的資料可以顯著縮短響應時間。記憶體速度與周到的失效機制(使用 TTL 和更新)相結合,可以減輕後端昂貴的計算負擔。
  • 最佳化無伺服器以提高速度:Cloud Run 讓我們能夠輕鬆擴充套件,但為了真正實現低延​​遲,我們利用了其顯著的功能——保持最小例項預熱。這消除了冷啟動延遲,並調整了併發性和資源,使例項能夠高效利用,避免不堪重負。少量的前期成本(始終線上的例項)可以非常值得,因為它能帶來穩定的效能。
  • 儘可能並行化和精簡:我們重新審視了請求流程,以消除不必要的序列化和延遲。透過並行化外部呼叫和在啟動期間進行一次性設定(並非針對每個請求),我們將關鍵路徑縮短了數秒。每個微最佳化(非阻塞 I/O、更快的程式碼、預載入資料)都會在大規模分散式系統中累積起來。
  • 持續分析和迭代:最後,需要注意的是,這是一個迭代的過程。我們使用監控和分析來查詢最大的瓶頸,逐一解決它們,並衡量其影響。效能調整很少是一次性完成的。它關乎資料驅動的改進,有時還會進行創造性的修復,以達到您的延遲目標。

小結

雖然這些降低延遲的策略似乎難以一次性全部實現,但在實踐中,系統地逐一檢查每個策略都非常順利。整個測試過程中,我們最大的亮點在於,我們將旅行搜尋 API 從遲緩的體驗提升到了即時的體驗。在使用者期望“昨天”就能得到答案的時代,將 P95 延遲從 10 秒縮短到 2 秒,對於提供流暢的旅行搜尋體驗至關重要。

評論留言