天天看點

B站動态outbox本地緩存優化

作者:閃念基因

問題的發現

動态綜合頁比較容易因為高熱事件,引起大量使用者持續消費feed流,導緻線上拉取動态時間線feed流接口快速飙升至平時峰值2~3倍以上而大量逾時,較多使用者無法正常消費其feed流。從監控上發現outbox(使用者發件箱)服務依賴的redis叢集大量執行個體CPU使用率皆超過了95%甚至達到100%(如圖1)。是以,瓶頸在于outbox redis叢集壓力太大,無法扛住過大的高熱流量。而痛點在于redis叢集無法高效快速擴容,是以,我們遇到此類情況通常隻能被迫降級限流,以防情況進一步惡化。

B站動态outbox本地緩存優化

圖1 outbox-redis叢集執行個體cpu使用率過載

瓶頸根因的分析

在分析outbox redis叢集壓力過大的原因之前,先簡單介紹一下拉取動态時間線feed流的實作方案——”推拉結合“(如圖2)。在動态系統中,為每個使用者都分别維護了一個收件箱”inbox“與一個發件箱”outbox“。inbox存儲使用者關注的UP釋出的所有動态,outbox存儲了使用者本人釋出的所有動态。

”推“是指一個UP釋出的動态會推送至其所有粉絲的inbox中。而”拉“則是指從使用者關注的UP的outbox中分别拉取一頁feed流。我們一般對于粉絲數量大的UP主(簡稱大粉UP)采用拉的模式,其他UP主采用推的模式,因為”推“大粉UP寫擴散比較嚴重,會影響寫入性能并大幅提升存儲空間。所謂的“推拉結合”,就是從使用者的inbox拉取一頁feed流以及從使用者關注的所有未做推處理的大粉UP的outbox分别拉取一頁feed流,合并并按釋出時間降序排序取TopN最終形成使用者的一頁feed流。

不難發現,對于”拉“outbox而言存在着使用者關系鍊的讀擴散,是以,對outbox redis叢集讀放大較嚴重(幾百甚至上千倍)。引入”推“inbox的本質其實是緩解”拉“outbox讀擴散的壓力,因為使用者關注的且已經做推處理的UP釋出的動态可以直接從使用者的inbox中得到,無需去拉他們的outbox。然而,高熱事件引發的大量使用者同時通路帶來的瞬間高并發再疊加上outbox的讀放大效應,依然足以将outbox redis打過載。

B站動态outbox本地緩存優化

圖2 動态時間線feed流推拉結合方案

解決方案的PK

那麼, 如何進一步降低outbox redis壓力,解決我們目前outbox遇到的可擴充性瓶頸呢?首先想到的方案是進一步提高inbox的使用率。可以提高目前既定的符合“推”inbox條件的粉絲數門檻值,對更多的UP做“推”處理,進一步減輕outbox的讀擴散壓力。但是該方案會引起inbox寫擴散壓力與存儲空間成本成倍增加,最終獲得的收益效果可能也比較有限。是以,我們又把焦點轉移到了outbox本身。我們假設關系鍊中被”拉“outbox的UP存在熱點,如果我們緩存這些熱UP的最新一部分動态清單于本地緩存中,同樣可以幫助redis抵擋相當一部分壓力。對比兩個方案發現,outbox本地緩存方案不僅不會額外增加硬體成本,而且實作簡單,可以快速上線驗證效果,收益可能比優化”推“方案要高。是以,最終它成為了PK的勝出方。

方案的設計與上線後的效果

緩存哪些UP

既然決定采用本地緩存優化的方案,那麼我們首先需要知道哪些UP是熱的呢?從關系鍊的特點,我們推斷大粉UP被通路的機率應該更高。我們通過統計曆史動态時間線的UP流量分布也論證了我們的猜測。是以,我們定義了一個粉絲數門檻值,将粉絲數達到該門檻值及以上的UP作為熱key,緩存他們一部分最新動态清單于本地(門檻值的設定基于記憶體可以承受的空間),理論上可以獲得較高的緩存命中率,并有效緩解“拉”outbox對redis叢集的壓力。

如何建構本地緩存

