9年演進史:位元組跳動 10EB 級大資料儲存實戰

語言: CN / TW / HK

背景

HDFS 簡介

HDFS 全名 Hadoop Distributed File System,是業界使用最廣泛的開源分散式檔案系統。原理和架構與 Google 的 GFS 基本一致。它的特點主要有以下幾項:

  • 和本地檔案系統一樣的目錄樹檢視

  • Append Only 的寫入(不支援隨機寫)

  • 順序和隨機讀

  • 超大資料規模

  • 易擴充套件,容錯率高

HDFS 在位元組跳動的發展

位元組跳動已經應用 HDFS 非常長的時間了。經歷了 9 年的發展,目前已直接支援了十多種資料平臺,間接支援了上百種業務發展。從叢集規模和資料量來說,HDFS 平臺在公司內部已經成長為總數十萬臺級別伺服器的大平臺,支援了 10 EB 級別的資料量。

當前在位元組跳動,HDFS 承載的主要業務如下:

  • Hive,HBase,日誌服務,Kafka 資料儲存

  • Yarn,Flink 的計算框架平臺數據

  • Spark,MapReduce 的計算相關資料儲存

位元組跳動特色的 HDFS 架構

在深入相關的技術細節之前,我們先看看位元組跳動的 HDFS 架構。

架構介紹

位元組跳動 HDFS 架構

接入層

接入層是位元組版 HDFS 區別於社群版本最大的一層,社群版本中並無這一層定義。

在位元組跳動的落地實踐中,由於叢集的節點過於龐大,我們需要非常多的 NameNode 實現聯邦機制來接入不同上層業務的資料服務。但當 NameNode 數量也變得非常多了以後,使用者請求的統一接入及統一檢視的管理也會有很大的問題。為了解決使用者接入過於分散,我們需要一個獨立的接入層來支援使用者請求的統一接入,轉發路由;同時也能結合業務提供使用者許可權和流量控制能力。另外,該接入層也需要提供對外的目錄樹統一檢視。

接入層從部署形態上來講,依賴於一些外部元件如 Redis,MySQL 等,會有一批無狀態的 NNProxy 組成,他們提供了請求路由、Quota 限制、Tracing 能力及流量限速等能力。

元資料層

這一層主要模組有 Name Node、ZKFC 和 BookKeeper(不同於 QJM,BookKeeper 在大規模多節點資料同步上表現得更穩定可靠)。

Name Node 負責儲存整個 HDFS 叢集的元資料資訊,是整個系統的大腦。一旦故障,整個叢集都會陷入不可用狀態。因此 Name Node 有一套基於 ZKFC 的主從熱備的高可用方案。

Name Node 還面臨著擴充套件性的問題,單機承載能力始終受限。於是 HDFS 引入了聯邦(Federation)機制。一個叢集中可以部署多組 Name Node,它們獨立維護自己的元資料,共用 Data Node 儲存資源。這樣,一個 HDFS 叢集就可以無限擴充套件了。但是這種 Federation 機制下,每一組 Name Node 的目錄樹都互相割裂的。於是又出現了一些解決方案,能夠使整個 Federation 叢集對外提供一個完整目錄樹的檢視。

資料層

相比元資料層,資料層主要節點是 Data Node。Data Node 負責實際的資料儲存和讀取。使用者檔案被切分成塊,複製成多副本,每個副本都存在不同的 Data Node 上,以達到容錯容災的效果。每個副本在 Data Node 上都以檔案的形式儲存,元資訊在啟動時被載入到記憶體中。

Data Node 會定時向 Name Node 做心跳彙報,並且週期性將自己所儲存的副本資訊彙報給 Name Node。這個過程對 Federation 中的每個叢集都是獨立完成的。在心跳彙報的返回結果中,會攜帶 Name Node 對 Data Node 下發的指令。例如,需要將某個副本拷貝到另外一臺 Data Node,或者將某個副本刪除等。

發展階段

在位元組跳動,隨著業務的快速發展,HDFS 的資料量和叢集規模快速擴大,原來的 HDFS 的叢集從幾百臺,迅速突破萬臺和十萬臺的規模,此前我們曾梳理過位元組跳動 HDFS 叢集的多機房架構演進之路。在發展的過程中,可以說踩了無數的坑,大的階段歸納起來會有這樣幾個階段。

第一階段

業務增長初期,叢集規模增長趨勢非常陡峭,單叢集規模很快在元資料伺服器 Name Node 側遇到瓶頸。引入聯邦機制(Federation)實現叢集的橫向擴充套件。

聯邦又帶來統一名稱空間問題,因此,需要統一檢視空間幫助業務構建統一接入。這裡我們引入了 Name Node Proxy 元件實現統一檢視和多租戶管理等功能。為了解決這個問題,我們引入了 Name Node Proxy 元件實現統一檢視和多租戶管理等功能,這部分會在下文的 NNProxy 章節中介紹。

