Uber Hadoop 文件系統最佳實踐 頂 原 薦

How Uber implemented these improvements to facilitate the continued growth, stability, and reliability of our storage system.

三年前, Uber 工程團隊引入 Hadoop 作爲大數據分析的存儲 (HDFS) 和計算 (YARN) 基礎設施。

Uber 使用 Hadoop 進行批量和流式分析, 廣泛應用於包括欺詐檢測( fraud detection)、機器學習(machine learning)和 ETA 計算(Estimated Time of Arrival)等領域。在過去的幾年裏, Uber 的業務發展迅猛,數據量和相關的訪問負載呈指數級增長 ; 僅在 2017年, 存儲在 HDFS 上的數據量就增長了400% 以上。

在擴展基礎設施的同時保持高性能可不是一件輕鬆的事。爲了實現這一目標,Uber 數據架構團隊通過實施若干新的調整和功能來擴展 HDFS , 包括可視化文件系統(View File System ,ViewFs)、頻繁的 HDFS 版本升級、NameNode 垃圾回收調整, 限制通過系統篩選小文件的數量、HDFS 負載管理服務和只讀 NameNode 副本。下面將詳細介紹如何執行這些改進以促進存儲系統的持續增長、穩定性和可靠性。

Challenges

HDFS 被設計爲可伸縮的分佈式文件系統, 單個羣集支持上千個節點。只要有足夠的硬件, 在一個集羣中可以輕鬆、快速地擴展實現超過 100 pb 的原始存儲容量。

然而對於 Uber 而言, 業務迅速增長使其難以可靠地進行擴展同時而不減慢數據分析的速度。成千上萬的用戶每週都要執行數以百萬計的查詢(通過 Hive 或 Presto )。

目前, HDFS 超過一半以上的訪問源於 Presto, 並且 90% 的 Presto 查詢需要 100 秒以上的時間來處理。如果我們的 HDFS 基礎結構超載, 那麼在隊列中的查詢就會堆積起來, 從而導致查詢延遲。更爲重要的是,對於每個查詢而言,我們需要在 HDFS 上儘快地提供數據。

針對原來的存儲基礎架構, 我們設計了提取(extract)、轉換(transform)和加載 (ETL) 機制以便在用戶運行查詢時減少同一集羣中發生的複製延遲。這些羣集由於具有雙重職責,因而需要生成小文件以適應頻繁的寫入和更新, 這反而進一步堵塞了隊列。

在我們面臨的挑戰中,首要任務是多個團隊需要大量的存儲數據, 這就決定了不能採用按照用例或組織進行集羣分割的方案, 那樣反過來會降低效率的同時增加成本。

造成減速的根源 — 在不影響用戶體驗的情況下擴展 HDFS 的主要瓶頸是 NameNode 的性能和吞吐量, 它包括系統中所有文件的目錄樹, 用於跟蹤保存數據文件的位置。由於所有元數據都存儲在 NameNode 中, 因此客戶端對 HDFS 羣集的請求必須首先通過它。更復雜的是, NameNode 命名空間上的ReadWriteLock 限制了 NameNode 可以支持的最大吞吐量, 因爲任何寫入請求都將被獨佔寫鎖定, 並強制任何其他請求都在隊列中等待。

2016 年晚些時候, 我們開始發現 NameNode RPC 隊列時間高的問題。有時, NameNode 隊列時間可能超過每個請求 500毫秒 (最慢的隊列時間達到接近一秒), 這意味着每一個 HDFS 請求在隊列中至少等待半秒 -- 與我們的正常進程時間(10 毫秒以下)相比, 這是明顯的減速。

  • Figure 1. In 2016, our NameNode RPC queue time could exceed half a second per HDFS request.

Enabling scaling & improving performance

爲了確保 HDFS 高性能運行的同時持續擴展, Uber 並行開發多個解決方案, 以避免在短期內出現停機。這些解決方案使我們建立了一個更可靠和可擴展的系統, 能夠支持未來的長期增長。

改進方案概述如下:

Scaling out using ViewFs

Twitter 嘗試過類似努力,在他們的啓發下, 我們利用可視化文件系統 (ViewFs) 將 HDFS 拆分爲多個物理命名空間, 並使用 ViewFs 掛載點向用戶呈現一個虛擬命名空間。

爲了完成這一目標, 我們將 HBase(YARN 和 Presto 操作)從相同的 HDFS 集羣分開。該調整不僅大大減少了主集羣上的負載, 而且使我們的 HBase 更加穩定, 將 HBase 集羣的重啓時間從幾小時減少到幾分鐘。

