全民 K 歌推薦後臺架構

分享嘉賓:davidwwang 騰訊音樂 | 基礎開發組副組長

編輯整理:梁爾舒

出品平臺:DataFunTalk

導讀:首先介紹一下我們業務背景,騰訊音樂集團,於 2018 年是從騰訊拆分獨立上市,目前涵蓋了四大移動音樂產品,像包括 QQ 音樂,酷狗,酷我音樂以及全民 K 歌四大產品,現在總的月活用戶量已經超過了八億,其中全民 K 歌和其他三款 APP 有顯著的不同,我們是以唱爲核心,在唱歌的功能上不斷衍生出了一些音樂娛樂的功能以及玩法,當前的月活規模也在 1.5 個億以上,我們團隊在全民 K 歌 APP 中負責各個場景的推薦。

本篇文章將會給大家介紹一下全民 K 歌推薦系統在工程上的一些實踐,整篇分享分爲 3 個部分: 首先介紹下 K 歌推薦後臺架構;其次根據推薦的整體流程,從召回、排序、推薦去重三個方面分別介紹下 K 歌的相關實踐; 最後介紹下推薦相關的 Debug 系統。

01

K 歌推薦後臺架構

K 歌推薦的後臺架構整體可以分爲在線和離線兩個部分,離線的部分主要依賴兩個平臺:數據處理平臺和 VENUS 算法平臺。

位於我們數據存儲層之上的是在線的部分,包括召回層、排序層和重排層,排序層根據業務複雜度的不同,又可以進一步分爲精排層和粗排層,在這三層之上是我們的中臺層,它包括了我們整個內部的 abtest 平臺,內容分發平臺等相應的一系列的平臺組件。除此之外還有我們的服務質量監控,以及我們的推薦質量監控,推薦 Debug 的一些輔助工具等一些支持系統。

接下來主要會對整個推薦的在線服務相關的模塊做一些介紹。

02

召回

首先,要介紹的是我們的召回部分,在整個 K 歌的實踐中,其實大概經歷了三個階段的演進,在業務的初期流量比較小的情況下,爲了支持業務的快速上線,我們主要採用的是一個基於 Redis 的 KV 倒排索引的方案,後期隨着整個業務的逐漸放量,還有我們整個的召回的路徑也越來越多,所以 V1 版的方案,它暴露出來了兩個問題,首先第一個就是說召回越來越多,對於頻繁修改,導致整個開發效率是比較低的,對於人力的成本投入也是比較高。第二個就是線上採用了對遠程分佈式 Redis 直接拉取的方案,那麼對於 Redis 存儲壓力是比較大的。爲了解決上面兩個問題,我們後來演化到了 V2 版本的方案,是一個基於雙 mongo 和本地 KV 緩存的方案,通過本地索引解決了性能問題,但它還會存在另外一個問題,因爲我們採用的是一個懶加載的本地 Cache 方案,所以說它會存在首次不命中的一個問題。爲了解決這個問題,我們在 V2 版的方案上要進一步迭代到 V3 版的方案。V3 版方案是基於雙 mongo 和本地雙 buffer 全緩存的一個方案。下面來對這三個方案來做一一的介紹。

1. 召回 V1: 基於 Redis 的 KV 倒排索引

首先是基於 Redis 的 KV 倒排索引的方案。這個方案的實現是比較簡單的,簡單來說就是算法的同學會對每一個召回做一個預先的分類,對於每一個召回維度構建相應的倒排索引,將離線的數據寫入到線上,線上服務採用的是一個批量併發拉取遠程 Redis KV 的方式做召回。

這個方案它的問題是什麼?首先隨着業務複雜度的增加,整個召回的維度它是非常多樣的,而且我們不斷地做 abtest 的實驗,它整個召回的變化也是比較頻繁的,這樣人工構建的一個方案對於整個的開發工作量是非常大的,而且後續非常難以維護。然後第二個問題是在線是採用了直接拉取,並沒有增加一些緩存的一些邏輯在裏面,在召回這個階段,用戶的請求大概可以達到 1 比 100 以上的一個請求放大,對於整個 Redis 後端的存儲壓力以及存儲成本是相對比較高的。另外在線上的存儲採用了一個序列化的存儲協議,對於序列化的數據,我們在拉取回來之後,必然要涉及到一個反序列化,那麼頻繁的反序列化也會導致我們整個 CPU 的負載會偏高。那麼針對這三個問題,進行了第二版的方案優化。

2. 召回 V2: 雙 mongo + 本地 KV 索引