因為設定的粉絲數門檻值比較高,是以熱UP的數量變更不會特别頻繁。基于此特點,我們給出的本地緩存整體方案是(如圖3):從資料平台每天離線T+1地統計出所有粉絲數達标以及因掉粉粉絲數從達标變為不達标的UP名單,并通過kafka推送寫入redis。當outbox服務執行個體啟動時,會從redis拉取到全量名單,并從outbox redis分别拉取這些被緩存UP的最新動态清單,建構于本地緩存中。而啟動後的outbox執行個體每當感覺到來自資料平台的被緩存UP(包括需要删除的)名單推新時,也會拉取推新後的名單,但隻建構目前未被緩存的新UP,并删除粉絲數低于門檻值的UP緩存。當使用者擷取feed流“拉”其關注的UP的outbox時,優先從本地緩存擷取UP的最新動态清單,未命中的UP才回源拉,以此緩解outbox redis的讀擴散壓力。

B站動态outbox本地緩存優化

圖3 outbox本地緩存整體方案

如何防止回源雪崩

被緩存UP的最新動态清單不是一直不變的,當UP釋出或者删除動态後,需要及時回源outbox redis擷取該up變更後的最新動态清單并重構其本地緩存。是以,該方案還需要考慮回源對outbox redis的壓力問題。在萬級别的熱key個數加上百級别的執行個體規模場景下,如果我們采用簡單、正常的對每個被緩存UP設定一個較短過期時間,過期後回源重構則容易造成大量key同時過期回源導緻outbox redis叢集瞬時壓力過大,産生雪崩現象。

是以,我們給出的回源重構方案是”變更廣播+異步重構“(如圖4)。在這個方案中,outbox執行個體本地緩存的UP最新動态清單是常駐不過期的。當某個被緩存的UP釋出或删除動态時,會廣播該UP的“動态清單變更”事件給所有outbox執行個體,outbox執行個體接收通知後異步回源并重構該UP的本地緩存。因為被緩存UP變更頻次極少,所有這種回源重構方式對outbox redis的壓力也很小。

B站動态outbox本地緩存優化

圖4 變更通知+異步回源重構

如何保證本地緩存與redis的一緻性

采用無過期常駐緩存的方案也讓我們擔心本地緩存中的值與outbox redis中的值會長期不一緻。為了確定一緻性,我們在方案中加入了一緻性檢測的功能。我們提供了”UP變更時檢測“與”定期巡檢“兩種模式(如圖5)。每個outbox執行個體每次回源重構某個UP的本地緩存後,會計算出該UP最新動态清單的checksum并存入redis中。UP變更時檢測則會在某個被緩存UP釋出/删除動态觸發所有outbox執行個體重構本地緩存後,對比該UP在所有執行個體本地緩存中最新動态清單的checksum與在outbox redis中最新動态清單的checksum是否一緻。

而定期巡檢則會在每天固定的時間,對比全量被緩存UP在所有執行個體本地緩存中的最新動态清單與在outbox redis中的最新動态清單是否一緻。無論在”UP變更時檢測“還是”定期巡檢“的過程中,一旦檢測到被緩存UP在本地緩存中的值與outbox redis中的值不一緻時,皆會将該UP的”動态清單變更“事件再次廣播到所有outbox執行個體觸發對該UP緩存的重構,以實作自動修複不一緻的功能。

B站動态outbox本地緩存優化

圖5 一緻性檢測與自動修複

上線後的效果

outbox本地緩存優化上線後,命中率達到了55%以上(如圖6)。環比優化上線前後的周末高峰outbox redis壓力情況:outbox redis的壓力峰值降低了上線前的近44%(如圖7~8),而outbox redis叢集單執行個體CPU使用率峰值也降低了上線前的37.2%(如圖9~10)。

B站動态outbox本地緩存優化

圖6 outbox本地緩存命中率

B站動态outbox本地緩存優化

圖7 本地緩存優化上線前周末outbox redis的壓力峰值

B站動态outbox本地緩存優化

圖8 本地緩存優化上線後周末outbox redis的壓力峰值

B站動态outbox本地緩存優化

圖9 本地緩存優化上線前redis的高峰期間cpu使用率

B站動态outbox本地緩存優化

圖10 本地緩存優化上線後redis的高峰期間cpu使用率

後續規劃

目前我們隻緩存了普通類型的熱UP,然而從曆史UP流量分析發現,番劇與付費視訊類型的UP同樣存在較多數量的熱UP,如果我們後續将他們也加入本地緩存中,可以進一步提高緩存命中率。另一方面,對于按照上述政策未被識别為熱點的UP,我們會繼續采用實時熱key發現政策做本地緩存,以防止他們中少數UP在空間頁等場景産生突發的高熱流量,導緻redis叢集中部分執行個體CPU使用率過載的現象。

本期作者

窦晨超 哔哩哔哩資深開發工程師

來源:微信公衆号:哔哩哔哩技術

出處:https://mp.weixin.qq.com/s/ov1UPkhjIti0QuHdxm2t9Q

繼續閱讀