第二階段

資料量繼續增大,Federation 方式下的目錄樹管理也存在瓶頸,主要體現在資料量增大後,Java 版本的 GC 變得更加頻繁,跨子樹遷移節點代價過大,節點啟動時間太長等問題。因此我們通過重構的方式,解決了 GC,鎖優化,啟動加速等問題,將原 Name Node 的服務能力進一步提高。容納更多的元資料資訊。為了解決這個問題,我們也實現了位元組跳動特色的 DanceNN 元件,相容了原有 Java 版本 NameNode 的全部功能基礎上,大大增強了穩定性和效能。相關詳細介紹會在下面的 DanceNN 章節中介紹。

第三階段

當資料量跨過 10EB,叢集規模擴大到十萬+臺的時候,慢節點問題,更細粒度服務分級問題,成本問題和元資料瓶頸進一步凸顯。我們在架構上也向著包括多租戶體系構建、重構資料節點和元資料分層等方向進一步演進。

這些演進涉及到非常多優化點,我們將在下文中給出詳細的慢節點優化落地實踐。

位元組跳動架構關鍵演進實踐

在整個架構演進的過程中,我們做了非常多的探索和嘗試。如上所述,結合之前提到的幾個大挑戰和問題,我們就其中關鍵的 Name Node ProxyDance Name Node 這兩個重點元件做一下介紹。同時,也會介紹一下我們在慢節點方面的優化和改進

NNProxy(Name Node Proxy)

作為系統的元資料操作接入端,NNProxy 提供了聯邦模式下統一元資料檢視,解決了使用者請求的統一轉發,業務流量的統一管控問題。

先介紹一下 NNProxy 所處的系統上下游。

系統訪問路徑圖

我們先來看一下 NNProxy 都做了什麼工作。

路由管理

在上面 Federation 的介紹中提到,每個叢集都維護自己獨立的目錄樹,無法對外提供一個完整的目錄樹檢視。NNProxy 中的路由管理就解決了這個問題。路由管理儲存了一張 mount table,表中記錄若干條路徑到叢集的對映關係。

例如 /user -> hdfs**://namenodeB**,這條對映關係的含義就是 /user 及其子目錄這個目錄在 namenodeB 這個叢集上,所有對 /user 及其子目錄的訪問都會由 NNProxy 轉發給 namenodeB,獲取結果後再返回給 Client。

匹配原則為最長匹配,例如我們還有另外一條對映 /user/tiger/dump-> hdfs**://namenodeC**,那麼 /user/tiger/dump 及其所有子目錄都在 namenodeC,而 /user 目錄下其他子目錄都在 namenodeB 上。如下圖所示:

子樹和名稱空間對應關係

Quota 限制

使用過 HDFS 的同學會知道 Quota 這個概念。我們給每個目錄集合分配了額定的空間資源,一旦使用超過這個閾值,就會被禁止寫入。這個工作就是由 NNProxy 完成的。NNProxy 會通過 Quota 實時監控系統獲取最新 Quota 使用情況,當用戶進行元資料操作的時候,NNProxy 就會根據使用者的 Quota 情況作出判斷,決定通過或者拒絕。

Trace 支援

通過位元組跳動自研的 Trace 系統,記錄追蹤使用者和系統以及系統之間的呼叫行為,以達到分析和運維的目的。其中的 Trace 資訊會附在向 NNProxy 的請求 RPC 中。NNProxy 拿到 Trace 系統以後就可以知道當前請求的上游模組,USER 及 Application ID 等資訊。NNProxy 一方面將這些資訊發到 Kafka 做一些離線分析,一方面實時聚合並打點,以便追溯線上流量。

流量限制

雖然 NNProxy 非常輕量,可以承受很高的 QPS,但是後端的 Name Node 承載能力是有限的。因此突發的大作業造成高 QPS 的讀寫請求被全量轉發到 Name Node 上時,會造成 Name Node 過載,延時變高,甚至出現 OOM,影響叢集上所有使用者。因此 NNProxy 另一個非常重要的任務就是限流,以保護後端 Name Node。

目前限流基於路徑+RPC 以及 使用者+RPC 維度。例如,我們可以限制 /user/tiger/warhouse 路徑的 create 請求為 100 QPS,或者某個使用者的 delete 請求為 5 QPS。一旦該使用者的訪問量超過這個閾值,NNProxy 會返回一個可重試異常,Client 收到這個異常後會重試。因此被限流的路徑或使用者會感覺到訪問 HDFS 變慢,但是並不會失敗。

Dance NN(Dance Name Node)

解決的問題