第二版的方案利用 cmongo 的多索引特性和本地索引 Cache 的自動化構建,提供了自動化的索引構建和自動化的拉取的能力,解決了前面所說的整個索引構建開發複雜度比較高的問題。另外對於雙 mongo,實現了自動熱切換的功能,這兩個 mongo 之間如果最新的數據出現失敗的情況下,它會自動去切換到一個隔日的備份,本質上是一個降級的邏輯。另外採用雙 buffer,也是對於當日和隔天數據的一個解耦。第三個問題就是前面說的 CPU 的問題,我們解決的一個方案是在 Cache 組件的選取裏面,選取了一個本地的免序列化的一個 Cache 組件,來降低我們的 CPU 的一個負擔。那麼整個 V2 版的方案在上線之後,我們在 CPU 的性能上提高了一倍,在 Cache 命中率上達到 80%。當然這個地方還有一個可以改進的點,我們在個別場景有使用,就是說如果能搭配到前端的一個一致性 Hash 請求的話,那麼整個 Cache 的命中率可以達到 90% 以上。

V2 版的方案中,在本地 Cache 裏其實採用了一個懶加載的方案,就會導致首次不命中的問題,不過在一般的情況下不會有太大的問題,但是在業務的高峯期它就可能有問題,在業務的高峯期可能會有零星的線上的報警,還可能會出現一些請求的毛刺。

3. 召回 V3: 雙 mongo+ 本地雙 buff 全緩存

爲了解決這樣的一個問題,我們 V2 版方案的基礎上,我們又構建了 V3 版的方案——雙 buffer + 本地全緩存的一個方案,和 V2 版相比只有在線模塊有一些變動,在線模塊的變動的主要的變動,就是說將 mongo 存儲的拉取採用了一個定時器來自動化定時更新的一個方案,更新的週期是大概是分鐘級。OK。另外的話就是在本地全緩存的 Cache,也是採用了一個 buffer 和自動熱切換的一個方法,這樣做核心要解決的一個問題,首先是能夠讀寫分離,因爲整個 Cache 組件它本身是有鎖的,讀寫分離的話就可以對線上請求來說只有讀而沒有加鎖的問題,可以達到更高的一個性能。另外的話就是整個數據存儲之間的一個解耦。可以在整個圖的左邊看到,除了定時器的更新,其實還保留了一個灰度數據源的一個通路,這個通路會直接使線上請求從遠程 mongo 數據源召回,因爲對於我們做搜索推薦的來說,肯定都會有這樣的一個場景,就是說我們做了一堆的 ABtest 的實驗,其實我們真正能夠推到線上的可能也就不到 50%,如果我要對每一個相應的一個實驗都要做一個全緩存的定時器的,從開發工作量上來說會變得很大。而且像 ABtest 這種一般都是基於小流量的實驗,只有在最終驗證成功之後,纔會去進一步的放量這種小流量的實驗。對於小流量的實驗直接請求 mongo 源,其實性能上是完全可以支持的,因爲如果只是讀的話,經過測試,其實是可以支持到至少十幾萬的 TPS 的一個訪問量,所以說沒有任何問題。整個的方案在上線之後,我們進行了一個壓測,對於一個八核 32G 的機器,我們大概亞特的一個新的數據是這樣的,就是說如果我們以 10 臺一組去請求後端的話,我們大概的 QPS 在單臺機器上可能達到一個 1.6 萬,而整個的平均時延從 V2 版方案的十幾毫秒,大概能降低到四毫秒左右,整個成功率達到 4 個 9 以上了,完全滿足我們整個服務的性能要求。

03

排序

排序部分主要介紹三點:特徵平臺、特徵格式的選擇、特徵聚合與模型預測框架。

1. 特徵平臺

特徵平臺這邊主要解決的是特徵管理的問題,在最初的時候大家都是自己去構造特徵然後去上線,那會導致特徵散落在各個地方,不利於統一維護,特徵複用依賴於大家口口相傳,管理和維護成本是非常之高的。整個特徵平臺主要包含三個大的模塊:特徵註冊、特徵寫入、特徵拉取。在特徵註冊部分主要是提供了一個一站式的特徵管理界面,這樣就減少了前面所說的口口相傳的一個問題,另外就是將離線的數據和在線的存儲做了一個打通,減少了相應的特徵註冊成本,提高註冊的效率。在特徵寫入這個階段,我們是採用了一個組件化的開發方式,提供一個專用的免代碼開發的寫入組件,只需要相關的同學完成配置之後,就可以將相應的數據自動導入到線上的存儲裏,同時導入時支持了相當於限流的一個工具,可以支持的流量的按需控制。同時還開發了一些配套的通用驗證工具和一些成功率相關的監控。最後對於在線服務的特徵拉取這一塊,首先當然是要存儲結耦,於是提供了一個通用化的存儲協議來進行存儲。另外在特徵聚合框架內提供一個可配置的緩存支持,可以按需來進行選擇。另外就是特徵格式協議選擇的優化,進一步提升了我們整個特徵平臺線上特徵拉取過程的性能。

