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

来源:https://www.cnblogs.com/mfwtech/archive/2019/05/17/10880218.html
-Advertisement-
Play Games

資料庫突然斷開連接、第三方介面遲遲不返回結果、高峰期網路發生抖動...... 當程式突發異常時,我們的應用可以告訴調用方或者用戶「對不起,伺服器出了點問題」;或者找到更好的方式,達到提升用戶體驗的目的。 一、背景 用戶在馬蜂窩 App 上「刷刷刷」時,推薦系統需要持續給用戶推薦可能感興趣的內容,主要 ...


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

 

一、背景

用戶在馬蜂窩 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

 

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

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

關註馬蜂窩技術公眾號,找到更多你需要的內容


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在使用Ajax跨域請求時,如果設置Header的ContentType為application/json,會分兩次發送請求。第 一次先發送Method為OPTIONS的請求到伺服器,這個請求會詢問伺服器支持哪些請求方法(GET,POST等), 支持哪些請求頭等等伺服器的支持情況。等到這個請求返回後, ...
  • 使用vue-video-player在移動端微信內置瀏覽器打開,點擊視頻自動全屏問題。 參考官方 API 是 H5 同層瀏覽器的原因,可通過設置video屬性來處理。 ...
  • Uploading Resources to the Web Resource Repository Prerequisites You have been assigned the Content Administrator role. Context You can upload Web res ...
  • 優點:利於搜索引擎,解決白屏問題,因為正常情況下在index.html文件中只有一個簡單的標簽,沒有內容,不利於爬蟲搜索 場景:交互少,數據多,例如新聞,博客,論壇類等 原理:相當於服務端前面加了一層url分配,可以假想為服務端的中間層, 當地址欄url改變或者直接刷新,其實直接從伺服器返回內容,是 ...
  • 作者: 阮一峰 日期: 2015年7月10日 網頁佈局(layout)是 CSS 的一個重點應用。 佈局的傳統解決方案,基於盒狀模型,依賴 display 屬性 + position屬性 + float屬性。它對於那些特殊佈局非常不方便,比如,垂直居中就不容易實現。 2009年,W3C 提出了一種新 ...
  • saltstack數據系統 數據系統Grains 1、Grains是SaltStack收集的有關底層管理系統的靜態信息。包括操作系統版本、功能變數名稱、IP地址、記憶體、內核、CPU、操作系統類型以及許多其他系統屬性。Minion 收集的信息,可以作為Master端匹配目標。2、如果需要自定義grains,需 ...
  • 模板方法模式就是在父(基)類定義模板(流程),而具體的處理環節交給子類來實現 附錄 github.com/maikec/patt… 個人GitHub設計模式案例 聲明 引用該文檔請註明出處 ...
  • 最近參加了一次AWS 架構師的面試,吐槽一下整個面試時間相當的長,幾乎經歷了半年左右,但是我也是抱著學習偉大的AWS雲產品的態度所以在整個過程中學到不少的雲產品的功能、設計等知識,所以說還是相當有益處的。前面的幾關解答客戶需求筆試還是相當順利,雖然最後在視頻面試會議中對可用區的概念上是被認為是不瞭解 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...