Hudi 實(shí)踐 | Notion 數(shù)據(jù)湖構(gòu)建和擴(kuò)展之路
共 7187字,需瀏覽 15分鐘
·
2024-07-25 14:16
在過(guò)去三年中,由于用戶(hù)和內(nèi)容的增長(zhǎng),Notion 的數(shù)據(jù)增長(zhǎng)了 10 倍,以 6-12 個(gè)月的速度翻了一番。要管理這種快速增長(zhǎng),同時(shí)滿(mǎn)足關(guān)鍵產(chǎn)品和分析用例不斷增長(zhǎng)的數(shù)據(jù)需求,尤其是我們最近的 Notion AI 功能,意味著構(gòu)建和擴(kuò)展 Notion 的數(shù)據(jù)湖。以下來(lái)介紹我們是如何做到的。
Notion 的數(shù)據(jù)模型和增長(zhǎng)
在 Notion 中看到的所有內(nèi)容(文本、圖像、標(biāo)題、列表、數(shù)據(jù)庫(kù)行、頁(yè)面等)盡管前端表示和行為不同,但在后端被建模為“塊”實(shí)體,并存儲(chǔ)在具有一致結(jié)構(gòu)、架構(gòu)和相關(guān)元數(shù)據(jù)的 Postgres 數(shù)據(jù)庫(kù)中(了解有關(guān) Notion 數(shù)據(jù)模型的更多信息)。
在用戶(hù)活動(dòng)和內(nèi)容創(chuàng)作的推動(dòng)下,所有這些區(qū)塊數(shù)據(jù)每 6 到 12 個(gè)月翻一番。在 2021 年初,我們?cè)?Postgres 中有超過(guò) 200 億個(gè)區(qū)塊行,此后這個(gè)數(shù)字已經(jīng)增長(zhǎng)到超過(guò) 2000 億個(gè)區(qū)塊——即使壓縮后的數(shù)據(jù)量也高達(dá)數(shù)百 TB。為了在增強(qiáng)用戶(hù)體驗(yàn)的同時(shí)管理這種數(shù)據(jù)增長(zhǎng),我們戰(zhàn)略性地將數(shù)據(jù)庫(kù)基礎(chǔ)設(shè)施從一個(gè) Postgres 實(shí)例擴(kuò)展到更復(fù)雜的分片架構(gòu)。我們從 2021 年開(kāi)始將 Postgres 數(shù)據(jù)庫(kù)水平分片為 32 個(gè)物理實(shí)例,每個(gè)實(shí)例包含 15 個(gè)邏輯分片,并在 2023 年繼續(xù)將物理實(shí)例數(shù)量增加到 96 個(gè),每個(gè)實(shí)例有 5 個(gè)邏輯分片。因此,我們總共維護(hù)了 480 個(gè)邏輯分片,同時(shí)確保了長(zhǎng)期可擴(kuò)展的數(shù)據(jù)管理和檢索能力。
到 2021 年,Postgres 構(gòu)成了我們生產(chǎn)基礎(chǔ)設(shè)施的核心,處理從在線(xiàn)用戶(hù)流量到各種離線(xiàn)數(shù)據(jù)分析和機(jī)器學(xué)習(xí)需求的所有內(nèi)容。隨著對(duì)線(xiàn)上和線(xiàn)下數(shù)據(jù)需求的增加,我們意識(shí)到構(gòu)建一個(gè)專(zhuān)用的數(shù)據(jù)基礎(chǔ)設(shè)施來(lái)處理離線(xiàn)數(shù)據(jù)而不干擾在線(xiàn)流量至關(guān)重要。
2021 年 Notion 的數(shù)據(jù)倉(cāng)庫(kù)架構(gòu)
2021 年,我們通過(guò)一個(gè)簡(jiǎn)單的 ELT(提取、加載和轉(zhuǎn)換)管道啟動(dòng)了這個(gè)專(zhuān)用數(shù)據(jù)基礎(chǔ)設(shè)施,該管道使用第三方工具 Fivetran 將數(shù)據(jù)從 Postgres WAL(預(yù)寫(xiě)日志)攝取到 Snowflake,并為 480 個(gè)分片設(shè)置了 480 個(gè)每小時(shí)運(yùn)行的連接器,以寫(xiě)入相同數(shù)量的原始 Snowflake 表。然后我們將這些表合并為一個(gè)大表,用于分析、報(bào)告和機(jī)器學(xué)習(xí)用例。
擴(kuò)展挑戰(zhàn)
隨著 Postgres 數(shù)據(jù)的增長(zhǎng),我們遇到了一些擴(kuò)展挑戰(zhàn)。
速度、數(shù)據(jù)新鮮度和成本
將數(shù)據(jù)攝取到 Snowflake 的速度變慢且成本更高,這主要是由于 Notion 獨(dú)特的更新繁重工作負(fù)載。Notion 用戶(hù)更新現(xiàn)有塊(文本、標(biāo)題、標(biāo)題、項(xiàng)目符號(hào)列表、數(shù)據(jù)庫(kù)行等)的頻率遠(yuǎn)遠(yuǎn)高于添加新塊的頻率。這導(dǎo)致塊數(shù)據(jù)主要是更新量大的 ~90% 的 Notion 更新插入是更新。大多數(shù)數(shù)據(jù)倉(cāng)庫(kù)(包括 Snowflake)都針對(duì)插入繁重的工作負(fù)載進(jìn)行了優(yōu)化,這使得它們攝取塊數(shù)據(jù)變得越來(lái)越具有挑戰(zhàn)性。
用例支持
數(shù)據(jù)轉(zhuǎn)換邏輯變得更加復(fù)雜和繁重,超過(guò)了現(xiàn)成數(shù)據(jù)倉(cāng)庫(kù)提供的標(biāo)準(zhǔn) SQL 接口的功能。
? 一個(gè)重要的用例是為關(guān)鍵產(chǎn)品(例如 AI 和搜索)構(gòu)建 Notion 區(qū)塊數(shù)據(jù)的非規(guī)范化視圖。例如,權(quán)限數(shù)據(jù)確保只有正確的人才能讀取或更改塊(本博客討論 Notion 的塊權(quán)限模型)。但是一個(gè)區(qū)塊的權(quán)限并不是靜態(tài)地存儲(chǔ)在相關(guān)的Postgres中,它必須通過(guò)昂貴的樹(shù)遍歷計(jì)算來(lái)動(dòng)態(tài)構(gòu)建。
? 在以下示例中,
block_1,block_2, 并block_3繼承其直接父級(jí) (page_3和page_2) 和祖先 (page_1 ``workspace_a).和 和 要為每個(gè)塊構(gòu)建權(quán)限數(shù)據(jù),我們必須遍歷其祖先樹(shù)一直到根 (workspace_a),以確保完整性。由于有數(shù)千億個(gè)區(qū)塊,其祖先深度從幾個(gè)到幾十個(gè)不等,這種計(jì)算成本非常高,而且只會(huì)在 Snowflake 中超時(shí)。
由于這些挑戰(zhàn),我們開(kāi)始探索構(gòu)建我們的數(shù)據(jù)湖。
構(gòu)建和擴(kuò)展 Notion 的內(nèi)部數(shù)據(jù)湖
以下是我們構(gòu)建內(nèi)部數(shù)據(jù)湖的目標(biāo):
? 建立一個(gè)能夠大規(guī)模存儲(chǔ)原始數(shù)據(jù)和處理數(shù)據(jù)的數(shù)據(jù)存儲(chǔ)庫(kù)。
? 為任何工作負(fù)載(尤其是 Notion 的更新密集型塊數(shù)據(jù))實(shí)現(xiàn)快速、可擴(kuò)展、可操作且經(jīng)濟(jì)高效的數(shù)據(jù)攝取和計(jì)算。
? 解鎖需要非規(guī)范化數(shù)據(jù)的 AI、搜索和其他產(chǎn)品用例。
但是,雖然我們的數(shù)據(jù)湖是向前邁出的一大步,但重要的是要澄清它不打算做什么:
? 完全替換 Snowflake。我們將繼續(xù)受益于 Snowflake 的操作和生態(tài)系統(tǒng)易用性,將其用于大多數(shù)其他工作負(fù)載,尤其是那些插入量大且不需要大規(guī)模非規(guī)范化樹(shù)遍歷的工作負(fù)載。
? 完全替換 Fivetran。我們將繼續(xù)利用 Fivetran 在非更新繁重表、小型數(shù)據(jù)集攝取以及多樣化的第三方數(shù)據(jù)源和目標(biāo)方面的有效性。
? 支持需要二級(jí)或更嚴(yán)格延遲的在線(xiàn)用例。Notion 數(shù)據(jù)湖將主要關(guān)注可以容忍幾分鐘到幾小時(shí)延遲的離線(xiàn)工作負(fù)載。
數(shù)據(jù)湖的高級(jí)設(shè)計(jì)
自 2022 年以來(lái),我們一直使用如下所示的內(nèi)部數(shù)據(jù)湖架構(gòu)。我們使用 Debezium CDC 連接器將增量更新的數(shù)據(jù)從 Postgres 攝取到 Kafka,然后使用 Apache Hudi(一個(gè)開(kāi)源數(shù)據(jù)處理和存儲(chǔ)框架)將這些更新從 Kafka 寫(xiě)入 S3。然后利用這些原始數(shù)據(jù),我們可以進(jìn)行轉(zhuǎn)換、非規(guī)范化(例如,每個(gè)塊的樹(shù)遍歷和權(quán)限數(shù)據(jù)構(gòu)建)和擴(kuò)充,然后將處理后的數(shù)據(jù)再次存儲(chǔ)在 S3 中或下游系統(tǒng)中,以滿(mǎn)足分析和報(bào)告需求,以及 AI、搜索和其他產(chǎn)品要求。
接下來(lái),我們將描述和說(shuō)明我們?cè)趶V泛的研究、討論和原型設(shè)計(jì)工作后得出的設(shè)計(jì)原則和決策。
設(shè)計(jì)決策 1:選擇數(shù)據(jù)存儲(chǔ)庫(kù)和湖
我們的第一個(gè)決定是將 S3 用作數(shù)據(jù)存儲(chǔ)庫(kù)和湖來(lái)存儲(chǔ)所有原始和處理過(guò)的數(shù)據(jù),并將數(shù)據(jù)倉(cāng)庫(kù)和其他面向產(chǎn)品的數(shù)據(jù)存儲(chǔ)(如 ElasticSearch、Vector Database、Key-Value Store 等)定位為其下游。我們做出這個(gè)決定有兩個(gè)原因:
? 它與 Notion 的 AWS 技術(shù)堆棧保持一致,例如,我們的 Postgres 數(shù)據(jù)庫(kù)基于 AWS RDS,其導(dǎo)出到 S3 的功能(在后面的部分中描述)允許我們輕松地在 S3 中引導(dǎo)表。
? S3 已經(jīng)證明了它能夠以低成本存儲(chǔ)大量數(shù)據(jù)并支持各種數(shù)據(jù)處理引擎(如 Spark)。
通過(guò)將繁重的攝取和計(jì)算工作負(fù)載卸載到 S3,并僅將高度清理的業(yè)務(wù)關(guān)鍵型數(shù)據(jù)攝取到 Snowflake 和面向產(chǎn)品的數(shù)據(jù)存儲(chǔ),我們顯著提高了數(shù)據(jù)計(jì)算的可擴(kuò)展性和速度,并降低了成本。
設(shè)計(jì)決策 2:選擇處理引擎
我們選擇Spark作為我們的主要數(shù)據(jù)處理引擎,因?yàn)樽鳛橐粋€(gè)開(kāi)源框架,它可以快速設(shè)置和評(píng)估,以驗(yàn)證它是否滿(mǎn)足我們的數(shù)據(jù)轉(zhuǎn)換需求。Spark 具有四個(gè)主要優(yōu)勢(shì):
? Spark 除了 SQL 之外,還具有廣泛的內(nèi)置函數(shù)和 UDF(用戶(hù)定義函數(shù)),可實(shí)現(xiàn)復(fù)雜的數(shù)據(jù)處理邏輯,如樹(shù)遍歷和塊數(shù)據(jù)非規(guī)范化,如上所述。
? 它為大多數(shù)輕量級(jí)用例提供了用戶(hù)友好的 PySpark 框架,并為高性能、繁重的數(shù)據(jù)處理提供了高級(jí) Scala Spark。
? 它以分布式方式處理大規(guī)模數(shù)據(jù)(例如,數(shù)十億個(gè)塊和數(shù)百 TB),并公開(kāi)廣泛的配置,這使我們能夠微調(diào)對(duì)分區(qū)、數(shù)據(jù)傾斜和資源分配的控制。它還使我們能夠?qū)?fù)雜的作業(yè)分解為更小的任務(wù),并優(yōu)化每個(gè)任務(wù)的資源配置,這有助于我們實(shí)現(xiàn)合理的運(yùn)行時(shí),而不會(huì)過(guò)度配置或浪費(fèi)資源。
? 最后,Spark的開(kāi)源特性提供了成本效益優(yōu)勢(shì)。
設(shè)計(jì)決策 3:優(yōu)先于快照轉(zhuǎn)儲(chǔ)增量攝取
在完成我們的數(shù)據(jù)湖存儲(chǔ)和處理引擎后,我們探索了將 Postgres 數(shù)據(jù)攝取到 S3 的解決方案。我們最終考慮了兩種方法:增量攝取更改的數(shù)據(jù)和 Postgres 表的定期完整快照。最后,基于性能和成本的比較,我們選擇了混合設(shè)計(jì):
? 在正常操作期間,以增量方式攝取更改的 Postgres 數(shù)據(jù)并將其持續(xù)應(yīng)用于 S3。
? 在極少數(shù)情況下,導(dǎo)出完整的 Postgres 快照以引導(dǎo) S3 中的表。
增量方法可確保以更低的成本和最小的延遲(幾分鐘到幾個(gè)小時(shí),具體取決于表大小)獲得更新鮮的數(shù)據(jù)。相比之下,導(dǎo)出完整快照并轉(zhuǎn)儲(chǔ)到 S3 需要 10 多個(gè)小時(shí),成本是 S3 的兩倍,因此在 S3 中引導(dǎo)新表時(shí),我們很少這樣做。
設(shè)計(jì)決策 4:簡(jiǎn)化增量引入
? 用于 Postgres → Kafka 的 Kafka CDC 連接器
我們選擇了 Kafka Debezium CDC(更改數(shù)據(jù)捕獲)連接器將增量更改的 Postgres 數(shù)據(jù)發(fā)布到 Kafka,類(lèi)似于 Fivetran 的數(shù)據(jù)攝取方法。我們之所以選擇它與 Kafka 一起,是因?yàn)樗鼈兙哂锌蓴U(kuò)展性、易于設(shè)置以及與我們現(xiàn)有基礎(chǔ)架構(gòu)的緊密集成。
? 用于 Kafka → S3 的 Hudi
為了將增量數(shù)據(jù)從 Kafka 引入到 S3,我們考慮了三種出色的數(shù)據(jù)湖解決方案:Apache Hudi、Apache Iceberg 和 Databricks Delta Lake。最后我們選擇了 Hudi,因?yàn)樗哂谐錾男阅埽梢蕴幚泶罅扛碌墓ぷ髫?fù)載,并且具有開(kāi)源特性以及與 Debezium CDC 消息的原生集成。另一方面,當(dāng)我們?cè)?2022 年考慮 Iceberg 和 Delta Lake 時(shí),它們并沒(méi)有針對(duì)我們的更新繁重工作負(fù)載進(jìn)行優(yōu)化。Iceberg 還缺乏一個(gè)能夠理解 Debezium 消息的開(kāi)箱即用的解決方案;Delta Lake 有一個(gè)但并不開(kāi)源。如果我們采用這兩種解決方案中的任何一個(gè),我們將不得不實(shí)現(xiàn)我們自己的 Debezium 消費(fèi)者。
設(shè)計(jì)決策 5:在處理之前引入原始數(shù)據(jù)
最后,我們決定將原始 Postgres 數(shù)據(jù)攝取到 S3,而無(wú)需進(jìn)行動(dòng)態(tài)處理,以便建立單一事實(shí)來(lái)源并簡(jiǎn)化整個(gè)數(shù)據(jù)管道的調(diào)試。一旦原始數(shù)據(jù)進(jìn)入 S3,我們就會(huì)進(jìn)行轉(zhuǎn)換、非規(guī)范化、擴(kuò)充和其他類(lèi)型的數(shù)據(jù)處理。我們?cè)俅螌⒅虚g數(shù)據(jù)存儲(chǔ)在 S3 中,并且僅將高度清理、結(jié)構(gòu)化和關(guān)鍵業(yè)務(wù)數(shù)據(jù)引入下游系統(tǒng),以滿(mǎn)足分析、報(bào)告和產(chǎn)品需求。
擴(kuò)展和運(yùn)營(yíng)我們的數(shù)據(jù)湖
我們嘗試了許多詳細(xì)的設(shè)置,以解決與 Notion 不斷增長(zhǎng)的數(shù)據(jù)量相關(guān)的可擴(kuò)展性挑戰(zhàn)。以下是我們嘗試的內(nèi)容和進(jìn)展情況:
CDC 連接器和 Kafka 設(shè)置
我們?cè)诿總€(gè) Postgres 主機(jī)上設(shè)置一個(gè) Debezium CDC 連接器,并將它們部署在 AWS EKS 集群中。由于 Debezium 和 EKS 管理的成熟度以及 Kafka 的可擴(kuò)展性,我們?cè)谶^(guò)去兩年中只需要升級(jí)幾次 EKS 和 Kafka 集群。截至 2024 年 5 月,它可以順利處理數(shù)十 MB/秒的 Postgres 行變更。我們還為每個(gè) Postgres 表配置一個(gè) Kafka 主題,并讓所有消耗 480 個(gè)分片的連接器寫(xiě)入該表的同一主題。此設(shè)置顯著降低了為每個(gè)表維護(hù) 480 個(gè)主題的復(fù)雜性,并簡(jiǎn)化了下游 Hudi 對(duì) S3 的攝取,從而顯著降低了運(yùn)營(yíng)開(kāi)銷(xiāo)。
Hudi設(shè)置
我們使用 Apache Hudi Deltastreamer(一個(gè)基于 Spark 的攝取作業(yè))來(lái)使用 Kafka 消息并在 S3 中復(fù)制 Postgres 表的狀態(tài)。經(jīng)過(guò)幾輪性能優(yōu)化后,我們建立了一個(gè)快速、可擴(kuò)展的攝取設(shè)置,以確保數(shù)據(jù)新鮮度。對(duì)于大多數(shù)表,此設(shè)置僅提供幾分鐘的延遲,而對(duì)于最大的表(塊表)則提供長(zhǎng)達(dá)兩個(gè)小時(shí)的延遲(見(jiàn)下圖)。
? 我們使用默認(rèn)的 COPY_ON_WRITE Hudi 表類(lèi)型和 UPSERT 操作,這適合我們的更新繁重工作負(fù)載。
? 為了更有效地管理數(shù)據(jù)并最大程度地減少寫(xiě)入放大(即每次批處理攝取運(yùn)行更新的文件數(shù)),我們微調(diào)了三種配置:
? 使用相同的 Postgres 分片方案對(duì)數(shù)據(jù)進(jìn)行分區(qū)/分片,即
hoodie.datasource.write.partitionpath.field: db_schema_source_partition配置。這會(huì)將 S3 數(shù)據(jù)集劃分為 480 個(gè)分片,從shard0001到shard0480, 更有可能將一批傳入更新映射到同一分片中的同一組文件。? 根據(jù)上次更新時(shí)間 (event_lsn) 對(duì)數(shù)據(jù)進(jìn)行排序,即
source-ordering-field: event_lsn配置。這是基于我們的觀察,即較新的塊更有可能得到更新,這使我們能夠僅使用過(guò)時(shí)的塊來(lái)修剪文件。? 將索引類(lèi)型設(shè)置為 bloom filter,即
hoodie.index.type: BLOOM配置,以進(jìn)一步優(yōu)化工作負(fù)載。
Spark數(shù)據(jù)處理設(shè)置
對(duì)于我們的大多數(shù)數(shù)據(jù)處理工作,我們使用 PySpark,其相對(duì)較低的學(xué)習(xí)曲線(xiàn)使許多團(tuán)隊(duì)成員都可以使用它。對(duì)于更復(fù)雜的工作,如樹(shù)遍歷和非規(guī)范化,我們?cè)趲讉€(gè)關(guān)鍵領(lǐng)域利用了Spark的卓越性能:
? 我們受益于 Scala Spark 的性能效率。
? 我們通過(guò)分別處理大分片和小分片來(lái)更有效地管理數(shù)據(jù)(請(qǐng)記住,我們?cè)?S3 中保留了相同的 480 分片方案,以便與 Postgres 保持一致);小分片將其全部數(shù)據(jù)加載到 Spark 任務(wù)容器內(nèi)存中以便快速處理,而超出內(nèi)存容量的大分片則通過(guò)磁盤(pán)重新洗牌進(jìn)行管理。
? 我們利用多線(xiàn)程和并行處理來(lái)加快 480 個(gè)分片的處理速度,從而優(yōu)化運(yùn)行時(shí)間和效率。
引導(dǎo)設(shè)置
以下是我們引導(dǎo)新表的方法:
? 我們首先設(shè)置了 Debezium 連接器,以將 Postgres 更改引入 Kafka。
? 從 timestamp
t開(kāi)始,我們啟動(dòng) AWS RDS 提供的導(dǎo)出到 S3 作業(yè),將 Postgres 表的最新快照保存到 S3。然后,我們創(chuàng)建一個(gè) Spark 作業(yè)來(lái)從 S3 讀取這些數(shù)據(jù),并將它們寫(xiě)入 Hudi 表格式。? 最后,我們通過(guò)設(shè)置 Deltastreamer 從 Kafka 消息中讀取
t來(lái)捕獲快照過(guò)程中所做的所有更改。此步驟對(duì)于保持?jǐn)?shù)據(jù)完整性和完整性至關(guān)重要。
由于 Spark 和 Hudi 的可擴(kuò)展性,這三個(gè)步驟通常在 24 小時(shí)內(nèi)完成,使我們能夠在可管理的時(shí)間內(nèi)執(zhí)行重新引導(dǎo),以適應(yīng)新的表請(qǐng)求和 Postgres 升級(jí)和重新分片操作。
收益:更少的錢(qián),更多的時(shí)間,更強(qiáng)大的人工智能基礎(chǔ)設(shè)施
我們?cè)?2022 年春季開(kāi)始開(kāi)發(fā)數(shù)據(jù)湖基礎(chǔ)設(shè)施,并于當(dāng)年秋季完成。由于 infra 固有的可擴(kuò)展性,我們能夠不斷優(yōu)化和擴(kuò)展 Debezium EKS 集群、Kafka 集群、Deltastreamer 和 Spark 作業(yè),以跟上 Notion 6 到 12 個(gè)月的數(shù)據(jù)倍增率,而無(wú)需進(jìn)行重大檢修。收益巨大:
? 將幾個(gè)大型、關(guān)鍵的 Postgres 數(shù)據(jù)集(其中一些數(shù)據(jù)集高達(dá)數(shù)十 TB)遷移到數(shù)據(jù)湖,使我們?cè)?nbsp;2022 年凈節(jié)省了超過(guò) 100 萬(wàn)美元,并在 2023 年和 2024 年按比例節(jié)省了更高的成本。
? 對(duì)于這些數(shù)據(jù)集,從 Postgres 到 S3 和 Snowflake 的端到端攝取時(shí)間從一天多減少到幾分鐘(對(duì)于小表)減少到幾分鐘,對(duì)于大型表(大表)則長(zhǎng)達(dá)幾個(gè)小時(shí)。必要時(shí),可以在 24 小時(shí)內(nèi)完成重新同步,而不會(huì)使實(shí)時(shí)數(shù)據(jù)庫(kù)過(guò)載。
? 最重要的是,這一轉(zhuǎn)變從各種分析和產(chǎn)品需求中節(jié)省了大量數(shù)據(jù)存儲(chǔ)、計(jì)算和新鮮度,使 Notion AI 功能能夠在 2023 年和 2024 年成功推出。
我們要感謝 OneHouse 和 Hudi 開(kāi)源社區(qū)的巨大及時(shí)支持。強(qiáng)大的開(kāi)源支持對(duì)于我們?cè)诙潭處讉€(gè)月內(nèi)啟動(dòng)數(shù)據(jù)湖的能力至關(guān)重要。
推薦閱讀
Apache Hudi 正式邁向 Rust/Python 生態(tài)圈
2024 年 6 月 Apache Hudi 社區(qū)新聞
Apache Doris + Apache Hudi 快速搭建指南