如前所述,在資料量上到 10EB 級別的場景後,原有的 Java 版本的 Name Node 存在了非常多的線上問題需要解決。以下是在實踐過程中我們遇到的一些問題總結:

  • Java 版本 Name Node 採用 Java 語言開發,在 INode 規模上億時,不可避免的會帶來嚴重的 GC 問題;

  • Java 版本 Name Node 將 INode meta 資訊完全放置於記憶體,10 億 INode 大約佔用 800GB 記憶體(包含 JVM 自身佔用的部分 native memory),更進一步加重了 GC;

  • 我們目前的叢集規模下,Name Node 從重啟到恢復服務需要 6 個小時,在主備同時發生故障的情況下,嚴重影響上層業務;

  • Java 版本 Name Node 全域性一把讀寫鎖,任何對目錄樹的修改操作都會阻塞其他的讀寫操作,併發度較低;

從上可以看出,在大資料量場景下,我們亟需一個新架構版本的 Name Node 來承載我們的海量元資料。除了 C++語言重寫來規避 Java 帶來的 GC 問題以外,我們還在一些場景下做了特殊的優化。

目錄樹鎖設計

HDFS 對內是一個分散式叢集,對外提供的是一個 unified 的檔案系統,因此對檔案及目錄的操作需要像操作 Linux 本地檔案系統一樣。這就要求 HDFS 滿足類似於資料庫系統中 ACID 特性一樣的原子性,一致性、隔離性和永續性。因此 DanceNN 在面對多個使用者同時操作同一個檔案或者同一個目錄時,需要保證不會破壞掉 ACID 屬性,需要對操作做鎖保護。

不同於傳統的 KV 儲存和資料庫表結構,DanceNN 上維護的是一棵樹狀的資料結構,因此單純的 key 鎖或者行鎖在 DanceNN 下不適用。而像資料庫的表鎖或者原生 NN 的做法,對整棵目錄樹加單獨一把鎖又會嚴重影響整體吞吐和延遲,因此 DanceNN 重新設計了樹狀鎖結構,做到保證 ACID 的情況下,讀吞吐能夠到 8W,寫吞吐能夠到 2W,是原生 NN 效能的 10 倍以上。

這裡,我們會重新對 RPC 做分類。

  • createFilegetFileInfosetXAttr 這類 RPC,依然是簡單的對某一個 INode 進行 CURD 操作;

  • delete RPC,有可能刪除一個檔案,也有可能會刪除目錄,後者會影響整棵子樹下的所有檔案;

  • rename RPC,則是更復雜的另外一類操作,可能會涉及到多個 INode,甚至是多棵子樹下的所有 INode。

DanceNN 啟動優化

由於我們的 DanceNN 底層元資料實現了本地目錄樹管理結構,因此我們 DanceNN 的啟動優化都是圍繞著這樣的設計來做的。

執行緒掃描和填充 BlockMap

在系統啟動過程中,第一步就是讀取目錄樹中儲存的資訊並且填入 BlockMap 中,類似 Java 版 NN 讀取 FSImage 的操作。在具體實現過程中,首先起多個執行緒並行掃描靜態目錄樹結構。將掃描的結果放入一個加鎖的 Buffer 中。當 Buffer 中的元素個數達到設定的數量以後,重新生成一個新的 Buffer 接收請求,並在老 Buffer 上起一個執行緒將資料填入 BlockMap。

接收塊上報優化

DanceNN 啟動以後會首先進入安全模式,接收所有 Date Node 的塊上報,完善 BlockMap 中儲存的資訊。當上報的 Date Node 達到一定比例以後,才會退出安全模式,這時候才能正式接收 client 的請求。所以接收塊上報的速度也會影響 Date Node 的啟動時長。DanceNN 這裡做了一個優化,根據 BlockID 將不同請求分配給不同的執行緒處理,每個執行緒負責固定的 Slice,執行緒之間無競爭,這樣就極大的加快了接收塊上報的速度。如下圖所示:

慢節點優化

慢節點問題在很多分散式系統中都存在。其產生的原因通常為上層業務的熱點或者底層資源故障。上層業務熱點,會導致一些資料在較短的時間段內被集中訪問。而底層資源故障,如出現慢盤或者盤損壞,更多的請求就會集中到某一個副本節點上從而導致慢節點。

通常來說,慢節點問題的優化和上層業務需求及底層資源量有很大的關係,極端情況,上層請求很小,下層資源充分富裕的情況下,慢節點問題將會非常少,反之則會變得非常嚴重。在位元組跳動的 HDFS 叢集中,慢節點問題一度非常嚴重,尤其是磁碟佔用百分比非常高以後,各種慢節點問題層出不窮。其根本原因就是資源的平衡滯後,許多機器的磁碟佔用已經觸及紅線導致寫降級;新增熱資源則會集中到少量機器上,這種情況下,當上層業務的每秒請求數升高後,對於 P999 時延要求比較高的一些大資料分析查詢業務就容易出現一大批資料訪問(>10000 請求)被卡在某個慢請求的處理上。