2. 特徵格式的選擇

提到了特徵協議,就來到了我們的第二部分,特徵格式的選擇。爲什麼單要拎出一頁 PPT 來單獨介紹特徵格式?原因就是說如果特徵格式的選擇不合適的話,它對整個線上的性能影響是非常之大的。其實最早我們選取的特徵格式,其實是谷歌的 Tfrecord 的格式,也就是 Tensorflow 支持的格式,這個格式簡單來說,就是通過一個 Map 和多層的 Vector 嵌套來實現了一個通用的幀格式。但是我們在線上在實際使用的過程中,發現線上非常消耗 CPU,假設我們 CPU 高負載到 90% 了,90% 裏面的 80%,它其實都是消耗在 Tfrecord 的這種格式的打解包以及打解包過程中相應的內存分配上,OK,大家現在可以知道,基本上這個點就是我們的業務一個瓶頸了。

通過我們對於業界的一些相關的平臺的調研和我們內部的壓測之後,我們選擇了右邊的特徵格式,它的主要改進點有兩個,第一個就是取消了 Map,因爲在我們的壓測中會發現它整個 Map 的打解包和序列化的性能都是非常之差。另外一個就是說我們去掉了 string,首先去掉 string 的原因,是因爲我們看到模型訓練的特徵是可以沒有 string 的。第二個原因其實是還可以減少網絡流量的浪費,對於同樣一個特徵,切換掉 string 後,新的特徵格式與老的特徵格式進行對比的話,在線上的存儲佔用基本上可以減少 1/2 左右,上面表格中新的特徵格式與老的特徵對比可以發現,我們在打解包的性能上,我們基本上可以拿到一個十倍的一個收益,在內存的分配大小上,我們基本上可以拿到五倍左右的一個收益,當然了,我們上線後的 QPS 表現上也基本上能拿到五倍左右的一個最終收益。

3. 特徵聚合與預測框架

介紹完了特徵平臺、特徵格式的選擇接下來就是特徵拉取(聚合)和預測框架了,然後這一塊主要介紹幾個我們的優化點,首先是在特徵聚合階段我們採用了一個週期緩存的方式,因爲整個特徵拉取的階段,也是一個擴散量非常大的一個場景,如果直接是拉取存儲,會跟召回側一樣,對於整個後端的存儲壓力和整個的存儲成本的要求會非常的高。所以說我們採取了一個多級緩存的方案,簡單來說首先就是利用了前面介紹的一個特徵平臺,它提供了一個支持特徵緩存的一個設置,另外在整個特徵拉取的框架中,對於特徵聚合的過程中也可以提供一個聚合後的特徵緩存的配置。另外的話我們在特徵聚合框架中採取了插件式的特徵拉取開發方式,原因是說我們目前這邊後臺的開發人力相對來說是比較緊缺的,現在的話,基本上算法同學也會做一些相應的一些開發工作。爲了將線上穩定性和性能調優這部分工作與算法同學的模型相關工作解耦出來,我們將這一層(穩定性和性能)在框架裏就做了一個透明化的處理,然後相關的同學只需要去實現其中的一兩個接口就可以完成整個的功能,進而提高上線效率,規避上線風險。第三個要介紹的是在用戶特徵這一塊,其實跟 item 的特徵做了一個分離處理,用戶特徵其實是通過上游來透傳的,爲什麼要這樣做?主要是爲了在 rank 特徵加載階段減少一個拉取和擴散的損耗,因爲在線上的請求過程中,假設 500 個要去做預測打分,是不可能給 500 個單個單個的通過模型預測服務去打分的。OK,如果 500 個,30 個爲一批次的話,大概就十幾個批次,如果每批次都要在整個特徵聚合的框架做拉取的話,大概也要拉取十幾次,整個的擴散量是 1:10 幾,對於整個用戶特徵的擴散量還是相對來說會比較大的。如果通過上游透傳過來的話,那就是 1:1 的一個擴散量,其實相對來說就非常小了,對成本的節省是非常明顯的。OK,這是特徵聚合的幾個大的優化點。

