快取的喜與憂

多年來,在 Amazon 建構服務時,我們經歷過以下場景的各種版本:我們建構一項新的服務,然後該服務需要進行一些網路叫用才能滿足其要求。也許這些叫用針對的是關聯式資料庫、Amazon DynamoDB 之類的 AWS 服務或其他內部服務。在簡單的測試或低請求率情況下,服務運作良好,但我們注意到即將出現一個問題。問題可能是,對其他服務的叫用速度很慢,或隨著叫用量的增加,擴展資料庫的成本昂貴。我們還注意到,許多請求正在使用相同的下游資源或相同的查詢結果,因此,我們認為對該資料進行快取可能是解決我們問題的答案。我們新增了快取,服務似乎有很大改進。我們觀察到,請求延遲減少、成本降低、下游可用性少量下降得到了緩解。一段時間後,沒有人會記得使用快取之前的狀況。相依項會相應減少其機群規模,並且資料庫會按比例縮小。當一切似乎都進展順利時,該服務可能會遭受災難。流量模式可能會發生變更,快取機群失敗,或可能導致快取變冷或不可用的其他非預期情況。反過來,這可能會引起流向下游服務的流量驟增,從而導致我們的相依項和服務中斷。

我們剛剛描述了一種陷入快取的服務。快取無意間從對服務的有益補充,升級為其營運能力的必要和關鍵部分。這個問題的核心是快取引入的強制回應行為,具體取決於是否快取給定物件。這種強制回應行為分散的非預期變化可能會導致災難。

在 Amazon 建構和營運服務的過程中,我們既體驗了快取的優點,也面臨著挑戰。本文其餘部分講述了我們的經驗教訓、最佳實踐,以及使用快取的注意事項。

在我們使用快取時

有幾個因素讓我們考慮要向系統新增快取。很多時候,這始於在給定請求速率下,對相依項延遲或效率的觀察。例如,這可能是在我們確定某個相依項可能開始節流,或無法跟上預期的負載時。我們發現,當我們遇到不均衡的請求模式而導致熱鍵/熱分區節流時,考慮使用快取很有幫助。若這種快取在請求間具有良好的 快取命中率,則來自此相依項的資料是進行快取的理想選擇。也就是說,相依項的叫用結果可用於多個請求或操作。若每個請求通常都需要對相依服務進行唯一查詢,並且每個請求的結果都是唯一的,則快取命中率可以忽略不計,且快取效果不好。第二項考量是如何對團隊服務容錯,及其用戶端實現 最終一致性。隨著時間的推移,快取資料必然會與來源增長不一致,因此,只有在服務及其用戶端進行相應補償的情況下,快取才會成功。來源資料的變更率,以及用於重新整理資料的快取政策將確定資料趨向於不一致的狀況。這兩個因素相互關聯。例如,相對靜態或變更緩慢的資料,快取時間可能更長。

本機快取

服務快取可在記憶體內或服務外部實作。盒上快取常見實作在程序記憶體中,實作起來相當快速簡易,又能以最少的工作帶來明顯的提升。一經識別出有快取處理的需要時,所實作和評估的第一種方式往往便是盒上快取。與外部快取明顯不同之處在於,不會帶來額外的操作負擔,因此整合進現有服務的風險相當低。我們經常將盒上快取實作成為記憶體內的雜湊表,其透過應用程式邏輯所管理 (例如,藉由服務呼叫完成之後,將結果明確置入快取) 或內嵌至服務用戶端 (例如,使用快取 HTTP 用戶端)。

儘管記憶體內快取有其優點而且簡單得誘人,但終究有一些缺點存在。其中一項包括所快取的資料會在其機群的伺服器之間不一致,形成快取一致性的問題。如果用戶端對服務重複發出呼叫,可能取決於碰巧是哪一部伺服器處理請求,而隨第一次呼叫得到所用較新的資料, 第二次呼叫得到較早的資料。

另一項缺點在於這時的下游負載按照服務的機隊大小成比例,於是隨著伺服器的數量成長,仍有可能造成相依服務疲於奔命。我們發現一種監控的有效方式,是針對快取命中/未中,以及對下游服務所作出的請求數量發出指標。

記憶體內快取也易發生「冷啟動」問題。這類問題會發生在新伺服器以全空快取啟動時,可導致相依服務於填充快取的過程中接收爆量請求。這會在部署過程中、或快取於全機隊排清的其他情況之下構成明顯的問題。快取一致性和空快取的問題經常能以使用請求聯合予以解決,將於本文後段詳細描述。

外部快取

