大廠的視頻推薦索引構建解決方案

關注我,緊跟本系列專欄文章,咱們下篇再續!

作者簡介:魔都技術專家兼架構,多家大廠後端一線研發經驗,各大技術社區頭部專家博主。具有豐富的引領團隊經驗,深厚業務架構和解決方案的積累。

負責:

  • 中央/分銷預訂系統性能優化
  • 活動&優惠券等營銷中臺建設
  • 交易平臺及數據中臺等架構和開發設計

目前主攻降低軟件複雜性設計、構建高可用系統方向。

參考:

1 背景

在視頻推薦場景:

  • 讓新啓用的視頻儘可能快的觸達用戶,對新聞類內容尤爲關鍵
  • 快速識別新物品的好壞,通過分發的流量,以及對應的後驗數據,來判斷新物品是否值得繼續分發流量

這兩點對索引先驗數據和後驗數據的延遲都高要求。下文介紹視頻推薦的索引構建方案。

  • 先驗數據:視頻創建時就帶有的數據如tag,作者賬號id
  • 後驗數據:用戶行爲反饋的數據如曝光、點擊、播放

2 視頻推薦整體架構

數據鏈路角度,從下往上:

  • 視頻內容由內容中心通過MQ給到我們,經過一定的處理入庫、建索引、生成正排/倒排數據,這時候在存儲層可召回的內容約1千萬條
  • 經召回層,通過用戶畫像、點擊歷史等特徵召回出數千條視頻,給到粗排層
  • 粗排將這數千條視頻打分,取數百條給到精排層
  • 精排再一次打分,給到重排
  • 重排根據一定規則和策略進行打散和干預,最終取10+條給到用戶

視頻在用戶側曝光後,從上到下,是另一條數據鏈路:用戶對視頻的行爲,如曝光、點擊、播放、點贊、評論等經過上報至日誌服務,然後通過實時/離線處理產生特徵回到存儲層,由此形成循環。

基於此架構,需設計一套召回/倒排索引,以實時/近實時延遲來處理所有數據。

3 方案設計

舊方案的索引每半小時定時構建,無法滿足近實時要求。分析索引構建方案,發現挑戰:

  • 數據雖不要求強一致性,但需要保證最終一致性
  • 後驗數據寫入量極大,APP用戶行爲每日百億+
  • 召回系統要求高併發、低延遲、高可用

3.1 業界主流方案調研

Redis方案靈活性較差,直接使用較難,需較多定製化開發,先排除。

可選方案主要在自研或開源成熟方案。研究發現:

  • 自研索引開發成本較高
  • 簡單自研方案可能無法滿足業務需求,完善的自研索引方案所需開發成本較高,需多人團隊開發維護

最終選擇基於ES的索引服務。不選Solr,主要因爲ES有更成熟社區及雲廠商PaaS服務支持,使用更靈活方便。

3.2 數據鏈路圖

3.2.1 方案介紹

數據鏈路角度分兩塊:

  • 先驗數據鏈路,數據源主要來自內容中心,通過解析服務寫入到CDB中。其中這個鏈路又分爲全量鏈路和增量鏈路

    • 全量鏈路主要是在重建索引時才需要的,觸發次數少但也重要。它從DB這裏dump數據,寫入kafka,然後通過寫入服務寫入ES
    • 增量鏈路是確保其實時性的鏈路,通過監聽binlog,發送消息至kafka,寫入服務消費kafka然後寫入ES
  • 後驗數據鏈路。APP用戶行爲流水每天有上百億,這個量級直接打入ES絕對扛不住。需對此進行聚合計算

用Flink做了1分鐘滾動窗口的聚合,然後把結果輸出到寫模塊,得到1分鐘增量的後驗數據。在這裏,Redis存儲近7天的後驗數據,寫模塊消費到增量數據後,需要讀出當天的數據,並於增量數據累加後寫回Redis,併發送對應的rowkey和後驗數據消息給到Kafka,再經由ES寫入服務消費、寫入ES索引。

3.2.2 一致性問題分析

該數據鏈路存在的一致性問題:

① Redis寫模塊,需先讀數據,累加後再寫入

Redis寫模塊,需先讀數據,累加後再寫入。先讀後寫,需要保證原子性,而這裏可能存在同時有其他線程在同一時間寫入,造成數據不一致。

解決方案1是通過redis加鎖來完成;解決方案2如下圖所示,在kafka隊列中,使用rowkey作爲分區key,確保同一rowkey分配至同一分區,而同一只能由同一消費者消費,也就是同一rowkey由一個進程處理,再接着以rowkey作爲分線程key,使用hash算法分線程,這樣同一rowkey就在同一線程內處理,因此解決了此處的一致性問題。另外,通過這種方案,同一流內的一致性問題都可以解決。

② Redis寫模塊,Redis寫入需先消費kafka的消息

這就要求kafka消息commit和redis寫入需要在一個事務內完成,即需保證原子性。

如果這裏先commit再進行redis寫入,那麼如果系統在commit完且寫入redis前宕機了,那麼這條消息將丟失掉;如果先寫入,在commit,那麼這裏就可能會重複消費。

如何解決?先寫入redis,且寫入的信息裏帶上時間戳作版本號,再commit消息;寫入前會比較消息版本號和redis版本號,若小於,則直接丟棄。

③ 寫入ES有3個獨立進程

