Netflix實戰指南:規模化時序資料儲存

AI前線發表於2019-02-25

本文由 「AI前線」原創,原文連結:Netflix實戰指南:規模化時序資料儲存
作者|Ketan Duvedi 等
譯者|蓋磊
編輯|Natalie

AI 前線導讀:”Netflix 使用會員的視訊觀看記錄實時準確地記錄使用者的觀看情況,併為會員提供個性化推薦。Netflix 的發展,對視訊觀看記錄時序資料儲存的規模化提出了挑戰,原有的單表儲存架構無法適應會員的大規模增長。本文介紹了 Netflix 團隊在規模化時序儲存中的做法,包括資料儲存方式的改進,以及在儲存架構中新增快取層。儲存架構在 Netflix 的實際應用驗證了該時序資料儲存的有效性。”

引言

因特網互聯裝置的發展,提供了大量易於訪問的時序資料。越來越多的公司有興趣去挖掘這類資料,意圖從中獲取一些有意義的洞悉,並據此做出決策。技術的最新進展提高了時序資料的收集、儲存和分析效率,激發了人們對如何處理此類資料的考量。然而,大多數現有時序資料體系結構的處理能力,可能無法跟上時序資料的爆發性增長。

作為一家根植於資料的公司,Netflix 已習慣於面對這樣的挑戰,多年來一直在推進應對此類增長的解決方案。該系列部落格文章分為兩部分發表,我們將分享 Netflix 在改進時序資料儲存架構上的做法,如何很好地應對資料規模的成倍增長。

時序資料:會員視訊觀看記錄

每天,Netflix 的全部會員會觀看合計超過 1.4 億小時的視訊內容。觀看一個視訊時,每位會員會生成多個資料點,儲存為視訊觀看記錄。Netflix 會分析這些視訊觀看資料,實時準確地記錄觀看書籤,併為會員提供個性化推薦。具體實現可參考如下帖子:

  • 我們是如何知道會員觀看視訊的具體位置的?
  • 如何幫助會員在 Netflix 上發現值得繼續觀看的視訊?

視訊觀看的歷史資料將會在以下三個維度上取得增長:

  1. 隨時間的推進,每位會員會生成更多需要儲存的視訊觀看資料。
  2. 隨會員數量的增長,需要儲存更多會員的視訊觀看資料。
  3. 隨會員每月觀看視訊時間的增加,需要為每位會員儲存更多的視訊觀看資料。

Netflix 經過近十年的發展,全球使用者數已經超過一億,視訊觀看歷史資料也在大規模增長。這篇部落格帖子將聚焦於其中的一個重大挑戰,就是我們的團隊是如何解決視訊觀看歷史資料的規模化儲存的。

基本架構的初始設計

最初,Netflix 的雲原生儲存架構使用了 Cassandra 儲存觀看歷史資料。團隊是出於如下方面的考慮:

  • Cassandra 對時序資料的建模提供了很好的支援,支援一行中的列數動態可變。
  • 在觀看歷史資料上,讀操作和寫操作的數量比大約為 1:9。因為 Cassandra 提供了非常高效的寫操作,特別適用於此類寫密集的工作負載。
  • 從 CAP 定理方面考慮,相對於可用性而言,團隊更側重於實現最終一致性。Cassandra 支援可調整的一致性,有助於實現 CAP 上的權衡。

在最初的架構中,使用 Cassandra 儲存所有會員的觀看歷史記錄。其中,每位會員的觀看記錄儲存為一行,使用 CustomerId 標識。這種水平分割槽設計支援資料儲存隨會員數量的增長而有效擴充套件,並支援簡單並高效地讀取會員的完整觀看歷史資料。這一讀取操作是歷史資料儲存上最頻繁發生的操作。然而,隨著會員數量的持續增長,尤其是每位會員觀看的視訊流越來越多,儲存的資料行數和整體資料量也日益膨脹。隨著時間的推移,這將導致儲存和操作的成本增大。而且對於觀看了大量視訊的會員而言,效能會嚴重降低。

下圖展示了最初使用的資料模型中的讀操作和寫操作流。

Netflix實戰指南:規模化時序資料儲存

圖 1:單表資料模型

寫操作流

當一位會員開始播放視訊時,一條觀看記錄會以一個新列的方式插入。當會員暫停或停止觀看視訊流時,觀看記錄會做更新。在 Cassandra 中,對單一列值的寫操作是快速和高效的。

讀操作流

為檢索一位會員的所有觀看記錄,需要讀取整行記錄。如果每位會員的觀看記錄數量不大,這時讀操作是高效的。如果一位會員觀看了大量的視訊,那麼他的觀看記錄數量將會增加,即記錄的列數增加。讀取一個具有大量列的資料行,會對 Cassandra 造成了額外壓力,進而對讀操作延遲產生負面影響。