外部快取可以解決我們剛剛討論的許多問題。外部快取會將快取資料儲存在單獨的機群,例如使用 Memcached 或 Redis由於外部快取保留了機群中所有伺服器使用的值,因此快取一致性問題有所減少。(請注意,這些問題並未完全消除,因為更新快取時可能會失敗。) 相較於記憶體內快取,下游服務的總負載有所降低,並且與機群規模不成比例。由於外部快取在整個部署過程中始終保持填充狀態,因此在部署等事件期間不會出現冷啟動問題。最後,外部快取比記憶體內快取提供更多的可用儲存空間,從而減少了因空間限制而導致快取移出的情況。

不過,外部快取有其自身的缺點。首先是增加了整體系統的複雜性和操作負載,因為有額外的機群來監控、管理和擴展。快取機群的可用性特徵,與其充當快取的相依服務有所不同。例如,若快取機群不支援零停機時間升級,並且需要維護時段,則其可用性通常會降低。

為防止由於外部快取而導致服務可用性下降,我們發現,必須新增服務代碼來處理快取機群不可用、快取節點失敗或快取放置/獲取失敗的情況。一種選擇是,回復以叫用相依服務,但我們了解到,採用這種方法時需要謹慎。在擴充快取中斷期間,這會使得下游服務的流量出現非典型尖峰,從而導致該相依服務節流或品質不佳,並最終降低可用性。我們更偏好將外部快取與記憶體內快取結合使用,若外部快取變得不可用,我們可以回復,或減少負載及限制傳送給下游服務的最大請求速率。我們在停用快取的情況下測試了服務行為,以驗證我們為防止相依項失效而採取的保護措施實際上是否按預期運作。

第二項考量是快取機群的擴展性和彈性。快取機群開始達到其請求速率或記憶體限制時,需要新增節點。我們確定哪些指標是這些限制的領先指標,因此可以相應地設定監控器和警報。例如,在我最近從事的一項服務中,我們的團隊發現,隨著 Redis 請求速率達到其極限,CPU 使用率變得很高。我們使用具有實際流量模式的負載測試,來確定限制並找到正確的警報閾值。

當我們為緩存機群新增容量時,我們會注意以不造成快取資料中斷或大量遺失的方式來執行此操作。不同的快取技術有獨特的考量。例如,某些快取伺服器不支援在沒有停機的情況下,將節點新增至叢集,而且並非所有快取用戶端庫都提供一致的雜湊,這對於將節點新增至快取機群,並重新分散快取資料是必需的。由於用戶端實作一致雜湊的可變性,以及對快取機群中節點的探索,我們在投入生產之前對新增和移出快取伺服器進行了全面測試。

使用外部快取時,我們會格外小心,以確保在變更儲存格式時的穩健性。快取資料將被視為持久儲存中的資料。我們確保更新的軟體始終可以讀取該軟體先前版本寫入的資料,並且確保較舊版本可以正常處理看到的新格式/欄位 (例如,在部署過程中,當機群混用了新舊代碼時)。當遇到非預期格式時,防止未攔截的異常對於阻止破壞式防禦很有必要。然而,這還不足以防止所有與格式相關的問題。偵測版本格式不相符的情況,並丟棄快取資料可能會導致大量重新整理快取,這可能致使相依服務節流或品質不佳。如需關於序列化格式問題的深入探討資訊,請參閱在部署期間確保轉返安全一文

外部快取的最後一項考量是,它們由服務機群中的個別節點更新。快取通常不具備條件式認沽和交易等功能,因此我們要注意確保快取更新代碼正確無誤,並且絕不會讓快取處於無效或不一致狀態。

內嵌快取與端快取

在評估不同的快取方法時,我們需要做出的另一個決定是,在內嵌快取與端快取之間進行選擇。內嵌快取或通讀/通寫式快取,將快取管理嵌入主資料存取 API 中,從而讓快取管理成為該 API 的實作細節。範例包括應用程式特定實作,如 Amazon DynamoDB Accelerator (DAX),以及基於標準的實作,如 HTTP 快取 (使用本機快取用戶端或外部快取伺服器,如 Nginx 或 Varnish)。相比之下,端快取則為通用物件儲存,如 Amazon ElastiCache (Memcached and Redis) 提供的物件儲存,或是 Ehcache 和 Google Guava 等用於記憶體內快取的庫。使用端快取,應用程式代碼可在叫用資料來源之前和之後直接操控快取,在進行下游叫用之前檢查快取物件,並在這些叫用完成之後將物件放入快取。