關於模型預測這一塊兒,主要介紹兩個地方,首先就是說在真正我們喂到模型去進行打分預測的時候,我們用戶的特徵跟物料的特徵其實是做了一個分開去投傳的,爲什麼要分開?之前我們也是說把物料特徵和用戶才能全部拼在一起,拼成物料的特徵。其實分開打分主要是要解決線上的特徵拉取流量過大的問題,因爲在整個推薦這個場景,打分的時候,我們的特徵是非常多的,對於一個用戶的打分,基本上你拉取的整個特徵量可能是按 M 來計的,這對於整個線上的網絡 IO 的流量會比較大,我們在之前就達到了整個線上網絡 IO 流量的瓶頸,可能是單機都能達到 1G 以上,或者是接近對於實際的網卡,我們可能達到 8G 左右,相當大了。通過這樣的一個改進,就可以至少降低 1/3 的網絡 IO 的流量,對整個網絡帶寬的成本消耗是非常之明顯的。

第二個要介紹的是特徵一致性的保障。其實分爲兩個方面,一個是特徵值的一致性,另外一個是特徵處理的一致性。特徵值的一致性通過將在線的特徵做一個離線的上報,這樣的話在線和離線所用的特徵都是同一份數據源,那就消除了特徵穿越導致的不一致。關於特徵處理的一致性的解決方案,是說在前面介紹的 VENUS 的一個數據處理平臺上,提供了一個統一的特徵處理的插件,離線和在線都是通過同一個插件來做特徵的處理,這樣的話就可以規避線上線下特徵處理不一致的問題。通過以上兩個方案,我們完成了整個的特徵一致性的一個保障。

04

推薦去重

1. 方案選型

第三部分介紹的是在萬級別去重過濾的場景的實踐。去重過濾在業界有兩種主要方案,一個是明文列表的方案,一個是基於布隆過濾器的方案。那麼像 K 歌之前在千級別以下的去重過濾,採用的是明文列表方案,因爲它比較簡單,一般來說都是我們第一個想到的一個方案,

明文列表優勢:

劣勢:

布隆過濾器它相對的明文列表的方案來說優勢:

劣勢:

通過前面介紹的方案的對比,我們可以知道萬級別這種場景下,因爲 key 值太大了,所以使用明文列表的方案不太合適。布隆過濾這種方法也有它的問題,我們通過對原生的布隆過濾器做一些基本的改造,來完成我們線上的業務的一個訴求。

2. K 歌實現方案

這個是我們改造的方案。簡單來說,我們實現了一個基於多分片的和自動淘汰的布隆過濾器的設計。在這個設計中我們介紹三點,第一點就是我們支持了多個存儲組件,主要就是支持了 Cmongo、CKV + 兩種,Cmongo 就我們騰訊內部的一個 mongoDB,它的底層存儲是 SSD,成本上在我們內部大概估的話應該就是每 G 每月幾塊錢的租賃成本。像 CKV + 這一塊,它是一個純內存的,它的每月每 G 的租賃成本,由於機房不同,大概是二十或幾十不等。爲了滿足不同場景的成本考量,我們提供了多種存儲的支持。另外第二個就是我們線上實施的過程是將整個布隆過濾器數據拉到本地來進行判斷,主要的原因是想解決一個網絡 IO 的問題,它不像 Redis 默認支持布隆過濾器,需要你通過網絡 IO 把你的相應的信息傳到遠端的 Redis 裏面,通過 Redis 的 API 來判斷。我們知道整個網絡 IO 的延時跟本地判斷延時相比基本上前者是毫秒級別的,後者是納秒級的,兩邊差了一到兩個數量級。所以說在本地判斷的話,對於大批量判斷的效率,它會非常之高效。第三個就是多分片的設計了,通過多分片的最大分片數的限制,我們可以自動淘汰舊分片,同時減少存儲的浪費。通過存儲組件提供的特性,我們還支持過期這樣的一個功能,也就是解決了前面我們所說的原生布隆過濾器存在的一些問題。最後我們還提供了一些通用的代理服務和多元的 SDK 來使業務完成快速接入。

這裏介紹一個應用場景,是我們在內部的一個推薦 feed 的業務實踐。在這個業務實踐裏,我們是採用了五分片,然後每個分片大概支持 1000 個 ID,千分之一誤判率的一個配置。實踐中有兩點還是要介紹一下的,第一個就是說客戶端流水,因爲是併發上報的,它會存在一個讀寫衝突的問題,如何解決流水處理作業衝突,我們採用的是一致性 hash 的負載均衡的路由算法,另外再配合我們服務內部的一個 hash 隊列,將整個數據的並行寫入,變成了一個串行寫入,來解決這樣的一個併發衝突問題。