要讀取一段時間內的會員資料,需要做一次時間範圍查詢。這同樣會導致上面介紹的效能不一致問題。因為查詢效能依賴於給定時間範圍內的觀看記錄數量。

如果要檢視的歷史資料規模很大,需要做分頁才能進行整行讀操作。分頁對 Cassandra 更好,因為查詢不需要等待所有資料都就緒,就能返回給使用者。分頁也避免了客戶超時問題。但是,隨著觀看記錄的增長,分頁增加了讀取整行的整體延遲。

延遲的原因

下面介紹一些 Cassandra 的內部機制,進而理解為什麼我們最初的簡單設計會產生效能下降。隨著資料的增長,SSTable 的數量也隨之增加。因為只有最近的資料是維護在記憶體中的,因此在很多情況下,檢索觀看歷史記錄時需要同時讀取記憶體表和 SSTable。這對於讀取延遲具有負面影響。同樣,隨著資料的增長,合併(Compaction)操作將佔用更多的 IO 和時間。此外,隨著一行記錄越來越寬,讀修復(Read repair)和全列修復(Full column repair)也會變慢。

快取層

Cassandra 可以很好地對觀看資料執行寫操作,但是需要改進讀操作上的延遲。為優化讀操作延遲,我們考慮以增加寫路徑上的工作為代價,在 Cassandra 儲存前增加了一個記憶體中的分片快取層(即 EVCache)。快取實現為一種基本的鍵 – 值儲存,鍵是 CustomerId,值是觀看歷史資料的二進位制壓縮表示。每次 Cassandra 的寫操作,將額外生成一次快取查詢操作。一旦快取命中,直接給出快取中的已有值。對於觀看歷史記錄的讀操作,首先使用快取提供的服務。一旦快取沒有命中,再從 Cassandra 讀取條目,壓縮後插入到快取中。

在新增了快取層後,多年來 Cassandra 單表儲存方法一直工作很好。在 Cassandra 叢集上, 基於 CustomerId 的分割槽提供了很好的擴充套件。到 2012 年,檢視歷史記錄的 Cassandra 叢集成為了 Netflix 的最大專用 Cassandra 叢集之一。為進一步實現儲存的規模化,團隊需要實現叢集的規模翻番。這意味著,團隊需要冒險進入 Netflix 在使用 Cassandra 上尚未涉足的領域。同時,Netflix 業務也在持續快速增長,其中包括國際會員的增長,以及企業即將推出的自制節目業務。

重新設計:實時儲存和壓縮儲存

很明顯,為適應未來五年中企業的發展,團隊需要嘗試多種不同的方法去實現儲存的規模化。團隊分析了資料的特徵和使用模式,重新定義了觀看歷史儲存。團隊給出了兩個主要目標:

  • 更小的儲存空間;
  • 考慮每位會員觀看視訊的增長情況,提供一致的讀寫效能。

團隊將每位會員的觀看歷史資料劃分為兩個資料集:

  • 實時 / 近期觀看歷史記錄(LiveVH):一小部分頻繁更新的近期觀看記錄。LiveVH 資料以非壓縮形式儲存,詳細設計隨後介紹。
  • 壓縮 / 歸檔觀看歷史記錄(CompressedVH):大部分很少更新的歷史觀看記錄。該部分資料將做壓縮,以降低儲存空間。壓縮觀看歷史作為一列,按鍵值儲存在一行中。

為提供更好的效能,LiveVH 和 CompressedVH 儲存在不同的資料庫表中,並做了不同的優化。考慮到 LiveVH 更新頻繁,並且涉及的觀看記錄數量不大,因此可對 LiveVH 做頻繁的 Compaction 操作。並且為了降低 SSTable 數量和資料規模,可以設定很小的 gc_grace_seconds。為改進資料的一致性,也可以頻繁執行讀修復和全列族修復(full column family repair)。而對於 CompressedVH,由於該部分資料很少做更新操作,因此為了降低 SSTable 的數量,偶爾手工做完全 Compaction 即可。在偶爾執行的更新操作中,會檢查資料一致性,因此也不必再做讀修復以及全列族修復。

寫操作流

對於新的觀看記錄,使用同上的方法寫入到 LiveVH。

讀操作流

為有效地利用新設計的優點,團隊更新了觀看歷史 API,提供了讀取近期資料和讀取全部資料的選項。

  • 讀取近期觀看歷史:在大多數情況下,近期觀看歷史僅需從 LiveVH 讀取。這限制了資料的規模,進而給出了更低的延遲。
  • 讀取完整觀看歷史:實現為對 LiveVH 和 CompressVH 的並行讀操作。

考慮到資料是壓縮的,並且 CompressedVH 具有更少的列,因此讀取操作涉及更少的資料,這顯著地加速了讀操作。

CompressedVH 更新流