寫入ES有3個獨立進程寫入,不同流寫入同一索引也會引入一致性問題。這裏我們可以分析出,主要是先驗數據的寫入可能會存在一致性問題,因爲後驗數據寫入的是不同字段,而且只有update操作,不會刪除或者插入。

若上游的MySQL這裏刪除一條數據,全量鏈路和增量鏈路同時執行,而剛好全量Dump時剛好取到這條數據,隨後binlog寫入delete記錄,那麼ES寫入模塊分別會消費到插入和寫入兩條消息,而他自己無法區分先後順序,最終可能導致先刪除後插入,而DB裏這條消息是已刪除的,這就造成了不一致。

那麼這裏如何解決該問題呢?其實分析到問題之後就比較好辦,常用的辦法就是利用Kfaka的回溯能力:在Dump全量數據前記錄下當前時間戳t1,Dump完成之後,將增量鏈路回溯至t1即可。而這段可能不一致的時間窗口1min,業務完全可接受。

線上0停機高可用在線索引升級流程:

3.2.3 寫入平滑

由於Flink聚合後的數據有很大的毛刺,導入寫入ES時服務不穩定,cpu和rt都有較大毛刺,寫入情況如圖:

此處監控間隔是10秒,可以看到,由於聚合窗口是1min,每分鐘前10秒寫入達到峯值,後面逐漸減少,然後新的一分鐘開始時又週期性重複這種情況。

對此我們需要研究出合適的平滑寫入方案,這裏直接使用固定閾值來平滑寫入不合適,因爲業務不同時間寫入量不同,無法給出固定閾值。

最終我們使用以下方案來平滑寫入:

使用自適應限流器來平滑寫,通過統計前1min接收的消息總量,來計算當前每秒可發送的消息總量。具體實現如圖,將該模塊拆分爲讀線程和寫線程,讀線程統計接收消息數,並把消息存入隊列;令牌桶數據每秒更新;寫線程獲取令牌桶,獲取不到則等待,獲取到了就寫入。最終平滑寫入後效果:




不同時間段,均達到平滑效果。

4 召回性能調優

4.1 高併發場景優化

由於存在多路召回,所以召回系統有讀放大的問題,我們ES相關的召回,總qps是50W。這麼大的請求量如果直接打入ES,一定是扛不住的,那麼如何來進行優化呢?

由於大量請求的參數是相同的,並且存在大量的熱門key,因此我們引入了多級緩存來提高召回的吞吐量和延遲時間。

多級緩存方案:

方案架構清晰,簡單明瞭,整個鏈路:本地緩存(BigCache)<->分佈式緩存(Redis)<->ES。

經計算,整體緩存命中率爲95+%,其中本地緩存命中率75+%,分佈式緩存命中率20%,打入ES的請求量大約爲5%。這大大提高召回的吞吐量並降低RT。

該方案還考慮緩存穿透和雪崩問題,上線後不久就發生一次雪崩,ES全部請求失敗,且緩存全部未命中。起初還分析究竟緩存失效導致ES失敗orES失敗導致設置請求失效,實際就是經典緩存雪崩問題。

該方案解決了:

  • 本地緩存定時dump到磁盤中,服務重啓時將磁盤中的緩存文件加載至本地緩存。
  • 巧妙設計緩存Value,包含請求結果和過期時間,由業務自行判斷是否過期;當下遊請求失敗時,直接延長過期時間,並將老結果返回上游。
  • 熱點key失效後,請求下游資源前進行加鎖,限制單key併發請求量,保護下游不會被瞬間流量打崩。
  • 最後使用限流器兜底,如果系統整體超時或者失敗率增加,會觸發限流器限制總請求量。

4.2 ES性能調優

4.2.1 設置合理的primary_shards

primary_shards即主分片數,是ES索引拆分的分片數,對應底層Lucene的索引數。這個值越大,單請求的併發度就越高,但給到上層MergeResult的數量也會增加,因此這個數字不是越大越好。

根據我們的經驗結合官方建議,通常單個shard爲150G比較合理,由於整個索引大小10G,我們計算出合理取值範圍爲110個,接下里我們通過壓測來取最合適業務的值。壓測結果:


根據壓測數據,我們選擇6作爲主分片數,此時es的平均rt13ms,99分位的rt爲39ms。

4.2.2 請求結果過濾不需要的字段

ES返回結果都是json,而且默認會帶上source和_id,_version等字段,我們把不必要的正排字段過濾掉,再使用filter_path把其他不需要的字段過濾掉,這樣總共能減少80%的包大小,過濾結果:

包大小由26k減小到5k,帶來的收益是提升了30%的吞吐性能和降低3ms左右的rt。

4.2.3 設置合理routing字段

ES支持使用routing字段來對索引進行路由,即在建立索引時,可以將制定字段作爲路由依據,通過哈希算法直接算出其對應的分片位置。

這樣查詢時也可根據指定字段路由,到指定分片查詢,無需到所有分片查詢。根據業務特點,將作者賬號id puin 作爲路由字段,路由過程:

這樣對帶有作者賬號id的召回的查詢吞吐量可以提高6倍,整體來看,給ES帶來了30%的吞吐性能提升。

4 關閉不需要索引或排序的字段

通過索引模板,我們將可以將不需要索引的字段指定爲"index":false,將不需要排序的字段指定爲"doc_values":false。這裏經測試,給ES整體帶來了10%左右的吞吐性能提升。

本文由博客一文多發平臺 OpenWrite 發佈!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章