我們還爲聚合 YARN 應用日誌創建了一個專用的 HDFS 羣集。要使日誌聚合支持 ViewFs, 需要 YARN-3269。我們的 Hive 臨時目錄也被移動到這個羣集。增加集羣的結果是非常令人滿意的 ; 目前, 新羣集的服務總寫入請求數約佔總數的 40%, 而且大多數文件都是小文件, 這也減輕了主羣集上的文件計數壓力。由於對現有應用程序而言,不需要更改客戶端, 因此改轉換非常順利。

最後, 我們在 ViewFs 後端實現了獨立的的 HDFS 羣集, 而不是基礎架構中的 HDFS Federation 。通過這種設置, 可以逐步執行 HDFS 升級, 最大限度地減少大規模停機的風險; 此外, 完全隔離還有助於提高系統的可靠性。然而, 這種修復方案的一個缺點是, 保持單獨的 HDFS 羣集會導致更高的運營成本。

  • Figure 2. We installed ViewFs in multiple data centers to help manage our HDFS namespaces.

HDFS upgrades

第二個解決方案是升級 HDFS 以跟上最新版本。我們一年執行了兩次主要升級, 首先從 CDH 5.7.2 ( 包含大量 HDFS 2.6.0 補丁) 升級到 Apache 2.7.3, 然後升級到 Apache 2.8.2。爲此, 我們還必須重構基於 Puppet 和 Jenkins 之上的部署框架, 以更換第三方羣集管理工具。

版本升級帶來了關鍵的可伸縮性改進, 包括 HDFS-9710、HDFS-9198 和 HDFS-9412。例如, 升級到 Apache 2.7.3 後, 增量塊報告(incremental block report)的數量明顯減少, 從而減輕了 NameNode 的負載。

升級 HDFS 可能會有風險, 因爲它可能會導致停機、性能下降或數據丟失。爲了解決這些可能的問題, 我們花了幾個月的時間來驗證 Apache 2.8.2 之後纔將其部署到生產環境中。但是, 在升級最大的生產集羣時, 仍然有一個 Bug (HDFS-12800) 讓我們措手不及。儘管 Bug 引起的問題很晚才發現, 但是憑藉獨立羣集、分階段升級過程(a staged upgrade process)和應急回滾計劃(contingency rollback plans),最後給我們的影響非常有限。

事實證明,在同一臺服務器上運行不同版本的 YARN 和 HDFS 的能力對於我們實現擴展至關重要。由於 YARN 和 HDFS 都是 Hadoop 的一部分, 它們通常一起升級。然而, YARN 主線版本的升級需要更長時間的充分驗證之後纔會推出, 一些生產應用的 YARN 可能需要更新,由於 YARN API 的變化或 YARN 和這些應用的 JAR 依賴衝突。雖然 YARN 的可伸縮性在我們的環境中不是一個問題, 但我們不希望關鍵的 HDFS 升級被 YARN 升級阻塞。爲了防止可能的堵塞, 我們目前運行的 YARN 比 HDFS 的版本更早, 在我們的場景很有效。(但是, 當採用諸如 Erasure Coding 之類的功能時, 由於需要更改客戶端, 此策略可能不起作用。)

NameNode Garbage collection

垃圾回收 (Garbage collection , GC) 調優在整個優化方案中也發揮了重要作用。它在擴展存儲基礎架構的同時,給我們創造了必要的喘息空間。

通過強制使用併發標記掃描收集器 (Concurrent Mark Sweep collectors ,CMS) 防止長時間 GC 暫停, 通過調整 CMS 參數 (如 CMSInitiatingOccupancyFraction、UseCMSInitiatingOccupancyOnly 和 CMSParallelRemarkEnabled ) 來執行更具侵略性的老年代集合(注:CMS 是分代的,新生代和老年代都會發生回收。CMS 嘗試通過多線程併發的方式來跟蹤對象的可達性,以便減少老生代的收集時間)。雖然會增加 CPU 利用率, 但幸運的是我們有足夠的空閒 CPU 來支持此功能。

由於繁重的 RPC 負載, 在新生代中創建了大量短期的對象, 迫使新生代收集器頻繁地執行垃圾回收暫停(stop-the-world)。通過將新生代的規模從 1.5GB 增加到 16GB , 並調整 ParGCCardsPerStrideChunk 值 (設置爲 32768), 生產環境中 NameNode 在 GC 暫停時所花費的總時間從 13% 減少到 1.7% , 吞吐量增加了 10% 以上。

與 GC 相關的 JVM 參數( NameNode 堆大小 160GB ), 供參考:

XX:+UnlockDiagnosticVMOptions
XX:ParGCCardsPerStrideChunk=32768 -XX:+UseParNewGC
XX:+UseConcMarkSweepGC -XX:+CMSConcurrentMTEnabled
XX:CMSInitiatingOccupancyFraction=40
XX:+UseCMSInitiatingOccupancyOnly
XX:+CMSParallelRemarkEnabled -XX:+UseCondCardMark
XX:+DisableExplicitGC

