1.擴展內存緩存的挑戰
我們用于與各種程序化和需求方平臺 (DSP) 集成的應用程序之一是低延遲、高吞吐量的基于 JVM 的應用程序。這是 付款憑單(DV)付前前驗證解決方案的核心組件。自多年前成功推出此解決方案以來,我們不斷添加多項關鍵功能,同時保持嚴格的延遲 SLA 并每天處理數千億個請求。
近年來,用于投標前評估的測量數據(欺詐、品牌安全等)的大小大幅增長。隨著數據集的增長,我們的內存緩存的大小也在增長。有一次,我們面臨著一個關鍵挑戰,即擴展我們最大的內存緩存以支持可能將我們的緩存大小增加四倍的新功能發布。我們有嚴格的低延遲響應 SLA??、要支持的各種部署選項,以及低于 32GB 的有限 JVM 分配以使用壓縮對象指針。在評估了幾個選項后,我們決定使用 Chronicle Software 的開源 ChronicleMap 作為我們的解決方案。
在這篇博文中,我將討論我們評估各種開源和企業解決方案的過程,以及我們最終選擇 ChronicleMap 的原因。我還將分享我們的最佳實踐和經驗教訓。
2.當前的問題
由于上述原因,我們現有的緩存解決方案無法處理增加的工作負載,因此我們需要一種可以擴展且仍能滿足嚴格的延遲和部署要求的新解決方案。該應用程序服務于兩種主要類型的競價前集成:— (1) 緩存解決方案,DSP 在其終端緩存我們的響應,因此沒有嚴格的延遲要求;(2) 其他集成,相比之下,它們使用實時競價流響應,要求端到端延遲小于 10 毫秒。
此應用程序需要在 付款憑單(DV)、云端甚至我們一些合作伙伴的本地托管,以使要求更加嚴格。我們可能擁有多個版本的應用程序,但決定只使用一個版本,以降低部署和維護的復雜性、我們的發布管道和可維護性。因此,我們選擇的任何解決方案都需要符合上述要求。我們評估了幾個選項,以找到最適合我們需求的選項。
3.評估各種開源和企業解決方案
為了找到滿足我們要求的解決方案,我們評估了幾種開源和企業選項,包括:
- Redis — 一種流行的開源內存鍵值數據存儲。
- EhCache — 一種廣泛使用的開源 Java 緩存。
- Aerospike — 一種支持高吞吐量、低延遲緩存的企業級分布式緩存解決方案。
- Hazelcast — 一種用于分布式計算和內存緩存的開源解決方案。
- ChronicleMap — 一種專為低延遲和/或多進程應用程序設計的內存鍵值存儲。
4.為什么我們決定使用 ChronicleMap?
我們研究并評估了上述選項的優點和局限性。雖然每種方案都有其優點,但我們需要針對我們獨特的需求組合而制定非常具體的方法。
我們排除了需要進行進程外查找的任何解決方案,因為與訪問本地 RAM 的速度相比,完成查找需要額外的時間。
這可能會破壞我們 10 毫秒的整體端到端延遲要求。此外,在每個地區為這些解決方案創建服務器集群會給我們增加大量的維護和基礎設施成本開銷,更重要的是,也會給托管我們應用程序的本地合作伙伴增加成本開銷。因此,我們排除了 Redis 和 Aerospike。
雖然 Terracotta 的 EhCache 似乎為較小的緩存提供了良好的性能,但由于其在擴展大型緩存方面的局限性以及開源版本不支持堆外存儲,它并不適合我們的需求。Terracotta 的堆外存儲解決方案 BigMemory 僅作為商業解決方案提供。
Hazelcast 表現出良好的性能和可擴展性,但需要進行大量配置和調整才能實現最佳性能。此外,Hazelcast 的堆外解決方案高密度存儲僅在商業上可用。
相比之下,ChronicleMap 脫穎而出,成為最適合我們需求的解決方案,因為它滿足了我們的核心要求。
- 它針對低垃圾收集進行了優化,并提供高性能數據存儲、檢索和迭代。
- 它可以作為我們應用程序的一部分實施,使我們能夠保持部署簡單,而無需為外部緩存維護單獨的服務器集群。
- 它被實現為內存中的堆外解決方案,可以使用磁盤進一步擴展。由于它是堆外的,- 我們可以擴展內存緩存,同時將應用程序的堆內內存分配保持在 32GB 以下。
- 開源版本中提供的功能似乎足以滿足我們的要求 - 主要是內存、堆外映射和持久化到磁盤,以實現快速應用程序重啟。
我們還發現,ChronicleMap 在全球銀行和對沖基金的生產中被廣泛使用,它們必須處理與 付款憑單(DV) 類似的規模。我們覺得它很合適。我們實施了概念驗證 (POC),結果令人鼓舞。此時,我們對選擇 ChronicleMap 來擴展我們的內存緩存充滿信心。
5.經驗和最佳實踐
隨著數據集的增長,我們能夠擴展內存緩存,而不會犧牲低延遲響應時間。
以下是我們通過利用 ChronicleMap 提供的一些功能在應用程序中看到的顯著改進:
- 縮放數據集的能力。在我們成功將頁面分類類別移至堆外緩存并啟動依賴于擴展的功能后,我們開始遷移其他數據集。與原始 HashMap 的查找延遲相比,ChronicleMap 緩存的查找延遲增加了可忽略不計的額外時間(納秒)。我們最終能夠支持在需要時按多個因子擴展每個數據集,這是一個巨大的好處。
- 改進的垃圾收集。在將許多數據集移至 ChronicleMap 后,我們注意到垃圾收集減少方面取得了顯著進步。由于堆上的大部分內存被移至堆外,JVM 內存占用減少導致垃圾收集周期更少且速度更快。
- 提高代碼可維護性。在使用 ChronicleMap 之前,我們必須為每個內存緩存維護兩個副本。一個是用于處理 API 請求的“主動”緩存,另一個是用于數據更新的“被動”緩存。由于應用程序使用無鎖算法來降低延遲,因此我們需要保留單獨的緩存,并在每個數據更新周期后以原子操作交換它們。 ChronicleMap 的優點之一是它能夠以高性能處理并發訪問,而不會阻塞。這使我們能夠保留堆外緩存的單個副本并在保持低延遲的同時處理高吞吐量請求。我們能夠重構并刪除圍繞維護兩個副本并在每次數據更新后交換它們的所有冗余代碼。
- 降低代碼復雜性。在使用 ChronicleMap 之前,我們被迫采用多種方法來優化內存利用率,即使要付出額外查找的代價。其中一個例子就是頁面類別緩存的布局。由于每個分類的 URL 都分配了不同數量的類別,為了節省內存,我們根據 URL 的分布及其分配的類別數量創建了預先分配內存的存儲桶,例如,500 萬個 URL 的類別少于 10 個,屬于一個存儲桶。相比之下,少于 100 萬的 URL 最多有 50 個類別,屬于另一個存儲桶。每個存儲桶都是一個 HashMap,其中的鍵是 URL 哈希,值是連續數組的索引,該數組分為固定數量的類別。如果我們為每個 URL 使用單獨的數組,則每個存儲桶的大型連續數組旨在大幅減少所需的引用數量。如果沒有這個,數百萬次引用將大大延長任何完整的垃圾收集過程。還有另一個 HashMap 將 URL 映射到特定的存儲桶。此外,正如上文所述,我們必須維護兩個副本。我們可以使用更簡單的 ChronicleMap 消除所有這些代碼復雜性,因為我們不再需要擔心內存問題。
6.優點
6.1.更快的啟動速度
ChronicleMap 支持將內存緩存持久化到文件中。這樣,數據就可以在創建過程結束后繼續存在 — 例如,支持熱應用程序重新部署。我們利用此功能快速重啟應用程序,而無需對我們的緩存層進行 API 調用來加載初始數據。
6.2.共享緩存
在我們當前的設計中,每個 API 后端服務器都維護自己的 ChronicleMap 緩存。ChronicleMap 支持在不同的 JVM 進程之間共享相同的緩存。創建外部服務以將源數據移動到可在多個后端服務器之間共享的單個共置 ChronicleMap 緩存中將有助于我們降低網絡利用率。
6.3.自動調整大小
可以自動調整 ChronicleMap 緩存的大小。
7.不足
7.1.根據估算進行預分配
我們評估的版本沒有動態調整緩存大小的功能。我們需要預測數據集的增長并預先分配適當的內存量。對于上面提到的頁面類別緩存,我們還需要估計每頁的平均類別數并使用該數來分配內存。我們必須持續監控這一點以確保我們的假設成立。如果頁面類別的分布發生劇烈變化,則需要重新配置和重新部署應用程序。
7.2.基準
當 ChronicleMap 保存的數據可以裝入 RAM 中時,其性能最佳。但是,使用磁盤可以保存比 RAM 更大的數據。當數據無法裝入 RAM 中時,查找速度會變慢。對各種配置進行基準測試對我們來說并不容易,我們必須構建一個自定義測試應用程序來針對不同場景運行多個測試。
7.3.文檔
公開的產品文檔并未涵蓋所有細節。我們在源代碼中發現了多個有用的配置設置,而 Javadoc 文檔正是從這些設置中自動生成的。源代碼還被劃分為許多模塊和項目,這讓我們發現很難瀏覽。
7.4.處理自定義值
我們的要求之一是支持多映射,這是一種關聯容器,其中可以關聯多個值并返回給定鍵的值。我們必須編寫代碼來支持這一點。這還需要一個自定義序列化器代碼,這是我們自己實現的。后來,我們在代碼中發現了另一個可以擴展的類,于是我們改用這個類。不知道它的存在以及如何使用它,這延遲了我們的實現。