Categories
程式開發

字節跳動 EB 級 HDFS 實踐


HDFS 簡介

因為 HDFS 這樣一個系統已經存在了非常長的時間,應用的場景已經非常成熟了,所以這部分我們會比較簡單地介紹。

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

  • 和本地文件系統一樣的目錄樹視圖
  • Append Only 的寫入(不支持隨機寫)
  • 順序和隨機讀
  • 超大數據規模
  • 易擴展,容錯率高

字節跳動特色的 HDFS

字節跳動應用 HDFS 已經非常長的時間了,經歷了 7 年的發展,目前已直接支持了十多種數據平台,間接支持了上百種業務發展。從集群規模和數據量來說,HDFS 平台在公司內部已經成長為總數幾萬台服務器的大平台,支持了 EB 級別的數據量。

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

架構介紹

字節跳動 EB 級 HDFS 實踐 1

接入層

接入層是區別於社區版本最大的一層,社區版本中並無這一層定義。在字節跳動的落地實踐中,由於集群的節點過於龐大,我們需要非常多的 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 中的每個集群都是獨立完成的。在心跳匯報的返回結果中,會攜帶 N​​ame Node 對 Data Node 下發的指令,例如,需要將某個副本拷貝到另外一台 Data Node 或者將某個副本刪除等。

主要業務

先來看一下當前在字節跳動 HDFS 承載的主要業務:

  • Hive,HBase,日誌服務,Kafka 數據存儲
  • Yarn,Flink 的計算框架平台數據
  • Spark,MapReduce 的計算相關數據存儲

發展階段

在字節跳動,隨著業務的快速發展,HDFS 的數據量和集群規模快速擴大,原來的 HDFS 的集群從幾百台,迅速突破千台和萬台的規模。這中間,踩了無數的坑,大的階段歸納起來會有這樣幾個階段。

第一階段

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

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

第二階段

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

第三階段

當數據量跨過 EB,集群規模擴大到幾萬台的時候,慢節點問題,更細粒度服務分級問題,成本問題和元數據瓶頸進一步凸顯。我們在架構上進一步在包括完善多租戶體系構建,重構數據節點和元數據分層等方向進一步演進。這部分目前正在進行中,因為優化的點會非常多,本文會給出慢節點優化的落地實踐。

關鍵改進

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

NNProxy(Name Node Proxy)

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

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

字節跳動 EB 級 HDFS 實踐 2

我們先來看一下 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 上。如下圖所示:

字節跳動 EB 級 HDFS 實踐 3

Quota 限制

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

Trace 支持

ByteTrace 是一個 Trace 系統,記錄追踪用戶和系統以及系統之間的調用行為,以達到分析和運維的目的。其中的 Trace 信息會附在向 NNProxy 的請求 RPC 中。 NNProxy 拿到 ByteTrace 以後就可以知道當前請求的上游模塊,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)

解決的問題

如前所述,在數據量上到 EB 級別的場景後,原有的 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,線程之間無競爭,這樣就極大的加快了接收塊上報的速度。如下圖所示:

字節跳動 EB 級 HDFS 實踐 4

慢節點優化

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

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

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

讀慢節點優化

我們經歷了幾個階段:

  • 最早,使用社區版本,其 Switch Read 以讀取一個 packet 的時長為統計單位,當讀取一個 packet 的時間超過閾值時,認為讀取當前 packet 超時。如果一定時間窗口內超時 packet 的數量過多,則認為當前節點是慢節點。但這個問題在於以packet 作為統計單位使得算法不夠敏感,這樣使得每次讀慢節點發生的時候,對於小IO 場景(字節跳動的一些業務是以大量隨機小IO 為典型使用場景的),這些個積攢的Packet 已經造成了問題。
  • 後續,我們研發了 Hedged Read 的讀優化。 Hedged Read 對每一次讀取設置一個超時時間。如果讀取超時,那麼會另開一個線程,在新的線程中向第二個副本發起讀請求,最後取第一第二個副本上優先返回的 response 作為讀取的結果。但這種情況下,在慢節點集中發生的時候,會導致讀流量放大。嚴重的時候甚至導緻小範圍帶寬短時間內不可用。
  • 基於之前的經驗,我們進一步優化,開啟了 Fast Switch Read 的優化,該優化方式使用吞吐量作為判斷慢節點的標準,當一段時間窗口內的吞吐量小於閾值時,認為當前節點是慢節點。並且根據當前的讀取狀況動態地調整閾值,動態改變時間窗口的長度以及吞吐量閾值的大小。下表是當時線上某業務測試的值:

字節跳動 EB 級 HDFS 實踐 5

進一步的相關測試數據:

字節跳動 EB 級 HDFS 實踐 6

寫慢節點優化

寫慢節點優化的適用場景會相對簡單一些。主要解決的是寫過程中,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 所帶來的額外消耗是可接受的。

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

字節跳動 EB 級 HDFS 實踐 7

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

字節跳動 EB 級 HDFS 實踐 8

結尾

HDFS 在字節跳動的發展歷程已經非常長了。從最初的幾百台的集群規模支持 PB 級別的數據量,到現在幾萬台級別多集群的平台支持 EB 級別的數據量,我們經歷了 7 年的發展。伴隨著業務的快速上量,我們團隊也經歷了野蠻式爆發,規模化發展,平台化運營的階段。這過程中我們踩了不少坑,也積累了相當豐富的經驗。當然,最重要的,公司還在持續高速發展,而我們仍舊不忘初心,堅持“DAY ONE”,繼續在路上。

本文轉載自公眾號字節跳動技術團隊(ID:toutiaotechblog)。

原文鏈接

https://mp.weixin.qq.com/s/liiplasFnFW0ezc3VWsOHA