1. 程式人生 > >馬蜂窩推薦系統容災快取服務的設計與實現

馬蜂窩推薦系統容災快取服務的設計與實現

資料庫突然斷開連線、第三方介面遲遲不返回結果、高峰期網路發生抖動...... 當程式突發異常時,我們的應用可以告訴呼叫方或者使用者「對不起,伺服器出了點問題」;或者找到更好的方式,達到提升使用者體驗的目的。

 

一、背景

使用者在馬蜂窩 App 上「刷刷刷」時,推薦系統需要持續給使用者推薦可能感興趣的內容,主要分為根據使用者特性和業務場景,召回根據各種機器學習演算法計算過的內容,然後對這些內容進行排序後返回給前端這幾個步驟。

推薦的過程涉及到 MySQL 和 Redis 查詢、REST 服務呼叫、資料處理等一系列操作。對於推薦系統來說,對時延的要求比較高。馬蜂窩推薦系統對於請求的平均處理時延要求在 10ms 級別,時延的 99 線保持在 1s 以內。

當外部或者內部系統出現異常時,推薦系統就無法在限定時間內返回資料給到前端,導致使用者刷不出來新內容,影響使用者體驗。

所以我們希望通過設計一套容災快取服務,實現在應用本身或者依賴的服務發生超時等異常情況時,可以返回快取資料給到前端和使用者,來減少空結果數量,並且保證這些資料儘可能是使用者感興趣的。

 

二、設計與實現

設計思路和技術選型

不僅僅是推薦系統,快取技術在很多系統中已經被廣泛應用,小到 JVM 中的常用整型數,大到網站使用者的 session 狀態。快取的目的不盡相同,有些是為了提高效率,有些是為了備份;快取的要求也高低不一,有些要求一致性,有些則沒有要求。我們需要根據業務場景選擇合適的快取方案。

結合到我們上面提到的業務場景和需求,我們採用了基於 OHC 堆外快取和 SpringBoot 的方案,實現在現有推薦系統中增加本地容災快取系統。主要是考慮到以下幾點因素:

1. 避免影響線上服務,將業務邏輯和快取邏輯隔離

為了不影響線上服務,我們將快取系統封裝為一個 CacheService,配置在現有流程的末端,並提供讀、寫的 API 給外部呼叫,將業務邏輯和快取邏輯隔離。

2. 非同步寫入快取,提高效能

讀、寫快取都會帶來時間消耗,特別是寫入快取。為了提高效能,我們考慮將寫入快取做成非同步的方式。這部分使用的是 JDK 提供的執行緒池 ThreadPoolExecutor 來實現,主執行緒只需要提交任務到執行緒池,由執行緒池裡的 Worker 執行緒實現寫入快取。

3. 本地快取,提高訪問速度

在推薦系統中,給使用者推薦的內容應該是千人千面的,甚至同一位使用者每次重新整理看到的內容都可能不同,這就不要求快取具有強一致性。因此,我們只需要進行本地快取,而不需要採用分散式的方式。這裡使用到的是開源快取工具 OHC,快取的資料來源於成功處理過的請求。

4. 備份快取例項,保證可用性

為了保證快取的可用性,我們不僅在記憶體中進行快取,還定時備份到檔案系統中,從而保證在可以應用啟動時從檔案系統載入到記憶體。具體可以使用 SpringBoot 提供的定時任務、ApplicationRunner 來實現。

整體架構

我們保持了推薦系統的現有邏輯,並在現有流程的末端,配置了 CacheModule 和 CacheService,負責所有和快取相關的邏輯。

其中,CacheService 是快取的具體實現,提供讀寫介面;CacheModule 對本次請求的資料進行處理,並決定是否需要呼叫 CacheService 對快取進行操作。

模組解讀

1. CacheModule

在完成推薦系統的原有流程處理之後,CacheModule 會對得到的響應報文進行判斷,比如是否丟擲了異常,響應是否為空等,然後決定是否讀取快取或者提交快取任務。

CacheModule 的工作流程如圖所示,其中橘黃色部分代表對 CacheService 的呼叫:

  • 提交快取任務。如果該次請求沒有丟擲異常,並且響應結果也不為空,則會提交一個快取任務到 CacheService。任務的 key 值為對應的業務場景,value 為本次響應計算得到的內容。提交的動作是非阻塞的,對介面的耗時影響很小。

  • 讀取快取資料。當應用本身或者依賴應用丟擲異常時,系統會根據業務場景的 key 值從 CacheService 中讀取快取並返回給呼叫方。當出現使用者本身已經刷完所有可用資料的情況時,就不需要讀取快取,而是將請求的資料及時反饋給使用者。

2. CacheService

在快取的具體實現上,CacheService 使用到了從 Apache Cassandra 專案中獨立出來的 OHC。另外因為我們整個應用是基於 SpringBoot 的,也用到了 SpringBoot 提供的各種功能。

上文說到對快取沒有強一致性的要求,所以我們採用的是本地快取而非分散式快取,並且抽象出一個 CacheService 類負責對本地快取進行維護。