Uber 還在評估是否將第一垃圾回收器 (Garbage-First Garbage Collector , G1GC) 集成在系統中。雖然在過去使用 G1GC 時沒有看到優勢, 但 JVM 的新版本帶來了額外的垃圾回收器性能改進, 因此重新審視收集器和配置的選擇有時是必要的。

  • Figure 3. By increasing the young generation size from 1.5GB to 16GB and tuning the ParGCCardsPerStrideChunk value, the total time our production NameNode spent on GC pause decreased from 13 percent to 1.7 percent.

Controlling the number of small files

由於 NameNode 將所有文件元數據加載到內存中, 小文件增長會增加 NameNode 的內存壓力。此外, 小文件會導致讀取 RPC 調用增加, 以便在客戶端讀取文件時訪問相同數量的數據, 以及在生成文件時增加 RPC 調用。爲了減少存儲中小文件的數量, Uber 主要採取了兩種方法:

首先,Uber Hadoop 數據平臺團隊基於 Hoodie 庫建立了新的攝取管道, 生成比原始數據管道創建的更大的文件。不過, 作爲一個臨時解決方案, 在這些可用之前, 我們還建立了一個工具 (稱爲 stitcher "訂書機"), 將小文件合併成較大的文件(通常大於 1GB )。

其次, 在 Hive 數據庫和應用程序目錄上設置了嚴格的命名空間配額。爲了貫徹這一目標, 我們爲用戶創建了一個自助服務工具, 用於管理其組織內的配額。配額的分配比例爲每文件 256MB, 以鼓勵用戶優化其輸出文件大小。Hadoop 團隊還提供優化指南和文件合併工具以幫助用戶採用最佳實踐。例如, 在 Hive 上啓用自動合併(auto-merge)和調整減速器數量(the number of reducers )可以大大減少由 Hive insert-overwrite 查詢生成的文件數。

HDFS load management service

運行大型多租戶基礎架構 (如 HDFS ) 的最大挑戰之一是檢測哪些應用程序導致異常大的負載、如何快速採取措施來修復它們。爲了實現這一目的,Uber 構建了內置 HDFS 的負載管理服務, 稱爲 Spotlight 。

在目前的 Spotlight 實現中, 審計日誌從活躍的 NameNode 以流的形式送到一個基於 Flink 和 Kafka 的後端實時處理。最後,日誌分析結果通過儀表板輸出, 並用於自動化處理(例如自動禁用帳戶或殺死導致 HDFS 減速的工作流)。

  • Figure 4. Spotlight enables us to identify and disable accounts that are causing HDFS slowdown.

New Feature : Observer NameNode

Uber 正在開發一個新的 HDFS 功能 Observer NameNode (HDFS-12975) 。 Observer NameNode 設計爲一個 NameNode 只讀副本, 目的是減少在活躍的 NameNode 羣集上加載。由於 HDFS RPC 容量和增長的一半以上來自只讀的 Presto 查詢, Uber 希望藉助 Observer NameNodes 的幫助將總體 NameNode 吞吐量擴展到 100% 。Uber 已經完成了這個工具的驗證, 並正在將其投入生產環境中。

  • Figure 5. Uber Engineering’s current HDFS architecture incorporates high availability and Observer NameNodes.

最佳實踐

  • Layer your solutions: 考慮不同層次的解決方案。實現像 Observer NameNode 那樣的工具或將 HDFS 切分到多集羣需要付出巨大的努力。短期措施, 如 GC 調整和通過 stitcher 合併較小的文件, 給了我們很多喘息的空間以開發完善長期的解決方案。
  • Bigger is better: 因爲小文件對 HDFS 的威脅, 所以最好及早解決它們, 而不是延後。主動向用戶提供工具、文檔和培訓是幫助實施最佳實踐非常有效的方法。
  • Participate in the community: Hadoop 已經存在超過 10 年了, 其社區比以往任何時候都更加活躍, 幾乎每個版本中都引入了可伸縮性和功能改進。通過貢獻您自己的發現和工具來參與 Hadoop 社區對於你持續擴展基礎架構非常重要。

未來

在不久的將來, Uber 計劃將各種新服務集成到存儲系統(如 圖6 所示)。

  • Figure 6. Our near-future HDFS architecture will incorporate several additional features and functionalities that will contribute to the growth of our storage
infrastructure.

接下來重點介紹兩個主要項目, 基於路由的 HFDS Federation 和 tiered storage :

Router-based HDFS Federation

Uber 目前使用 ViewFs 擴展 HDFS (當 subclusters 超載時)。此方法的主要問題是, 每次在 ViewFs 上添加或替換新的掛載點時, 都需要更改客戶端配置, 而且很難在不影響生產工作流的情況下進行。這種困境是我們目前只拆分不需要大規模更改客戶端數據的主要原因之一, 例如 YARN 日誌聚合。

