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