(1) 資料格式

推薦系統返回資料時,根據業務場景和使用者特徵設定以「屏」為單位返回資料,每屏可以包含多個內容項,所以採取 key-set 的資料格式:key 值為業務場景,比如首頁的「視訊」頻道;快取內容則為「屏」的集合。

(2) 儲存位置

對於 Java 應用,快取可以存放在記憶體中或者硬碟檔案中。而記憶體空間又分為 heap(堆記憶體)和 off-heap(堆外記憶體)。我們對這幾種方式進行了對比:

為了保證較快的讀寫速度,避免快取 GC 影響線上服務,所以選擇 off-heap 作為快取空間。OHC 最早包含在 Apache Cassandra 專案中,之後獨立出來,成為了基於 off-heap 的開源快取工具。它既可以維護大量的 off-heap 記憶體空間,同時也使用於低開銷的小型快取實體。所以我們使用 OHC 作為 off-heap 的快取實現。

(3) 檔案備份

在應用重啟時,off-heap 中的快取為空。為了儘快載入快取,我們使用 SpringBoot 的 Scheduling Tasks 功能,定期將快取從 off-heap 備份到檔案系統;通過繼承 SpringBoot 的 ApplicationRunner 監聽應用啟動的過程,啟動完成後將硬碟中的備份檔案載入到 off-heap,保證快取資料的可用性。

CacheService 維護一個任務佇列,佇列中儲存著 CacheModule 通過非阻塞的方式提交的快取任務,由 CacheService 決定是否要執行這些快取任務。

(4) 對 CacheModule 提供的 API

  • 讀取快取時,傳入 key 值,快取模組隨機從 set 中讀取資料返回。

  • 寫入快取時,將 key 和 value 封裝為一個任務,提交到任務佇列,由任務佇列負責非同步寫入快取。

(5) 任務佇列與非同步寫入

這裡我們使用了 JDK 中的執行緒池來實現。在構造執行緒池時,使用 LinkedBlockingQueue 作為任務佇列,可以實現快速增刪元素;因為應用的 QPS 在 100 以內,所以工作執行緒數目固定為 1;佇列寫滿之後,則執行 DiscardPolicy,放棄插入佇列。

(6) 快取數量控制

如果快取佔用記憶體空間過大,會影響線上應用,我們可以採用為不同的業務場景配置最大快取數量來控制快取數量。沒有達到配置值時,將成功處理過的資料寫入快取;達到配置值時可以隨機抽樣覆蓋原有快取項,來保證快取的實時性。

綜合考慮以上各個方面,CacheService 的設計如下:

線上表現

為了驗證容災快取的效果,我們在命中快取時進行了埋點,並通過 Kibana 檢視每小時快取的命中數量。如圖所示,在 18:00 到 19:00 系統存在一定的超時,而這段時間由於快取服務發揮了作用,使系統的可用性得到提升。

我們還對 OHC 的讀取和寫入速度進行了監控。寫入快取的時延在毫秒級別,並且是非同步寫入;讀取快取的時延在微秒級別。基本沒有給系統增加額外的時間消耗。

踩過的坑

在將快取寫入 OHC 之前,需要進行序列化,我們使用了開源的 kryo 作為序列化工具。之前在使用 kyro 時,發現對於沒有實現 Serializable 的類,反序列化時可能失敗,比如使用 List#subList 方法返回的內部類 java.util.ArrayList$SubList。這裡可以手動註冊 Serializer 來解決這個問題,在 Github 上開源的 kryo-serializers 倉庫提供了各種型別的 serializers。

另外一點,需要注意根據具體使用場景,來配置 OHC 中的 capacity 和 maxEntrySize。如果配置的值太小的話,會導致寫入快取失敗。可以在上線之前測算快取的空間佔用,合理設定整個快取空間的大小和每個快取 entry 的大小。

 

三、優化方向

基於 SpringBoot 和 OHC,我們在現有的推薦系統中增加了一個本地容災快取系統,當依賴服務或者應用本身突發異常時可以返回快取的資料。

該快取系統還存在一些不足,我們近期會針對以下幾點進行重點優化:

  • 快取數目寫滿之後,目前應用會隨機覆寫已經存在的快取。未來可以進行優化,將最老的快取項替換。

  • 在某些場景下快取的粒度不夠精細,比如目的地頁推薦共用一個快取的 key 值。未來可以根據目的地的 ID,為每個目的地配置一份快取。

  • 現在推薦系統還有部分配置依賴於 MySQL,未來會考慮將在本地進行檔案快取。

 

[參考資料]

1. Java Caching Benchmarks 2016 - Part 1

2. On Heap vs Off Heap Memory Usage

3. OHC - An off-heap-cache

4. kryo-serializers

5. scheduling-tasks

 

本文作者:孫興斌,馬蜂窩推薦和搜尋後端研發工程師。

(馬蜂窩技術原創內容,轉載務必註明出處儲存文末二維碼圖片,謝謝配合。)

關注馬蜂窩技術公眾號,找到更