內嵌快取的主要優點是,為用戶端提供了統一的 API 模型。無須對用戶端邏輯做出任何變更,即可新增、移除或調整快取。內嵌快取還將快取管理邏輯從應用程式代碼中拉出,從而消除了潛在錯誤的來源。HTTP 快取之所以特別具有吸引力,是因為有許多現成的可用選項,例如記憶體內的庫,如前所述的獨立 HTTP 代理,以及內容交付網絡 (CDN) 之類的受管服務。

然而,內嵌快取的透明性也可能會降低可用性。現在,外部快取已成為這種相依項可用性方程的一部分。用戶端沒有機會補償暫時不可用的快取。例如,若您有一個 Varnish 機群來快取來自外部 REST 服務的請求,那麼從服務的角度來看,若該快取機群發生故障,就像是相依項本身發生故障。內嵌快取的另一個缺點是,它需要內嵌至要為其快取的通訊協定或服務中。若沒有可用於通訊協定的內嵌快取,除非您想自己建構整合式用戶端或代理服務,否則無法選擇內嵌快取。

快取到期

某些最具挑戰性的快取實作細節是選擇正確的快取大小、到期政策和移出政策。到期政策確定將項目保留在快取中的時間。最常用政策使用基於絕對時間的到期時間 (即,在每個物件載入時將存留時間 (TTL) 與其關聯)。根據用戶端要求 (例如,用戶端對過時資料的容忍程度,以及資料的靜態程度) 選擇 TTL,因為這樣可以更積極地快取緩慢變更的資料。理想的快取大小取決於對預期請求量,以及分散這些請求中快取物件的強制回應。我們據此估計快取大小,以確保這些流量模式具有較高快取命中率。移出政策控制當項目達到容量限制時,如何從快取中移除項目。最常用的移出政策是近來最少使用 (LRU)。

到目前為止,這只是一個思維訓練。實際流量模式可能與強制回應不同,因此我們將跟踪快取的實際效能。我們執行此操作的偏好方式是,根據快取命中數和未命中數、快取總大小,以及對下游服務的請求數目發出服務指標。

我們了解到,我們需要慎重選擇快取大小和到期政策值。我們希望避免以下情況:開發人員在初始實作期間,任意選擇一些快取大小和 TTL 值,之後再也不重新設定和驗證其適用性。我們已經看到,由於缺乏後續措施導致臨時服務中斷和持續中斷加劇的實際範例。

當下游服務不可用時,我們用於提高彈性的另一種模式是使用兩個 TTL:一個軟 TTL 和一個硬 TTL。用戶端將嘗試基於軟 TTL 來重新整理快取項目,但如果下游服務不可用,或以其他方式未回應請求,則會繼續使用現有的快取資料,直至達到硬 TTL。AWS Identity and Access Management (IAM) 用戶端中使用了此模式的範例。

此外,我們還使用具有背壓的軟 TTL 和硬 TTL 方法,來減少對下游服務品質不佳的影響。下游服務在品質不佳時,可以用背壓事件來回應,這表明叫用服務應使用快取資料,直至達到硬 TTL,然後僅請求不在其快取中的資料。我們繼續執行此操作,直至下游服務消除背壓。這種模式允許下游服務從品質不佳中復原,同時保持上游服務的可用性。

其他考量

一項重要的考量是,從下游服務接收到錯誤時快取有哪些行為。處理此情況的一種方法是,使用最後快取的有效值回覆用戶端,例如,利用前述軟 TTL/硬 TTL 模式。我們採用的另一種選擇是,使用與正確快取項目不同的 TTL 來快取錯誤回應 (即使用「錯誤快取」),並將錯誤傳播至用戶端。我們在給定情況下選擇的方法取決於服務的具體情況,並依據對用戶端最好何時看到過時資料與錯誤的評估來決定。無論採用哪種方法,務必確保在發生錯誤的情況下,某些內容包含在快取中。若不是這樣,並且下游服務暫時不可用,或者無法滿足某些請求 (例如,刪除下游資源時),上游服務將繼續使用流量轟炸,並可能導致服務中斷或加劇現有情況。我們已經探討了實際範例,若無法快取錯誤回應,則會導致失敗率和故障率增加。

安全性是快取的另一個重要方面。當我們將快取引入服務時,我們會評估並減輕其帶來的任何其他安全風險。例如,外部快取機群通常缺乏針對序列化資料和傳輸級安全性的加密。若敏感使用者資訊保留在快取中,則這尤其重要。可以透過使用 Amazon ElastiCache for Redis 之類的工具來緩解此問題,該工具支援在途加密和靜態加密。快取也容易受到破壞攻擊,其中下游通訊協定中的漏洞讓攻擊者可以用在其控制下的值來填充快取。這會放大攻擊的影響,因為當該值保留在快取中時,發出的所有請求都會看到惡意值。對於最後一個範例,快取也容易受到旁路定時攻擊的影響。快取的值比未快取的值傳回得更快,因此攻擊者可以利用回應時間,來獲取其他用戶端或原則發出的請求相關資訊。