另外就是說像這種消息隊列的上報,其實它是有一定的延遲的,雖然也很快,一般認爲大約是兩毫秒左右的一個延遲。如果延遲不管的話,其實對於用戶頻繁下拉的時候,它就可能會出現一個重複的問題。我如何解決?這一個重複的解決方案是這樣的,我們通過配合實時查詢後臺記錄的下發歷史的短列表來保證用戶的體驗上不刷到重複的。

整個線上業務實踐的數據結果,大概就像圖片裏表格內容所示,相比於明文列表的方案,採用布隆過濾器,我們在整個存儲的佔用上,拿到了五倍以上的收益,那麼在整個單 ID 的判斷效率上,我們也可以看到,基本上可以拿到十倍左右的一個收益。我們在拉取時延上,因爲整個它的存儲 key 的 value 變小了,所以說也拿到了接近 7 倍的一個收益。這個是我們整個在推薦系統部分的業務實踐。

05

Debug

最後一部分我們要介紹的是推薦的一些周邊系統。整個推薦的開發流程,可以分爲四個部分,先是代碼開發,開發完會進行自測和調試,感覺滿足需求後上線,上線之後監控用戶的反饋。

1. Debug:調試

在調試階段,開發了畫像平臺 & 特徵查詢的平臺,方便數據驗證。在內部 Debug 版本的 APP 中,內嵌了模塊化的調試工具,可以實時查看物料對應的推薦相關信息。

2. Debug:監控

在調試後的上線階段,我們構建了豐富的監控體系。包括了核心指標的實時監控, 整體效果的統計監控,基於 abtest 平臺的顯著性驗證和下鑽分析等。

3. Debug:日誌追溯

上線之後,不可避免的就是我們會收到各種各樣的反饋。對於產品和運營同學來說,整個推薦它其實一個偏黑盒兒的一個東西,它其實並不知道你爲什麼推給他。爲了解決黑盒,我們開發了一個基於日誌回溯的 Debug 系統,通過這個系統,我們可以將整個推薦的路徑做一個可視化的展示,然後通過這種可視化的展示,我們可以逐級定位推薦的數據從哪裏來,到哪裏去,進而可以幫助我們快速定位整個的線上問題。

接下來介紹這個平臺實現的架構方案,在整個的存儲端採用的是將 ES 作爲存儲組件,支持服務端採用本地日誌寫入,或者直接通過 ES 的 API 寫入,這個就看業務各自的一個選擇情況了。在前端的一個展示界面上,我們是採用 Django 開發框架來搭建,然後這個平臺可以將整個推薦路徑作逐級的展開。在基於日誌回溯的 Debug 系統裏,它其實有一個難點:我們其實知道整個推薦在召回階段是萬級別的,那麼等到我們排序打分的時候,粗排後可能是千級別或百級別,最終精排後,吐出的可能是個位數和幾十級別大小的數據,每一個請求便會產生很大的數據量,如果採用這樣一套方案的話,它對於整個網絡流量的衝擊也是很大。

這樣的問題我們有幾個解決方案,當然並不是能徹底解決它,更多隻是逐漸優化它。對於大部分的關聯信息,我們是採取了一個單獨存儲的方式,通過在查詢的時候做關聯,來避免將大流量的數據是直接寫入到 ES 裏面。另外的話就是對於流量很大的個別場景,支持採樣,當然採樣有可能會帶來一個問題是它可能沒有辦法復原現場用戶反饋了,你也不知道它現場是什麼。我們有一個白名單平臺,它產品或運營,或者說我們自己的同學可以直接就在配置平臺配置上白名單,然後這樣的話它就可以把用戶的整個路徑上需要展示的數據給存到存儲裏面,然後便可以在前端能看到一個大概秒級延遲的展示,另外目前我們是配置的 ES 的最短更新週期大概是 30 秒。

最後這個地方就是我們整個在線上的基於日誌回溯的 Debug 平臺的一個樣式,它支持用戶維度、item 維度兩個維度的追蹤方式,從這個圖我們其實可以看得出來,就是說它通過不斷的遞歸和逐級的展開,可以將整個推薦的路徑像召回、排序等可以逐級展開,同時可以跟進到每個 item 的召回源,以及這個 item 的一些詳情是什麼。另外日誌回溯平臺還跟特徵平臺、畫像平臺做了打通,方便問題定位。

今天的分享就到這裏,謝謝大家。

嘉賓介紹:

davidwwang

騰訊音樂 | 基礎開發組副組長

負責全民 K 歌基礎平臺和工具的研發, 和 K 歌國際版相關的推薦工作。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Xe4ZyUqLY3WJU0na4On6Hw