揭秘!Netflix百萬用戶的鍵值數據抽象層及其設計理念
在 Netflix,我們為數百萬用戶提供無縫、高質量的流媒體體驗的能力取決于強大的全球后端基礎設施。該基礎設施的核心是我們使用多個在線分布式數據庫,例如Apache Cassandra,這是一種以高可用性和可擴展性而聞名的 NoSQL 數據庫。Cassandra 是 Netflix 內各種用例的支柱,從用戶注冊和存儲觀看歷史記錄到支持實時分析和直播。
隨著新鍵值數據庫的引入和服務所有者推出新用例,我們遇到了許多數據存儲誤用方面的挑戰。首先,開發人員很難在這種跨多個商店的復雜全球部署中推斷一致性、耐用性和性能。其次,開發人員必須不斷重新學習新的數據建模實踐和常見但關鍵的數據訪問模式。這些挑戰包括尾部延遲和冪等性、管理具有多行的“寬”分區、處理單個大型“胖”列以及響應分頁緩慢。此外,與多個本機數據庫 API 的緊密耦合(這些 API 不斷發展,有時會引入向后不兼容的更改)導致整個組織都在進行工程工作以維護和優化我們的微服務的數據訪問。
為了克服這些挑戰,我們開發了一種基于數據網關平臺的整體方法。這種方法促成了幾種基礎抽象服務的創建,其中最成熟的是我們的鍵值 (KV) 數據抽象層 (DAL)。這種抽象簡化了數據訪問,增強了我們基礎設施的可靠性,并使我們能夠以最少的開發人員工作量支持 Netflix 要求的廣泛用例。
在這篇文章中,我們深入探討了 Netflix 的 KV 抽象的工作原理、指導其設計的架構原則、我們在擴展不同用例時面臨的挑戰,以及使我們能夠實現 Netflix 全球運營所需的性能和可靠性的技術創新。
鍵值服務
引入 KV 數據抽象服務是為了解決我們在分布式數據庫中面臨的數據訪問模式的持續挑戰。我們的目標是構建一個多功能且高效的數據存儲解決方案,可以處理各種各樣的用例,從最簡單的哈希圖到更復雜的數據結構,同時確保高可用性、可調一致性和低延遲。
數據模型
KV 抽象的核心是兩級映射 架構。第一級是散列字符串ID(主鍵),第二級是字節鍵值對的有序映射。此模型支持簡單和復雜的數據模型,在靈活性和效率之間取得平衡。
HashMap <String, SortedMap <Bytes, Bytes>>Records對于結構化或按時間順序排列的復雜數據模型Events,這種兩級方法可以有效地處理分層結構,從而允許一起檢索相關數據。對于更簡單的用例,它還表示平面鍵值Maps(例如id → {"" → value})或命名Sets(例如id → {key → ""})。這種適應性使 KV 抽象可用于數百種不同的用例,使其成為在 Netflix 等大型基礎設施中管理簡單和復雜數據模型的多功能解決方案。
KV 數據可以在高層次上進行可視化,如下圖所示,其中顯示了三條記錄。
圖片
message Item (
Bytes key,
Bytes value,
Metadata metadata,
Integer chunk
)數據庫無關的抽象
KV 抽象旨在隱藏底層數據庫的實現細節,為應用程序開發人員提供一致的接口,而不管該用例的最佳存儲系統是什么。雖然 Cassandra 就是一個例子,但該抽象適用于多種數據存儲,如EVCache、DynamoDB、RocksDB等……
例如,當使用 Cassandra 實現時,抽象利用了 Cassandra 的分區和聚類功能。記錄ID充當分區鍵,項目鍵充當聚類列:
圖片
Cassandra 中此結構對應的數據定義語言 (DDL) 是:
CREATE TABLE IF NOT EXISTS <ns>.<table> (
id text,
key blob,
value blob,
value_metadata blob,
PRIMARY KEY (id, key))
WITH CLUSTERING ORDER BY (key <ASC|DESC>)命名空間:邏輯和物理配置
命名空間定義了數據的存儲位置和存儲方式,在抽象底層存儲系統的同時提供邏輯和物理分離。它還充當訪問模式(例如一致性或延遲目標)的中央配置。每個命名空間可以使用不同的后端:Cassandra、EVCache 或多個后端的組合。這種靈活性使我們的數據平臺能夠根據性能、耐用性和一致性需求將不同的用例路由到最合適的存儲系統。開發人員只需提供他們的數據問題,而不是數據庫解決方案!
在此示例配置中,ngsegment命名空間由 Cassandra 集群和 EVCache 緩存層支持,從而實現高度耐用的持久存儲和低延遲點讀取。
"persistence_configuration":[
{
"id":"PRIMARY_STORAGE",
"physical_storage": {
"type":"CASSANDRA",
"cluster":"cassandra_kv_ngsegment",
"dataset":"ngsegment",
"table":"ngsegment",
"regions": ["us-east-1"],
"config": {
"consistency_scope": "LOCAL",
"consistency_target": "READ_YOUR_WRITES"
}
}
},
{
"id":"CACHE",
"physical_storage": {
"type":"CACHE",
"cluster":"evcache_kv_ngsegment"
},
"config": {
"default_cache_ttl": 180s
}
}
]KV 抽象的關鍵 API
為了支持不同的用例,KV 抽象提供了四個基本的 CRUD API:
PutItems — 將一個或多個項目寫入記錄
該PutItemsAPI是一個upsert操作,它可以在兩級map結構中插入新數據或者更新現有數據。
message PutItemRequest (
IdempotencyToken idempotency_token,
string namespace,
string id,
List<Item> items
)如您所見,請求包括命名空間、記錄 ID、一個或多個項目以及冪等性令牌,以確保重試相同的寫入是安全的。可以通過暫存塊然后使用適當的元數據(例如塊數)提交它們來寫入分塊數據。
**GetItems **— 從記錄中讀取一個或多個項目
該GetItemsAPI 提供了一種結構化且自適應的方法,可使用 ID、謂詞和選擇機制來獲取數據。這種方法既能滿足檢索大量數據的需求,又能滿足嚴格的性能和可靠性服務級別目標 (SLO)。
message GetItemsRequest (
String namespace,
String id,
Predicate predicate,
Selection selection,
Map<String, Struct> signals
)其中GetItemsRequest包括幾個關鍵參數:
- 命名空間:指定邏輯數據集或表
- Id:標識頂級 HashMap 中的條目
- 謂詞:過濾匹配的項目,可以檢索所有項目 ( match_all)、特定項目 ( match_keys) 或某個范圍 ( match_range)
- 選擇:縮小返回的響應范圍,例如page_size_bytes分頁、item_limit限制頁面中的項目總數和include/exclude或從響應中包含或排除較大的值
- **信號:**提供帶內信令來指示客戶端功能,例如支持客戶端壓縮或分塊。
該GetItemResponse消息包含匹配的數據:
message GetItemResponse (
List<Item> items,
Optional<String> next_page_token
)- 項目:根據請求中定義Predicate檢索到的項目列表。Selection
- 下一頁標記:可選標記,用于指示后續讀取的位置(如果需要),這對于處理跨多個請求的大型數據集至關重要。分頁是有效管理數據檢索的關鍵組件,尤其是在處理可能超出典型響應大小限制的大型數據集時。
DeleteItems — 從記錄中刪除一個或多個項目
該DeleteItemsAPI 提供了靈活的數據刪除選項,包括記錄級、項目級和范圍刪除——同時支持冪等性。
message DeleteItemsRequest (
IdempotencyToken idempotency_token,
String namespace,
String id,
Predicate predicate
)就像在 API 中一樣GetItems,Predicate允許一次處理一個或多個項目:
- 記錄級刪除(match_all):無論記錄中的項目數有多少,都會以恒定的延遲刪除整個記錄。
- 項目范圍刪除(match_range):這將刪除記錄中的一系列項目。對于保留“n 個最新”或前綴路徑刪除很有用。
- 項目級刪除(match_keys):刪除一個或多個單獨的項目。
某些存儲引擎(任何推遲真正刪除的存儲)如 Cassandra 因墓碑和壓縮開銷而難以處理大量刪除。鍵值優化記錄和范圍刪除,以便為操作生成單個墓碑 — 您可以在關于刪除和墓碑中了解有關墓碑的更多信息。
項目級刪除會創建許多墓碑,但 KV 通過基于TTL 的抖動刪除隱藏了存儲引擎的復雜性。項目元數據不會立即刪除,而是更新為已過期,并使用隨機抖動的 TTL 來錯開刪除。此技術可維護讀取分頁保護。雖然這不能完全解決問題,但它可以減少負載峰值并有助于在壓縮趕上時保持一致的性能。這些策略有助于保持系統性能、減少讀取開銷并通過最大限度地減少刪除的影響來滿足 SLO。
復雜的 Mutate 和 Scan API
除了對單個記錄進行簡單的 CRUD 之外,KV 還支持通過MutateItems和ScanItemsAPI 進行復雜的多項目和多記錄變更和掃描。PutItems還支持通過分塊協議在單個記錄中對大型 blob 數據進行原子寫入Item。這些復雜的 API 需要仔細考慮以確保可預測的線性低延遲,我們將在以后的文章中分享有關其實現的詳細信息。
可靠且可預測的性能設計理念
冪等性可以解決尾部延遲問題
為了確保數據完整性PutItems,DeleteItemsAPI 使用冪等性令牌,它可以唯一地標識每個可變操作,并保證操作按邏輯順序執行,即使由于延遲原因而進行對沖或重試也是如此。這在 Cassandra 等最后寫入獲勝的數據庫中尤其重要,因為確保請求的正確順序和重復數據刪除至關重要。
在 Key-Value 抽象中,冪等性 token 包含生成時間戳和隨機 nonce token。后端存儲引擎可能需要其中一個或兩個來刪除重復的突變。
message IdempotencyToken (
Timestamp generation_time,
String token
)在 Netflix,客戶端生成的單調令牌因其可靠性而受到青睞,尤其是在網絡延遲可能影響服務器端令牌生成的環境中。這將客戶端提供的單調generation_time時間戳與 128 位隨機 UUID相結合token。雖然基于時鐘的令牌生成可能會受到時鐘偏差的影響,但我們在 EC2 Nitro 實例上的測試表明偏差很小(不到 1 毫秒)。在某些需要更強排序的情況下,可以使用 Zookeeper 等工具生成區域唯一令牌,或者可以使用交易 ID 等全局唯一令牌。
下圖展示了我們在 Cassandra 集群上觀察到的時鐘偏差,表明該技術在可直接訪問高質量時鐘的現代云虛擬機上是安全的。為了進一步保持安全性,KV 服務器拒絕帶有較大偏移的令牌的寫入,這既可以防止易受這些攻擊的存儲引擎出現靜默寫入丟棄(寫入的時間戳遠在過去)和不可變的末日石(寫入的時間戳遠在未來)。
圖片
通過分塊處理大數據
鍵值對還旨在高效處理大型數據塊,這是傳統鍵值對存儲的常見挑戰。數據庫通常面臨每個鍵或分區可存儲的數據量限制。為了解決這些限制,KV 使用透明分塊來高效管理大數據。
對于小于 1 MiB 的項目,數據直接存儲在主后備存儲(例如 Cassandra)中,以確保快速高效的訪問。但是,對于較大的項目,只有 id 、 key和元數據存儲在主存儲中,而實際數據被分成較小的塊并單獨存儲在塊存儲中。此塊存儲也可以是 Cassandra,但具有針對處理大值優化的不同分區方案。冪等性令牌將所有這些寫入綁定到一個原子操作中。
通過將大型項目拆分成塊,我們確保延遲與數據大小成線性比例,從而使系統既可預測又高效。未來的博客文章將更詳細地描述分塊架構,包括其復雜性和優化策略。
客戶端壓縮
KV 抽象利用客戶端有效負載壓縮來優化性能,尤其是對于大數據傳輸。雖然許多數據庫都提供服務器端壓縮,但在客戶端處理壓縮可以減少昂貴的服務器 CPU 使用率、網絡帶寬和磁盤 I/O。在我們的一個部署中,這有助于支持 Netflix 的搜索,啟用客戶端壓縮可將有效負載大小減少 75%,從而顯著提高成本效率。
更智能的分頁
我們選擇以字節為單位的有效負載大小作為每個響應頁面的限制,而不是項目數量,因為這使我們能夠提供可預測的操作 SLO。例如,我們可以在 2 MiB 頁面讀取上提供個位數毫秒的 SLO。相反,使用每頁項目數作為限制會導致不可預測的延遲,因為項目大小存在很大差異。如果每頁 10 個項目的請求是 1 KiB 而不是 1 MiB,則延遲可能會有很大差異。
使用字節作為限制會帶來挑戰,因為很少有后備存儲支持基于字節的分頁;大多數數據存儲使用結果數(例如 DynamoDB 和 Cassandra 按項目數或行數進行限制)。為了解決這個問題,我們對后備存儲的初始查詢使用靜態限制,使用此限制進行查詢,然后處理結果。如果需要更多數據來滿足字節限制,則將執行其他查詢,直到滿足限制,丟棄多余的結果并生成頁面令牌。
這種靜態限制可能會導致效率低下,結果中的一個大項可能會導致我們丟棄許多結果,而小項可能需要多次迭代才能填滿一頁,從而導致讀取放大。為了緩解這些問題,我們實現了自適應分頁,可根據觀察到的數據動態調整限制。
自適應分頁
當發出初始請求時,將在存儲引擎中執行查詢并檢索結果。當消費者處理這些結果時,系統會跟蹤消費的項目數量和使用的總大小。這些數據有助于計算近似的項目大小,該大小存儲在頁面令牌中。對于后續的頁面請求,這些存儲的信息允許服務器對底層存儲應用適當的限制,從而減少不必要的工作并最大限度地減少讀取放大。
雖然此方法對于后續頁面請求有效,但對于初始請求會發生什么情況?除了將項目大小信息存儲在頁面令牌中之外,服務器還會估算給定命名空間的平均項目大小并將其緩存在本地。此緩存估算值可幫助服務器為初始請求在后備存儲上設置更優化的限制,從而提高效率。服務器會根據最近的查詢模式或其他因素不斷調整此限制以保持其準確性。對于后續頁面,服務器會同時使用緩存數據和頁面令牌中的信息來微調限制。
圖片
除了自適應分頁之外,如果服務器檢測到處理請求有可能超出請求的延遲 SLO,則還有一種機制可以提前發送響應。
例如,假設客戶端提交的GetItems請求每頁限制為 2 MiB,最大端到端延遲限制為 500 毫秒。在處理此請求時,服務器會從后備存儲中檢索數據。此特定記錄包含數千個小項目,因此收集整頁數據通常需要的時間超過 500 毫秒的 SLO。如果發生這種情況,客戶端將收到 SLO 違規錯誤,導致請求失敗,即使沒有任何異常。為了防止這種情況,服務器會在獲取數據時跟蹤已用時間。如果它確定繼續檢索更多數據可能會違反 SLO,則服務器將停止處理進一步的結果并返回帶有分頁令牌的響應。
圖片
這種方法可確保請求在 SLO 內得到處理,即使未滿足整個頁面大小,也能為客戶端提供可預測的進度。此外,如果客戶端是具有適當截止期限的 gRPC 服務器,則客戶端足夠智能,不會發出進一步的請求,從而減少無用的工作。
如果您想了解更多信息,Netflix 如何確保高可靠性的在線狀態系統一文將更詳細地討論這些技術以及許多其他技術。
信號
KV 使用帶內消息傳遞(我們稱之為信令),允許動態配置客戶端,并使其能夠將其功能傳達給服務器。這確保了配置設置和調整參數可以在客戶端和服務器之間無縫交換。如果沒有信令,客戶端將需要靜態配置(每次更改都需要重新部署),或者,如果使用動態配置,則需要與客戶端團隊進行協調。
對于服務器端信號,當客戶端初始化時,它會向服務器發送握手。服務器會以信號(例如目標或最大延遲 SLO)進行響應,從而允許客戶端動態調整超時和對沖策略。然后會在后臺定期進行握手以保持配置最新。對于客戶端通信信號,客戶端會隨每個請求一起傳達其功能,例如它是否可以處理壓縮、分塊和其他功能。
圖片
Netflix 的 KV 使用情況
KV 抽象支持 Netflix 的幾個關鍵用例,包括:
- 流元數據:高吞吐量、低延遲訪問流元數據,確保實時個性化內容交付。
- 用戶資料:高效存儲和檢索用戶偏好和歷史記錄,實現跨設備的無縫、個性化體驗。
- 消息傳遞:存儲和檢索消息傳遞需求的推送注冊表,使數百萬個請求能夠流經。
- 實時分析:這可以持久產生大規模印象并提供對用戶行為和系統性能的洞察,將數據從離線移動到在線,反之亦然。
未來的增強功能
展望未來,我們計劃通過以下方式增強 KV 抽象:
- 生命周期管理:對數據保留和刪除的細粒度控制。
- 匯總:通過將包含多項的記錄匯總為較少的后備行來提高檢索效率的技術。
- 新的存儲引擎:與更多存儲系統集成以支持新的用例。
- 字典壓縮:在保持性能的同時進一步減少數據大小。
結論
Netflix 的鍵值服務是一種靈活、經濟高效的解決方案,支持從低流量到高流量場景的各種數據模式和用例,包括關鍵的 Netflix 流媒體用例。簡單而強大的設計使其能夠處理各種數據模型,如 HashMap、集合、事件存儲、列表和圖形。它從我們的開發人員那里抽象了底層數據庫的復雜性,使我們的應用程序工程師能夠專注于解決業務問題,而不是成為每個存儲引擎及其分布式一致性模型的專家。隨著 Netflix 不斷在在線數據存儲方面進行創新,KV 抽象仍然是高效、可靠地大規模管理數據的核心組件,為未來的增長奠定了堅實的基礎。



