最後的考量是「驚群效應」情況,在這種情況下,許多用戶端約在同一時間發出需要未快取的相同下游資源的請求。當伺服器啟動並使用空的本機快取加入機群時,也會發生這種情況。這會導致每個伺服器的大量請求進入下游相依項,從而引起節流/品質不佳。為解決此問題,我們使用請求聯合,其中伺服器或外部快取確保未快取資源只有一個掛起的請求。一些快取庫為請求聯合提供支援,另一些外部內嵌快取 (例如 Nginx 或 Varnish) 也如此。此外,可以在現有快取的頂部實作請求聯合。 

Amazon 最佳實務和注意事項

本文介紹了幾種 Amazon 最佳實務,以及與快取關聯的權衡和風險。下面總結了我們的團隊在引入快取時使用的 Amazon 最佳實務和考量:

• 確保存在合理的快取需求,在成本、延遲及/或可用性方面均得到改善。確保資料可快取,這意味著可以在多個用戶端請求中使用該資料。對快取將帶來的價值持懷疑態度,並仔細評估其好處是否會超越快取帶來的附加風險。
• 計劃以與其餘服務機群和基礎架構相同的嚴格性和程序來操作快取。別小看這項工作。發出有關快取利用率和命中率的指標,以確保適當調整快取。監控關鍵指標 (例如 CPU 和記憶體),以確保外部快取機群運作狀態良好且適當擴展。在這些指標上設定警報。確保可以擴展快取機群而不會造成停機或大量快取失效 (即驗證一致的雜湊是否按預期運作)。
• 在選擇快取大小、到期政策和移出政策時要謹慎且經驗豐富。執行測試並使用上一個要點中提到的指標,來驗證和調整這些選擇。
• 確保在快取不可用的情況下您的服務具有彈性,其中包括導致無法使用快取資料處理請求的各種情況。這些包括冷啟動、快取機群中斷、流量模式變更或下游中斷。在許多情況下,這可能意味著要以部分可用性,來換取確保伺服器和相依服務不會用盡 (例如,通過減少負載、限制對相依服務的請求,或提供過時資料)。在停用快取的情況下執行負載測試來驗證這一點。
• 考慮維護快取資料的安全性方面,包括加密、與外部快取機群通訊時的傳輸安全性,以及快取破壞攻擊和旁路攻擊的影響。
• 設計快取物件的儲存格式,以隨著時間的推移而演進 (例如,使用版本號),並編寫能夠讀取較舊版本的序列化代碼。在快取序列化邏輯時注意破壞式防禦。
• 評估快取如何處理下游錯誤,並考慮使用獨特的 TTL 來維護錯誤快取。透過重複請求相同的下游資源並丟棄錯誤回應,避免造成中斷或加劇中斷。

Amazon 的許多服務團隊都使用快取技術。雖然這些技術具有很多優點,但我們還是決定不輕易合併緩存,因為缺點通常會超過優點。我們希望本文對您評估自己服務中的快取會有所幫助。


作者簡介

Matt 是 Amazon Emerging Devices 部門的首席工程師,致力於即將推出的消費裝置的相關軟體和服務。在此之前,他曾在 AWS Elemental 任職,領導團隊推出了 MediaTailor,這是一種針對即時影片和隨需影片的伺服器端個人化廣告服務。在此過程中,他還協助發佈了 PrimeVideo 第一季的 NFL Thursday Night Football 串流。在加入 Amazon 之前,Matt 在安全產業工作了 15 年,包括在 McAfee、Intel 和一些新創公司,從事企業安全管理、反惡意軟體和反漏洞技術、硬體輔助安全措施以及 DRM 的工作。

Jas Chhabra 是 AWS 的首席工程師。他於 2016 年加入 AWS,並在 AWS IAM 工作了幾年,然後才擔任目前在 AWS Machine Learning 的職務。在加入 AWS 之前,他曾在 Intel 擔任過物聯網、身分驗證和安全領域的各種技術職務。目前的興趣領域是機器學習、安全性和大規模分散式系統。過去曾專注於物聯網、比特幣、身分驗證和加密。他擁有電腦科學碩士學位。

在分散式系統中避免回復 使用負載卸除以免過載 在部署期間確保轉返安全