Microsoft 的新倡議—基於路由的 HFDS Federation (HDFS-10467, HDFS-12615),目前包含在 HDFS 2.9 版本中, 是一個基於 ViewFs 的分區聯盟的擴展。該聯盟添加了一層軟件集中管理 HDFS 命名空間。通過提供相同的接口 (RPC 和 WebHDFS 的組合), 它的外層爲用戶提供了對任何 subclusters 的透明訪問, 並讓 subclusters 獨立地管理其數據。

通過提供再平衡工具( a rebalancing tool ), 聯盟層( the federation layer )還將支持跨 subclusters 的透明數據移動, 用於平衡工作負載和實現分層存儲。聯盟層集中式維護狀態存儲區中全局命名空間的狀態, 並允許多個活躍的路由器將用戶請求定向到正確的 subclusters 時啓動和運行。

Uber 正在積極地與 Hadoop 社區密切協作,致力於將基於路由的 HDFS Federation 引入到生產環境, 並進一步開源改進, 包括支持 WebHDFS 。

Tiered Storage

隨着基礎架構的規模增長, 降低存儲成本的重要性也同樣重要。Uber 技術團隊中進行的研究表明, 相較舊數據 (warm data) 用戶會更頻繁地訪問最近的數據 (hot data)。將舊數據移動到一個單獨的、佔用較少資源的層將大大降低我們的存儲成本。HDFS Erasure Coding 、Router-based Federation、高密度 (250TB 以上) 硬件和數據移動服務 (在 "熱" 層羣集和 "暖" 層羣集之間處理移動數據) 是即將進行的分層存儲設計的關鍵組件。Uber 計劃在以後的文章中分享在分層存儲實現方面的經驗。

Apache Hadoop ABC

$ hadoop version
Hadoop 3.1.0
Source code repository https://github.com/apache/hadoop -r 16b70619a24cdcf5d3b0fcf4b58ca77238ccbe6d
Compiled by centos on 2018-03-30T00:00Z
Compiled with protoc 2.5.0
From source with checksum 14182d20c972b3e2105580a1ad6990
This command was run using /usr/local/Cellar/hadoop/3.1.0/libexec/share/hadoop/common/hadoop-common-3.1.0.jar

# 常見異常:檢查 JDK 版本是否過低
$ hadoop version
Exception in thread "main" java.lang.UnsupportedClassVersionError: org/apache/hadoop/util/VersionInfo : Unsupported major.minor version 52.0
	at java.lang.ClassLoader.defineClass1(Native Method)

Java Garbage Collection Types

  • Serial GC (-XX:+UseSerialGC): Serial GC uses the simple mark-sweep-compact approach for young and old generations garbage collection i.e Minor and Major GC. Serial GC is useful in client-machines such as our simple stand alone applications and machines with smaller CPU. It is good for small applications with low memory footprint.

  • Parallel GC (-XX:+UseParallelGC): Parallel GC is same as Serial GC except that is spawns N threads for young generation garbage collection where N is the number of CPU cores in the system. We can control the number of threads using -XX:ParallelGCThreads=n JVM option. Parallel Garbage Collector is also called throughput collector because it uses multiple CPUs to speed up the GC performance. Parallel GC uses single thread for Old Generation garbage collection.

  • Parallel Old GC (-XX:+UseParallelOldGC): This is same as Parallel GC except that it uses multiple threads for both Young Generation and Old Generation garbage collection. Concurrent Mark Sweep (CMS) Collector (-XX:+UseConcMarkSweepGC): CMS Collector is also referred as concurrent low pause collector. It does the garbage collection for Old generation. CMS collector tries to minimize the pauses due to garbage collection by doing most of the garbage collection work concurrently with the application threads. CMS collector on young generation uses the same algorithm as that of the parallel collector. This garbage collector is suitable for responsive applications where we can’t afford longer pause times. We can limit the number of threads in CMS collector using -XX:ParallelCMSThreads=n JVM option.

  • G1 Garbage Collector (-XX:+UseG1GC): The Garbage First or G1 garbage collector is available from Java 7 and it’s long term goal is to replace the CMS collector. The G1 collector is a parallel, concurrent, and incrementally compacting low-pause garbage collector. Garbage First Collector doesn’t work like other collectors and there is no concept of Young and Old generation space. It divides the heap space into multiple equal-sized heap regions. When a garbage collection is invoked, it first collects the region with lesser live data, hence “Garbage First”. You can find more details about it at Garbage-First Collector Oracle Documentation.

擴展閱讀:電子書《Linux Perf Master》

擴展閱讀:開源架構技術漫談

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章