在從 LiveVH 讀取觀看歷史記錄時,如果記錄數量超過了一個預設的閾值,那麼最近觀看記錄將由後臺任務打包(roll up)、壓縮並儲存在 CompressedVH 中。打包資料儲存在一個行標識為 CustomerId 的新行中。新打包的資料在寫入後會給出一個版本,用於讀操作檢查資料的一致性。只有驗證了新版本的一致性後,才會刪除舊版本的打包資料。出於簡化的考慮,在打包中沒有考慮加鎖,由 Cassandra 負責處理非常罕見的重複寫問題(即以最後寫入的資料為準)。

Netflix實戰指南:規模化時序資料儲存

圖 2:實時資料和壓縮資料的操作模型

如圖 2 所示,CompressedVH 的打包行中還儲存了後設資料資訊,其中包括最新版本資訊、物件規模和分塊資訊,細節稍後介紹。記錄中具有一個版本列,指向最新版本的打包資料。這樣,讀取 CustomerId 總是會返回最新打包的資料。為降低儲存的壓力,我們使用一個列儲存打包資料。為最小化具有頻繁觀看模式的會員的打包頻率,LiveVH 中僅儲存最近幾天的觀看歷史記錄。打包後,其餘的記錄在打包期間會與 CompressedVH 中的記錄歸併。

通過分塊實現自動擴充套件

通常情況是,對於大部分的會員而言,全部的觀看歷史記錄可儲存在一行壓縮資料中,這時讀操作流會給出相當不錯的效能。罕見情況是,對於一小部分具有大量觀看歷史的會員,由於最初架構中的同一問題,從一行中讀取 CompressedVH 的效能會逐漸降低。對於這類罕見情況,我們需要對讀寫延遲設定一個上限,以避免對通常情況下的讀寫延遲產生負面影響。

為解決這個問題,如果資料規模大於一個預先設定的閾值,我們會將打包的壓縮資料切分為多個分塊,並儲存在不同的 Cassandra 節點中。即使某一會員的觀看記錄非常大,對分塊做並行讀寫也會將讀寫延遲控制在設定的上限內。

Netflix實戰指南:規模化時序資料儲存

圖 3:通過資料分塊實現自動擴充套件

寫操作流

如圖 3 所示,打包壓縮資料基於一個預先設定的分塊大小切分為多個分塊。各個分塊使用標識 CustomerId$Version$ChunkNumber 並行寫入到不同的行中。在成功寫入分塊資料後,後設資料會寫入一個標識為 CustomerId 的單獨行中。對非常大的打包觀看資料,這一做法將寫延遲限制為兩次寫操作。這時,後設資料行實現為一個不具有資料列的行。這種實現支援對後設資料的快速讀操作。

為加快對通常情況(即經壓縮的觀看資料規模小於預定的閾值)的處理,我們將後設資料與觀看資料合併為一行,消除查詢後設資料的開銷,如圖 2 所示。

讀操作流

在讀取時,首先會使用行標識 CustomerId 讀取後設資料行。對於通常情況,分塊數是 1,後設資料行中包括了打包壓縮觀看資料的最新版本。對於罕見情況,存在多個壓縮觀看資料的分塊。我們使用後設資料資訊(例如版本和分塊數)對不同分塊生成不同的行標識,並行讀取所有的分塊。這將讀延遲限制為兩次讀操作。

改進快取層

為了支援對大型條目的分塊,我們還改進了記憶體中的快取層。對於存在大量觀看歷史的會員,整個壓縮的觀看歷史可能無法置於單個 EVCache 條目中。因此,我們採用類似於對 CompressedVH 模型的做法,將每個大型快取條目分割為多個分塊,並將後設資料儲存在首個分塊中。

結果

在引入了並行讀寫、資料壓縮和資料模型改進後,團隊達成了如下目標:

  1. 通過資料壓縮,實現了佔用更少的儲存空間;
  2. 通過分塊和並行讀寫,給出了一致的讀寫效能;
  3. 對於通常情況,延遲限制為一次讀寫。對於罕見情況,延遲限制為兩次讀寫。
Netflix實戰指南:規模化時序資料儲存

圖 4:執行結果

團隊實現了資料規模縮減約 6 倍,Cassandra 維護時間降低約 13 倍,平均讀延遲降低約 5 倍,平均寫時間降低約 1.5 倍。更為重要的是,團隊實現了一種可擴充套件的架構和儲存空間,可適應 Netflix 觀看資料的快速增長。

在該部落格系列文章的第二部分中,我們將介紹儲存規模化中的一些最新挑戰。這些挑戰推動了會員觀看歷史資料儲存架構的下一輪更新。如果讀者對解決類似問題感興趣,可加入到我們的團隊中。

檢視英文原文:

medium.com/netflix-tec…

更多幹貨內容,可關注AI前線,ID:ai-front,後臺回覆「AI」、「TF」、「大資料」可獲得《AI前線》系列PDF迷你書和技能圖譜。

相關文章