Netflix實戰指南:規模化時序數據存儲

本文由 「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 標識。這種水平分區設計支持數據存儲隨會員數量的增長而有效擴展,並支持簡單並高效地讀取會員的完整觀看歷史數據。這一讀取操作是歷史數據存儲上最頻繁發生的操作。然而,隨著會員數量的持續增長,尤其是每位會員觀看的視頻流越來越多,存儲的數據行數和整體數據量也日益膨脹。隨著時間的推移,這將導致存儲和操作的成本增大。而且對於觀看了大量視頻的會員而言,性能會嚴重降低。

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

圖 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 負責處理非常罕見的重複寫問題(即以最後寫入的數據為準)。

圖 2:實時數據和壓縮數據的操作模型

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

通過分塊實現自動擴展

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

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

圖 3:通過數據分塊實現自動擴展

寫操作流

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

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

讀操作流

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

改進緩存層

為了支持對大型條目的分塊,我們還改進了內存中的緩存層。對於存在大量觀看歷史的會員,整個壓縮的觀看歷史可能無法置於單個 EVCache 條目中。因此,我們採用類似於對 CompressedVH 模型的做法,將每個大型緩存條目分割為多個分塊,並將元數據存儲在首個分塊中。

結果

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

  1. 通過數據壓縮,實現了佔用更少的存儲空間;
  2. 通過分塊和並行讀寫,給出了一致的讀寫性能;
  3. 對於通常情況,延遲限制為一次讀寫。對於罕見情況,延遲限制為兩次讀寫。

圖 4:運行結果

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

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

查看英文原文:

medium.com/netflix-tech

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

推薦閱讀:

美國視頻流媒體大戰,Netflix、Amazon和Hulu的的獨家內容是制敵利器么?
【2016新美劇推薦】《Luke Cage:盧克凱奇》第一季第一集至第六集影評:無堅不摧的街頭正義
怪奇物語第二季觀後感?
跟花和尚學系統設計:明星公司之Netflix(中篇)
檢驗智商的時刻到了!Netflix出了部燒腦德劇

TAG:Netflix | 數據存儲技術 |