摘要:馬蜂窩推薦系統(tǒng)對于請求的平均處理時(shí)延要求在級別,時(shí)延的線保持在以內(nèi)。任務(wù)隊(duì)列與異步寫入這里我們使用了中的線程池來實(shí)現(xiàn)。三優(yōu)化方向基于和,我們在現(xiàn)有的推薦系統(tǒng)中增加了一個(gè)本地容災(zāi)緩存系統(tǒng),當(dāng)依賴服務(wù)或者應(yīng)用本身突發(fā)異常時(shí)可以返回緩存的數(shù)據(jù)。
數(shù)據(jù)庫突然斷開連接、第三方接口遲遲不返回結(jié)果、高峰期網(wǎng)絡(luò)發(fā)生抖動...... 當(dāng)程序突發(fā)異常時(shí),我們的應(yīng)用可以告訴調(diào)用方或者用戶「對不起,服務(wù)器出了點(diǎn)問題」;或者找到更好的方式,達(dá)到提升用戶體驗(yàn)的目的。
一、背景
用戶在馬蜂窩 App 上「刷刷刷」時(shí),推薦系統(tǒng)需要持續(xù)給用戶推薦可能感興趣的內(nèi)容,主要分為根據(jù)用戶特性和業(yè)務(wù)場景,召回根據(jù)各種機(jī)器學(xué)習(xí)算法計(jì)算過的內(nèi)容,然后對這些內(nèi)容進(jìn)行排序后返回給前端這幾個(gè)步驟。
推薦的過程涉及到 MySQL 和 Redis 查詢、REST 服務(wù)調(diào)用、數(shù)據(jù)處理等一系列操作。對于推薦系統(tǒng)來說,對時(shí)延的要求比較高。馬蜂窩推薦系統(tǒng)對于請求的平均處理時(shí)延要求在 10ms 級別,時(shí)延的 99 線保持在 1s 以內(nèi)。
當(dāng)外部或者內(nèi)部系統(tǒng)出現(xiàn)異常時(shí),推薦系統(tǒng)就無法在限定時(shí)間內(nèi)返回?cái)?shù)據(jù)給到前端,導(dǎo)致用戶刷不出來新內(nèi)容,影響用戶體驗(yàn)。
所以我們希望通過設(shè)計(jì)一套容災(zāi)緩存服務(wù),實(shí)現(xiàn)在應(yīng)用本身或者依賴的服務(wù)發(fā)生超時(shí)等異常情況時(shí),可以返回緩存數(shù)據(jù)給到前端和用戶,來減少空結(jié)果數(shù)量,并且保證這些數(shù)據(jù)盡可能是用戶感興趣的。
二、設(shè)計(jì)與實(shí)現(xiàn)
設(shè)計(jì)思路和技術(shù)選型
不僅僅是推薦系統(tǒng),緩存技術(shù)在很多系統(tǒng)中已經(jīng)被廣泛應(yīng)用,小到 JVM 中的常用整型數(shù),大到網(wǎng)站用戶的 session 狀態(tài)。緩存的目的不盡相同,有些是為了提高效率,有些是為了備份;緩存的要求也高低不一,有些要求一致性,有些則沒有要求。我們需要根據(jù)業(yè)務(wù)場景選擇合適的緩存方案。
結(jié)合到我們上面提到的業(yè)務(wù)場景和需求,我們采用了基于 OHC 堆外緩存和 SpringBoot 的方案,實(shí)現(xiàn)在現(xiàn)有推薦系統(tǒng)中增加本地容災(zāi)緩存系統(tǒng)。主要是考慮到以下幾點(diǎn)因素:
1. 避免影響線上服務(wù),將業(yè)務(wù)邏輯和緩存邏輯隔離
為了不影響線上服務(wù),我們將緩存系統(tǒng)封裝為一個(gè) CacheService,配置在現(xiàn)有流程的末端,并提供讀、寫的 API 給外部調(diào)用,將業(yè)務(wù)邏輯和緩存邏輯隔離。
2. 異步寫入緩存,提高性能
讀、寫緩存都會帶來時(shí)間消耗,特別是寫入緩存。為了提高性能,我們考慮將寫入緩存做成異步的方式。這部分使用的是 JDK 提供的線程池 ThreadPoolExecutor 來實(shí)現(xiàn),主線程只需要提交任務(wù)到線程池,由線程池里的 Worker 線程實(shí)現(xiàn)寫入緩存。
3. 本地緩存,提高訪問速度
在推薦系統(tǒng)中,給用戶推薦的內(nèi)容應(yīng)該是千人千面的,甚至同一位用戶每次刷新看到的內(nèi)容都可能不同,這就不要求緩存具有強(qiáng)一致性。因此,我們只需要進(jìn)行本地緩存,而不需要采用分布式的方式。這里使用到的是開源緩存工具 OHC,緩存的數(shù)據(jù)來源于成功處理過的請求。
4. 備份緩存實(shí)例,保證可用性
為了保證緩存的可用性,我們不僅在內(nèi)存中進(jìn)行緩存,還定時(shí)備份到文件系統(tǒng)中,從而保證在可以應(yīng)用啟動時(shí)從文件系統(tǒng)加載到內(nèi)存。具體可以使用 SpringBoot 提供的定時(shí)任務(wù)、ApplicationRunner 來實(shí)現(xiàn)。
整體架構(gòu)
我們保持了推薦系統(tǒng)的現(xiàn)有邏輯,并在現(xiàn)有流程的末端,配置了 CacheModule 和 CacheService,負(fù)責(zé)所有和緩存相關(guān)的邏輯。
其中,CacheService 是緩存的具體實(shí)現(xiàn),提供讀寫接口;CacheModule 對本次請求的數(shù)據(jù)進(jìn)行處理,并決定是否需要調(diào)用 CacheService 對緩存進(jìn)行操作。
模塊解讀
1. CacheModule
在完成推薦系統(tǒng)的原有流程處理之后,CacheModule 會對得到的響應(yīng)報(bào)文進(jìn)行判斷,比如是否拋出了異常,響應(yīng)是否為空等,然后決定是否讀取緩存或者提交緩存任務(wù)。
CacheModule 的工作流程如圖所示,其中橘黃色部分代表對 CacheService 的調(diào)用:
提交緩存任務(wù)。如果該次請求沒有拋出異常,并且響應(yīng)結(jié)果也不為空,則會提交一個(gè)緩存任務(wù)到 CacheService。任務(wù)的 key 值為對應(yīng)的業(yè)務(wù)場景,value 為本次響應(yīng)計(jì)算得到的內(nèi)容。提交的動作是非阻塞的,對接口的耗時(shí)影響很小。
讀取緩存數(shù)據(jù)。當(dāng)應(yīng)用本身或者依賴應(yīng)用拋出異常時(shí),系統(tǒng)會根據(jù)業(yè)務(wù)場景的 key 值從 CacheService 中讀取緩存并返回給調(diào)用方。當(dāng)出現(xiàn)用戶本身已經(jīng)刷完所有可用數(shù)據(jù)的情況時(shí),就不需要讀取緩存,而是將請求的數(shù)據(jù)及時(shí)反饋給用戶。
2. CacheService
在緩存的具體實(shí)現(xiàn)上,CacheService 使用到了從 Apache Cassandra 項(xiàng)目中獨(dú)立出來的 OHC。另外因?yàn)槲覀冋麄€(gè)應(yīng)用是基于 SpringBoot 的,也用到了 SpringBoot 提供的各種功能。
上文說到對緩存沒有強(qiáng)一致性的要求,所以我們采用的是本地緩存而非分布式緩存,并且抽象出一個(gè) CacheService 類負(fù)責(zé)對本地緩存進(jìn)行維護(hù)。
(1) 數(shù)據(jù)格式
推薦系統(tǒng)返回?cái)?shù)據(jù)時(shí),根據(jù)業(yè)務(wù)場景和用戶特征設(shè)定以「屏」為單位返回?cái)?shù)據(jù),每屏可以包含多個(gè)內(nèi)容項(xiàng),所以采取 key-set 的數(shù)據(jù)格式:key 值為業(yè)務(wù)場景,比如首頁的「視頻」頻道;緩存內(nèi)容則為「屏」的集合。
(2) 存儲位置
對于 Java 應(yīng)用,緩存可以存放在內(nèi)存中或者硬盤文件中。而內(nèi)存空間又分為 heap(堆內(nèi)存)和 off-heap(堆外內(nèi)存)。我們對這幾種方式進(jìn)行了對比:
為了保證較快的讀寫速度,避免緩存 GC 影響線上服務(wù),所以選擇 off-heap 作為緩存空間。OHC 最早包含在 Apache Cassandra 項(xiàng)目中,之后獨(dú)立出來,成為了基于 off-heap 的開源緩存工具。它既可以維護(hù)大量的 off-heap 內(nèi)存空間,同時(shí)也使用于低開銷的小型緩存實(shí)體。所以我們使用 OHC 作為 off-heap 的緩存實(shí)現(xiàn)。
(3) 文件備份
在應(yīng)用重啟時(shí),off-heap 中的緩存為空。為了盡快載入緩存,我們使用 SpringBoot 的 Scheduling Tasks 功能,定期將緩存從 off-heap 備份到文件系統(tǒng);通過繼承 SpringBoot 的 ApplicationRunner 監(jiān)聽?wèi)?yīng)用啟動的過程,啟動完成后將硬盤中的備份文件加載到 off-heap,保證緩存數(shù)據(jù)的可用性。
CacheService 維護(hù)一個(gè)任務(wù)隊(duì)列,隊(duì)列中保存著 CacheModule 通過非阻塞的方式提交的緩存任務(wù),由 CacheService 決定是否要執(zhí)行這些緩存任務(wù)。
(4) 對 CacheModule 提供的 API
讀取緩存時(shí),傳入 key 值,緩存模塊隨機(jī)從 set 中讀取數(shù)據(jù)返回。
寫入緩存時(shí),將 key 和 value 封裝為一個(gè)任務(wù),提交到任務(wù)隊(duì)列,由任務(wù)隊(duì)列負(fù)責(zé)異步寫入緩存。
(5) 任務(wù)隊(duì)列與異步寫入
這里我們使用了 JDK 中的線程池來實(shí)現(xiàn)。在構(gòu)造線程池時(shí),使用 LinkedBlockingQueue 作為任務(wù)隊(duì)列,可以實(shí)現(xiàn)快速增刪元素;因?yàn)閼?yīng)用的 QPS 在 100 以內(nèi),所以工作線程數(shù)目固定為 1;隊(duì)列寫滿之后,則執(zhí)行 DiscardPolicy,放棄插入隊(duì)列。
(6) 緩存數(shù)量控制
如果緩存占用內(nèi)存空間過大,會影響線上應(yīng)用,我們可以采用為不同的業(yè)務(wù)場景配置最大緩存數(shù)量來控制緩存數(shù)量。沒有達(dá)到配置值時(shí),將成功處理過的數(shù)據(jù)寫入緩存;達(dá)到配置值時(shí)可以隨機(jī)抽樣覆蓋原有緩存項(xiàng),來保證緩存的實(shí)時(shí)性。
綜合考慮以上各個(gè)方面,CacheService 的設(shè)計(jì)如下:
線上表現(xiàn)
為了驗(yàn)證容災(zāi)緩存的效果,我們在命中緩存時(shí)進(jìn)行了埋點(diǎn),并通過 Kibana 查看每小時(shí)緩存的命中數(shù)量。如圖所示,在 18:00 到 19:00 系統(tǒng)存在一定的超時(shí),而這段時(shí)間由于緩存服務(wù)發(fā)揮了作用,使系統(tǒng)的可用性得到提升。
我們還對 OHC 的讀取和寫入速度進(jìn)行了監(jiān)控。寫入緩存的時(shí)延在毫秒級別,并且是異步寫入;讀取緩存的時(shí)延在微秒級別。基本沒有給系統(tǒng)增加額外的時(shí)間消耗。
踩過的坑
在將緩存寫入 OHC 之前,需要進(jìn)行序列化,我們使用了開源的 kryo 作為序列化工具。之前在使用 kyro 時(shí),發(fā)現(xiàn)對于沒有實(shí)現(xiàn) Serializable 的類,反序列化時(shí)可能失敗,比如使用 List#subList 方法返回的內(nèi)部類 java.util.ArrayList$SubList。這里可以手動注冊 Serializer 來解決這個(gè)問題,在 Github 上開源的 kryo-serializers 倉庫提供了各種類型的 serializers。
另外一點(diǎn),需要注意根據(jù)具體使用場景,來配置 OHC 中的 capacity 和 maxEntrySize。如果配置的值太小的話,會導(dǎo)致寫入緩存失敗。可以在上線之前測算緩存的空間占用,合理設(shè)置整個(gè)緩存空間的大小和每個(gè)緩存 entry 的大小。
三、優(yōu)化方向
基于 SpringBoot 和 OHC,我們在現(xiàn)有的推薦系統(tǒng)中增加了一個(gè)本地容災(zāi)緩存系統(tǒng),當(dāng)依賴服務(wù)或者應(yīng)用本身突發(fā)異常時(shí)可以返回緩存的數(shù)據(jù)。
該緩存系統(tǒng)還存在一些不足,我們近期會針對以下幾點(diǎn)進(jìn)行重點(diǎn)優(yōu)化:
緩存數(shù)目寫滿之后,目前應(yīng)用會隨機(jī)覆寫已經(jīng)存在的緩存。未來可以進(jìn)行優(yōu)化,將最老的緩存項(xiàng)替換。
在某些場景下緩存的粒度不夠精細(xì),比如目的地頁推薦共用一個(gè)緩存的 key 值。未來可以根據(jù)目的地的 ID,為每個(gè)目的地配置一份緩存。
現(xiàn)在推薦系統(tǒng)還有部分配置依賴于 MySQL,未來會考慮將在本地進(jìn)行文件緩存。
[參考資料]
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
本文作者:孫興斌,馬蜂窩推薦和搜索后端研發(fā)工程師。
(馬蜂窩技術(shù)原創(chuàng)內(nèi)容,轉(zhuǎn)載務(wù)必注明出處保存文末二維碼圖片,謝謝配合。)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/6693.html
摘要:馬蜂窩推薦系統(tǒng)對于請求的平均處理時(shí)延要求在級別,時(shí)延的線保持在以內(nèi)。任務(wù)隊(duì)列與異步寫入這里我們使用了中的線程池來實(shí)現(xiàn)。三優(yōu)化方向基于和,我們在現(xiàn)有的推薦系統(tǒng)中增加了一個(gè)本地容災(zāi)緩存系統(tǒng),當(dāng)依賴服務(wù)或者應(yīng)用本身突發(fā)異常時(shí)可以返回緩存的數(shù)據(jù)。 數(shù)據(jù)庫突然斷開連接、第三方接口遲遲不返回結(jié)果、高峰期網(wǎng)絡(luò)發(fā)生抖動...... 當(dāng)程序突發(fā)異常時(shí),我們的應(yīng)用可以告訴調(diào)用方或者用戶「對不起,服務(wù)器出了...
摘要:為了解決以上問題,我們的分流系統(tǒng)選擇基于實(shí)現(xiàn),通過或者協(xié)議來傳遞分流信息。正交是指用戶進(jìn)入所有的實(shí)驗(yàn)之間沒有必然關(guān)系。流量層內(nèi)實(shí)驗(yàn)分流流量層內(nèi)實(shí)驗(yàn)的因子有設(shè)備流量層。統(tǒng)計(jì)功效對于置信區(qū)間特征值等產(chǎn)品化功能支持。 什么是 ABTest 產(chǎn)品的改變不是由我們隨便「拍腦袋」得出,而是需要由實(shí)際的數(shù)據(jù)驅(qū)動,讓用戶的反饋來指導(dǎo)我們?nèi)绾胃玫馗纳品?wù)。正如馬蜂窩 CEO 陳罡在接受專訪時(shí)所說:「有...
摘要:為了解決以上問題,我們的分流系統(tǒng)選擇基于實(shí)現(xiàn),通過或者協(xié)議來傳遞分流信息。正交是指用戶進(jìn)入所有的實(shí)驗(yàn)之間沒有必然關(guān)系。流量層內(nèi)實(shí)驗(yàn)分流流量層內(nèi)實(shí)驗(yàn)的因子有設(shè)備流量層。統(tǒng)計(jì)功效對于置信區(qū)間特征值等產(chǎn)品化功能支持。 什么是 ABTest 產(chǎn)品的改變不是由我們隨便「拍腦袋」得出,而是需要由實(shí)際的數(shù)據(jù)驅(qū)動,讓用戶的反饋來指導(dǎo)我們?nèi)绾胃玫馗纳品?wù)。正如馬蜂窩 CEO 陳罡在接受專訪時(shí)所說:「有...
閱讀 1408·2023-04-26 01:58
閱讀 2282·2021-11-04 16:04
閱讀 1753·2021-08-31 09:42
閱讀 1765·2021-07-25 21:37
閱讀 1066·2019-08-30 15:54
閱讀 2074·2019-08-30 15:53
閱讀 3047·2019-08-29 13:28
閱讀 2687·2019-08-29 10:56