深入淺出快手圖資料庫:看架構如何讓推薦召回更高效

qing_yun發表於2024-01-26

導讀 本文將從工程的角度,分享快手圖資料庫存算分離架構及其在實時推薦召回的應用。

主要內容包括以下四大部分:

1. 應用場景

2. 核心訴求

3. 存算分離架構

4. 效能要點

分享嘉賓|李本利 快手 圖資料庫工程師

01、應⽤場景

1. 三角問題-擴散

首先來看一個圖推薦中經常會遇到的場景,圖擴散。

如上圖所示,已知一個點,由此點出發,找自己的行為關係,到達一箇中間結點,再到另外一個結點,這樣就是兩跳。兩跳之後獲取全部資料,然後進行內容的推薦計算,比如我關注的人還關注誰、我關注的大 V 的朋友圈有哪些大 V、我點讚的影片相似度高的影片有哪些。其中朋友圈有很多定義方式,比如他們的互關、互動程度即親密分數比較高、互動比較頻繁,或者其它一些定義。

這種場景的特點是所見即所得。對簡單規則推薦的業務場景來說,用語法上線速度是非常快的。

對於其他擴充場景,關係可以是多樣化的,比如關注、互關、點贊、評論、分享,還有一些其它的關係比如 Facebook 好友等等。另一方面,節點也可以是多樣的,包括使用者、影片以及認證賬號。

這種推薦方式非常常見,可以取得很好的定向推薦效果。

這種推薦方式可以用 Cypher 語法來描述,如上圖所示,從一個點出發,經過一個邊,再經過一個邊,最終拿到一個點,對這個點來計數。當然這個計數只是一種方式,還有其它一些方法比如 sum(sim_score),計算出最終的 score 值,根據 score 值排序、截斷,然後推薦。正常情況下,推薦的結果一般不會超過一千,但是中間訪問到d的資料量是很大的,突破幾十萬都是很正常的。快手這邊 Follow 上線目前是五千,其它邊可以輕鬆達到幾十萬、上百萬的資料量。

2. 三角問題-共同

再來看第二個場景,共同關注。

已知兩個點,來確定他們之間是不是存在一些關係,比如共同關注的關係、有共同的好友或是有共同的圈子。主要應用場景是,點開一個陌生的頭像或是陌生的主頁,為了誘使使用者與其發生更進一步的關係,就會告訴他我們有共同的興趣愛好,這屬於點對點的推薦方式。

為了解決這樣的問題,採用共同類的方式,具體方法是分別從這兩個點出發,最終拿到一個目標,共同的一個點,進行聚合 UNION ALL,得到一個聚合後的 COUNT 值。

為什麼要用聚合的方式而不是用其他方式?我們在訪問過程中,會有很多的篩選條件,比如要求過濾掉大 V 帳號、過濾掉已經封禁的帳號等等,篩選條件會比較豐富,可能還要根據最終節點拿到他的屬性進行篩選,這樣每兩個跳都可以單獨篩選,最終拿到這樣的結果。

3. 存在問題

存在問題,指的是我和群體有什麼關係。比如博主發了一個影片,評論列表中有幾十萬,不可能每個評論都回復,這時就需要推薦出一些有價值的評論,進行回覆,可能需要判斷他們是不是朋友、是不是親密度比較高,這樣可以打上一些標籤,博主就更可能會和其進行一些互動。

對於這類需求,我們採用了內建的圖演算法,看是否存在一個這樣的邊,如果存在一個這樣的邊,就返回一個 ‘Follow’、‘FollowBy’、‘Friend’、‘Like’ 等關係,這些關係是根據使用者的使用場景具體定義的,最終對邊進行標籤化的展示。

02、核心訴求

前文中講到了三種場景,更多的情況是這三種場景的糅合,會比較複雜。使用者的核心訴求為以下三點:

  • 成本:資料量超過千萬級別,多種不同的邊,總資料量可達到萬億級別,幾百臺機器滿足簡單的需求是不可接受的,因此成本是備受關注的一個點。

  • 效能

  • 易用

以上三點缺一不可。

03、存算分離架構

1. 整體架構

存算分離架構是近年來資料庫領域非常火熱的一個架構,其主要特點就是按需部署。CPU、Memory 和 Disk 是相互隔離開的,每一部分都可以單獨擴容。這樣就可以很方便地找到瓶頸所在,並單獨對其進行擴容,從而降低成本。

快手當前的架構主要是分為 Graph Service 層、Tree Service 層和 Storage 層。

Graph Service 層就是語法的執行層。

Tree Service 層是圖的模型層,中間會有 Cache,主要提供 Memory 選項。

Storage 層是由 SSD 磁碟儲存和 S3 冷儲存多樣儲存組成,這一層進行了冷熱分離。

正常的情況下,我們的成本主要集中在記憶體層,因為我們的圖對記憶體的需要比較高。

2. BWTree Service

BWTree Service 是記憶體層,是圖描述的一層,對其最主要的訴求就是強一致。因為圖的模型有一些其他衍生出來的資料結構,比如唯一性索引,物化出來的 num neighbors、雙關等,如果不能做到強一致,整個模型會存在一些問題。它主要是利用 Mem 和 SSD 作為 Cache。