我們優化的方向會分為讀慢節點和寫慢節點兩個方面。

讀慢節點優化

我們經歷了幾個階段:

  • 最早,使用社群版本,其 Switch Read 以讀取一個 packet 的時長為統計單位,當讀取一個 packet 的時間超過閾值時,認為讀取當前 packet 超時。如果一定時間視窗內超時 packet 的數量過多,則認為當前節點是慢節點。但這個問題在於以 packet 作為統計單位使得演算法不夠敏感,這樣使得每次讀慢節點發生的時候,對於小 IO 場景(位元組跳動的一些業務是以大量隨機小 IO 為典型使用場景的),這些個積攢的 Packet 已經造成了問題。

  • 後續,我們研發了 Hedged Read 的讀優化。Hedged Read 對每一次讀取設定一個超時時間。如果讀取超時,那麼會另開一個執行緒,在新的執行緒中向第二個副本發起讀請求,最後取第一第二個副本上優先返回的 response 作為讀取的結果。但這種情況下,在慢節點集中發生的時候,會導致讀流量放大。嚴重的時候甚至導致小範圍頻寬短時間內不可用。

  • 基於之前的經驗,我們進一步優化,開啟了 Fast Switch Read 的優化,該優化方式使用吞吐量作為判斷慢節點的標準,當一段時間視窗內的吞吐量小於閾值時,認為當前節點是慢節點。並且根據當前的讀取狀況動態地調整閾值,動態改變時間視窗的長度以及吞吐量閾值的大小。下表是當時線上某業務測試的值:

進一步的相關測試資料:

寫慢節點優化

寫慢節點優化的適用場景會相對簡單一些。主要解決的是寫過程中,Pipeline 的中間節點變慢的情況。為了解決這個問題,我們也發展了 Fast Failover 和 Fast Failover+兩種演算法。

Fast Failover

Fast Failover 會維護一段時間內 ACK 時間過長的 packet 數目,當超時 ACK 的數量超過閾值後,會結束當前的 block,向 namenode 申請新塊繼續寫入。

Fast Failover 的問題在於,隨意結束當前的 block 會造成系統的小 block 數目增加,給之後的讀取速度以及 namenode 的元資料維護都帶來負面影響。所以 Fast Failover 維護了一個切換閾值,如果已寫入的資料量(block 的大小)大於這個閾值,才會進行 block 切換。

但是往往為了達到這個寫入資料大小閾值,就會造成使用者難以接收的延遲,因此當資料量小於閾時需要進額外的優化。

Fast Failover +

為了解決上述的問題,當已寫入的資料量(block 的大小)小於閾值時,我們引入了新的優化手段——Fast Failover+。該演算法首先從 pipeline 中篩選出速度較慢的 datanode,將慢節點從當前 pipeline 中剔除,並進入 Pipeline Recovery 階段。Pipeline Recovery 會向 namenode 申請一個新的 datanode,與剩下的 datanode 組成一個新的 pipeline,並將已寫入的資料同步到新的 datanode 上(該步驟稱為 transfer block)。由於已經寫入的資料量較小,transfer block 的耗時並不高。統計 p999 平均耗時只有 150ms。由 Pipeline Recovery 所帶來的額外消耗是可接受的。

下表是當時線上某業務測試的值:

一些進一步的實際效果對比:

總結

HDFS 在位元組跳動的發展歷程已經非常長了。從最初的幾百臺的叢集規模支援 PB 級別的資料量,到現在十萬臺級別多叢集的平臺支援 10 EB 級別的資料量,我們經歷了 9 年的發展。伴隨著業務的快速上量,我們團隊也經歷了野蠻式爆發,規模化發展,平臺化運營的階段。這過程中我們踩了不少坑,也積累了相當豐富的經驗。我們將在技術上的探索和業務上的積累結合,打造出了檔案儲存產品“大資料檔案儲存”並上線了火山引擎,目前火山引擎大資料檔案儲存正在免費公測中!

免費公測!火山引擎大資料檔案儲存

大資料檔案儲存是面向大資料和機器學習生態的統一檔案儲存。支援對接多雲物件儲存,並提供統一資料管理和資料快取加速服務,具備低成本、高可靠、高可用等特性。加速資料處理、資料湖分析、機器學習等場景下海量資料的儲存訪問速度。採用雲中立模式,支援公有云混合雲多雲部署,全面貼合企業上雲策略,歡迎申請公測

掃碼瞭解更全產品資訊

 

相關文章推薦:

位元組跳動10萬節點HDFS叢集多機房架構演進之路

免費公測|火山引擎大資料檔案儲存公測現已開啟!