先來看一下使用者請求的處理過程,如上圖中右側所示。使用者發起一個請求,加入到請求佇列,使用者所有請求都在一個佇列中。Tree-writer 執行緒工作中,會把這些請求打包成一個 Log,commit 到 WAL-service,這是強一致性儲存的一個外部儲存平臺。commit 成功就可以應用 Wal-diff 到記憶體中;如果 commit 衝突,即 WAL-service 已經有了這個 Log,就要把最新的 Log 拉回來應用到記憶體,再重新執行上述過程。這樣就可以保證強一致。最後就是 Log 應用到記憶體中,並返回成功。這裡的 flush 後臺執行緒,執行的頻率比較低,大概是幾十分鐘至一個小時才會把所有的資料 flush 一遍,速率取決於當前的髒頁率,儘量降低對持久化儲存的影響。Page 持久化儲存會分為兩層,一層是 SSD Cache,還有一層是 S3 儲存,可以進一步降低成本。我們的 Wal-service 除了剛才提到的日誌,還兼具選主的功能,即多副本的情況下進行選主。

3. 緊湊記憶體模型

前文中提到,記憶體是成本的主要來源。我們的記憶體模型和作業系統是比較相似的。用 mmap 申請很大的三塊記憶體,分別作為一/二/三級頁表,和作業系統的頁表是同一個概念,在緊湊的記憶體空間是沒有任何浪費的。第三級頁表,指向真實的資料。資料是用 malloc,因為我們希望頁本身是可變大小的。例項記憶體比較滿,就進行淘汰,從 page records 左側開始遍歷,遇到 access_num 為 0 的頁就可以淘汰,這個過程也和作業系統比較像,因此我們也是用的 CLOCK 淘汰演算法。

4. 邊模型

前面講解了樹模型,但是樹模型不能完整地描述邊,邊有四種樹結構,分別是 Record Tree、Unique Index Tree、Num Neighbors Materialize Tree 和 Bidirectional Materialize Tree。Record Tree 是記錄樹,Unique Index Tree 是索引樹,關係鏈樹是需要維護索引的。Num Neighbors Materialize Tree 是鄰居的物化檢視,主要記錄在一點有多少個鄰居,有多少個評論樹,多少個好友樹。物化檢視的更新主要是依據普通的樹更新,根據主樹來更新。Bidirectional Materialize Tree 是雙關物化檢視,根據出邊和入邊物化出一個雙關列表。以上四種樹可以根據使用者配置來進行生成或不生成。

5. Snapshot 隔離性

在實時讀寫情況下。需要做到讀檢視 snapshot 一致性,不能出現幻讀和未提交讀。每個頁有多個版本,修改頁則複製一份頁資料,併產生新的版本號,多個版本的頁都記錄在 page record 中。訪問請求攜帶一個版本號 n+1,就可以區分並訪問期望的頁,從而實現了隔離。

04、效能要點

1. Share Nothing

比如一個兩跳查詢,一跳是 500,最終需要拉取 25 萬資料。假如每跳是 5000,那麼最終訪問的資料就會有 2500 萬。當然,2500 萬資料的訪問量級在實際使用中是不會出現的,我們都會進行限制,但即使限制到幾十萬的資料,對於經典的資料庫也很難做到百毫秒以內返回,我們現在可以做到十萬資料的查詢計算 10 毫秒量級返回。效能是比較好的。那麼我們是如何做到這樣的效能的呢?

比較重要的做 Share Nothing,這是資料庫中一個比較經典的概念。使用者的一個請求到了一個執行緒,這個執行緒有個協程,協程在發起請求的情況下使用執行緒繫結的連線池發起,不用再跨執行緒。因為跨執行緒是很耗時的,即使什麼也不做也要時延大概 0.08 毫秒,如果多次跨執行緒總時延會達到 0.4 毫秒,對於線上的一些核心產品應用來說,和 Redis 對比,0.4 毫秒已經是比較高的延時了。Share Nothing 要求連線和本執行緒進行繫結,和 Tree Service 特定 worker 執行緒繫結,同樣,在收到請求的情況下,也是在本執行緒執行,主要是讀的過程,如果要發起到 S3 冷儲存查詢請求,或 KV 磁碟儲存查詢請求也都是在本執行緒。這就是 Share Nothing 的概念,儘量不要跨執行緒,資料也是在本執行緒完成的。

2. 資料流

以上介紹了資料請求的過程,接下來看一下資料回傳的過程。資料最終的葉子節點,儲存格式是行存,但是在讀之後就成為了列存格式。因為行存在更新時效能會比較好,列存時實時更新效能極差。因此,我們做了取捨,儲存時用行存,讀出之後所有資料用列存。讀出之後的列存資料格式,在經過 RPC 時,壓縮效率和傳輸效率等都會比較高。最終到 Graph 層,資料經過一個個運算元表示式。列存資料作為運算元的輸入,可以做向量化的運算。最終拿到的輸出也是列式進行輸出,我們用的是 Apache-Arrow 資料儲存格式返回給使用者,也是列式儲存。因此,這樣的架構特別適合圖地查詢和計算。

以上就是本次分享的內容,謝謝大家。

來自 “ DataFunTalk ”, 原文作者:李本利;原文連結:https://mp.weixin.qq.com/s/SGcbd3PhuLT6bMMHgMXU_Q,如有侵權,請聯絡管理員刪除。

相關文章