Uber 容器化 Apache Hadoop 基礎設施的實踐

語言: CN / TW / HK

隨著 Uber 的業務持續增長,我們用了 5 年時間擴充套件 Apache Hadoop(本文中稱為“Hadoop”),部署到了 21000 多臺主機上,以支援多種分析和機器學習用例。我們組建了一支擁有多樣化專業知識的團隊來應對在裸金屬伺服器上執行 Hadoop 所面臨的各種挑戰,這些挑戰包括:主機生命週期管理、部署和自動化,Hadoop 核心開發以及面向客戶的門戶。

隨著 Hadoop 基礎設施的複雜性和規模越來越大,團隊越來越難以應對管理如此龐大系統時需要承擔的各種職責。基於指令碼和工具鏈的全系統規模運營工作消耗了大量的工程時間。損壞的主機越來越多,修復工作逐漸跟不上節奏。

在我們繼續為 Hadoop 維護自己的裸金屬部署時,公司的其他部門在微服務領域取得了重大進展。容器編排、主機生命週期管理、服務網格和安全性等領域的解決方案逐漸成型,讓微服務管理起來更加高效和簡便。

2019 年,我們開始了重構 Hadoop 部署技術棧的旅程。兩年時間過去了,如今有超過 60%的 Hadoop 執行在 Docker 容器中,為團隊帶來了顯著的運營優勢。作為這一計劃的成果之一,團隊將他們的許多職責移交給了其他基礎設施團隊,並且得以更專注於核心 Hadoop 的開發工作。

圖 1:團隊職責轉移

本文總結了這一過程中我們面臨的各種問題,以及我們解決這些問題的方法和途徑。

回顧過去

在具體分析架構之前,有必要簡要介紹一下我們之前運維 Hadoop 的方法及其缺陷。彼時,幾個互相分離的解決方案協同工作,驅動 Hadoop 的裸金屬部署。這些方案包括:

  • 讓主機設定就地突變的自動化方案

  • 手動觸發和監控,基於非冪等動作的指令碼

  • 鬆散耦合的主機生命週期管理解決方案

在底層,它們是通過幾個 Golang 服務、大量 Python 和 Bash 指令碼、Puppet 清單和一些 Scala 程式碼實現的。早期我們使用了 Cloudera Manager(免費版)並評估了 Apache Ambari。然而,由於 Uber 使用了自定義部署模型,這兩個系統都被證明是不夠用的。

我們舊的運維方法遇到了一些挑戰,包括但不僅限於以下方面:

  • 生產主機的手動就地突變導致了許多漂移,後來這讓我們感到很驚訝。工程師經常就部署過程發生爭論,因為在事件響應期間某些更改沒有經過審查和識別。

  • 全系統範圍的更改需要漫長的時間來做手動計劃和編排。我們上次的作業系統升級被推遲了,最終花了 2 年多的時間才完成。

  • 幾個月後,管理不善的配置導致了諸多事故。我們錯誤地配置了 dfs.blocksize,最終導致我們的一個叢集中的 HDFS RPC 佇列時間降級。

  • 自動化與人類互動之間缺乏良好的契約,這會導致一些意想不到的嚴重後果。由於一些主機意外退役,我們丟失了一些副本。

  • “寵物” 主機 的存在和越來越多的“寵物”所需的人工處理過程導致了一些影響嚴重的事件。我們的一次 HDFS NameNode 遷移引發了一起影響整個批處理分析棧的事件。

我們在重構期間仔細考慮了從以前的經驗中吸取的各種教訓。

架構

開始設計新系統時,我們遵循以下原則:

  • 對 Hadoop 核心的更改應該保持在最低水平,以免同開原始碼分道揚鑣(例如用於安全性的 Kerberos)

  • Hadoop 守護程序必須容器化,以實現不可變和可重複的部署

  • 必須使用宣告性概念(而不是基於動作的命令式模型)來建模叢集運維

  • 叢集中的任何主機都必須在發生故障或降級時易於更換

  • 應儘可能重用和利用 Uber 內部的基礎設施,以避免重複

以下部分詳細介紹了定義新架構的一些關鍵解決方案。

叢集管理

我們之前使用命令式、基於動作的指令碼來配置主機和運維叢集的方法已經到了無以為繼的地步。鑑於負載的有狀態(HDFS)和批處理(YARN)性質,以及部署運維所需的自定義部分,我們決定將 Hadoop 納入 Uber 的內部有狀態叢集管理系統。

叢集管理系統通過幾個基於通用框架和庫構建的鬆散耦合元件來運維 Hadoop。下圖代表了我們今天所用的架構的簡化版本。黃色元件描述了核心叢集管理系統,而綠色標記的元件代表了專門為 Hadoop 構建的自定義元件。

圖 2:叢集管理架構

叢集管理員(Cluster Admin)與叢集管理器介面(Cluster Manager Interface)Web 控制檯互動,來觸發對叢集的運維操作。管理員的意圖被傳播到叢集管理器(Cluster Manager)服務,然後觸發突變叢集目標狀態(Goal State)的 Cadence 工作流。

叢集管理(Cluster Management)系統維護預配置的主機,稱為託管主機(Managed Hosts)。一個節點(Node)代表一組部署在一個託管主機上的 Docker 容器。目標狀態定義了叢集的整個拓撲結構,包括節點位置資訊(主機位置)、叢集到節點的歸屬、節點資源(CPU、記憶體、磁碟)的定義及其環境變數。一個持久資料儲存負責儲存目標狀態,使叢集管理系統可以從非常嚴重的故障中快速恢復。

我們非常依賴 Uber 開發的開源解決方案 Cadence 來編排叢集上的狀態變化。Cadence 工作流負責所有運維操作,諸如新增或停用節點、升級整個佇列中的容器等等。Hadoop 管理器(Hadoop Manager)元件定義了所有工作流。

叢集管理器(Cluster Manager)不瞭解 Hadoop 的內部運維操作以及管理 Hadoop 基礎設施的複雜性。Hadoop 管理器實現了自定義邏輯(類似於 K8s Custom Operator ),以在 Hadoop 的運維範圍內以安全的方式管理 Hadoop 叢集和模型工作流。例如,我們所有的 HDFS 叢集都有兩個 NameNode;而 Hadoop 管理器元件內有一些相關的防護措施,例如禁止同時重啟這兩個 NameNode。

Hadoop Worker 是在分配給 Hadoop 的每個節點上啟動的第一個代理(agent)。系統中的所有節點都在 SPIRE 註冊,SPIRE 是一個開源身份管理和負載證明系統。Hadoop Worker 元件在容器啟動時使用 SPIRE 進行身份驗證,並接收一個 SVID (X.509 證書)。Hadoop Worker 使用它與其他服務通訊,以獲取其他配置和金鑰(例如 Kerberos 金鑰表)。

Hadoop 容器(Hadoop Container)代表在 Docker 容器中執行的任何 Hadoop 元件。在我們的架構中,所有 Hadoop 元件(HDFS NameNode、HDFS DataNode 等)都部署為 Docker 容器。

Hadoop Worker 定期從叢集管理器中獲取節點的目標狀態,並在節點本地執行各種動作以實現目標狀態(這是一個控制迴圈,也是 K8s 的核心概念)。該狀態定義要啟動、停止或停用的 Hadoop 容器以及其他設定。在執行 HDFS NameNode 和 YARN ResourceManager 的節點上,Hadoop Worker 負責更新“主機檔案”(例如 dfs.hosts和dfs.hosts.exclude )。這些檔案指示需要包含在叢集中或從叢集中排除的 DataNodes/NodeManager 主機。Hadoop Worker 還負責將節點的實際(Actual)狀態(或當前狀態)回報給叢集管理器。叢集管理器在啟動新的 Cadence 工作流時,根據實際狀態和目標狀態將叢集收斂到定義的目標狀態。

一個與叢集管理器良好整合的系統負責持續檢測主機問題。叢集管理器能做出很多智慧決策,例如限制速率以避免同時停用過多的損壞主機。Hadoop 管理器在採取任何行動之前會確保叢集在不同的系統變數下都是健康的。Hadoop 管理器中包含的檢查可確保叢集中不存在丟失或複製不足的塊,並且在執行關鍵運維操作之前確保資料在 DataNode 之間保持均衡,並執行其他必要檢查。

使用宣告式運維模型(使用目標狀態)後,我們減少了運維叢集時的人工操作。一個很好的例子是系統可以自動檢測到損壞主機並將其安全地從叢集中停用以待修復。每退役一臺損壞主機,系統都會補充一個新主機來保持叢集容量不變(維持目標狀態中所定義的容量)。

下圖顯示了由於各種問題在一週時間段內的各個時間點退役的 HDFS DataNode 數量。每種顏色描繪了一個 HDFS 叢集。

圖 3:自動檢測和停用損壞的 HDFS 資料節點

容器化 Hadoop

在過去,我們基礎設施的內在可變性曾多次給我們帶來了意想不到的麻煩。有了新架構後,我們得以在不可變 Docker 容器中執行所有 Hadoop 元件(NodeManager、DataNode 等)和 YARN 應用程式。

當我們開始重構時,我們在生產環境中為 HDFS 執行的是 Hadoop v2.8,為 YARN 叢集執行的是 v2.6。v2.6 中不存在對 YARN 的 Docker 支援。鑑於依賴 YARN 的多個系統(Hive、Spark 等)對 v2.x 存在緊密依賴,將 YARN 升級到 v3.x(以獲得更好的 Docker 支援)是一項艱鉅的任務。我們最終將 YARN 升級到了支援 Docker 容器執行時的 v2.9,並從 v3.1 向後移植了幾個補丁( YARN-5366YARN-5534 )。

YARN NodeManager 執行在主機上的 Docker 容器中。主機 Docker 套接字掛載到 NodeManager 容器,使使用者的應用程式容器能夠作為兄弟容器啟動。這繞過了執行 Docker-in-Docker 會引入的所有複雜性,並使我們能夠在不影響客戶應用程式的情況下管理 YARN NodeManager 容器的生命週期(例如 重啟 )。

圖 4:YARN NodeManager 和應用程式兄弟容器

為了讓超過 150,000 多個應用程式從裸金屬 JVM( DefaultLinuxContainerRuntime )無縫遷移到 Docker 容器( DockerLinuxContainerRuntime ),我們添加了一些補丁以在 NodeManager 啟動應用程式時支援一個預設 Docker 映象。此映象包含所有依賴項(python、numpy、scipy 等),使環境看起來與裸金屬主機完全一樣。

在應用程式容器啟動期間拉取 Docker 映象會產生額外的開銷,這可能會 導致超時 。為了規避這個問題,我們通過 Kraken 分發 Docker 映象。Kraken 是一個最初在 Uber 內部開發的開源點對點 Docker 登錄檔。我們在啟動 NodeManager 容器時預取預設應用程式 Docker 映象,從而進一步優化了設定。這可確保在請求進入之前預設應用程式 Docker 映象是可用的,以啟動應用程式容器。

所有 Hadoop 容器(DataNode、NodeManager)都使用卷掛載(volume mount)來儲存資料(YARN 應用程式日誌、HDFS 塊等)。這些卷在節點放在託管主機上時可用,並在節點從主機退役 24 小時後刪除。

在遷移過程中,我們逐漸讓應用轉向使用預設 Docker 映象啟動。我們還有一些客戶使用了自定義 Docker 映象,這些映象讓他們能夠帶來自己的依賴項。通過容器化 Hadoop,我們通過不可變部署減少了可變性和出錯的機率,併為客戶提供了更好的體驗。

Kerberos 整合

我們所有的 Hadoop 叢集都由 Kerberos 負責安全性。叢集中的每個節點都需要在 Kerberos(dn/hdfs-dn-host-1.example.com)中 註冊 主機特定服務主體 Principal(身份)。在啟動任何 Hadoop 守護程式之前,需要生成相應的金鑰表(Keytab)並將其安全地傳送到節點。

Uber 使用 SPIRE 來做負載證明。SPIRE 實現了 SPIFFE 規範。形式為 spiffe://example.com/some-service 的 SPIFFE ID 用於表示負載。這通常與部署服務的主機名無關。

很明顯,SPIFFE 和 Kerberos 都用的是它們自己獨特的身份驗證協議,其身份和負載證明具有不同的語義。在 Hadoop 中重新連線整個安全模型以配合 SPIRE 並不是一個可行的解決方案。我們決定同時利用 SPIRE 和 Kerberos,彼此之間沒有任何互動/交叉證明。

這簡化了我們的技術解決方案,方案中涉及以下自動化步驟序列。我們“信任”叢集管理器和它為從叢集中新增/刪除節點而執行的目標狀態運維操作。

圖 5:Kerberos 主體註冊和金鑰表分發

  1. 使用位置資訊(目標狀態)從叢集拓撲中獲取所有節點。

  2. 將所有節點的對應主體註冊到 Kerberos 中並生成相應的金鑰表。

  3. Hashicorp Vault 中儲存金鑰表。設定適當的 ACL,使其只能由 Hadoop Worker 讀取。

  4. 叢集管理器代理獲取節點的目標狀態並啟動 Hadoop Worker。

  5. Hadoop Worker 由 SPIRE 代理驗證。

  6. Hadoop Worker:

  7. 獲取金鑰表(在步驟 2 中生成)

  8. 將其寫入 Hadoop 容器可讀的一個只讀掛載(mount)

  9. 啟動 Hadoop 容器

  10. Hadoop 容器(DataNode、NodeManager 等):

  11. 從掛載讀取金鑰表

  12. 在加入叢集之前使用 Kerberos 進行身份驗證。

一般來說,人工干預會導致金鑰表管理不善,從而破壞系統的安全性。通過上述設定,Hadoop Worker 由 SPIRE 進行身份驗證,Hadoop 容器由 Kerberos 進行身份驗證。上述整個過程是端到端的自動化,無需人工參與,確保了更嚴格的安全性。

使用者組管理

在 YARN 中,分散式應用程式的容器作為提交應用程式的使用者(或服務帳戶)執行。使用者組(UserGroup)在活動目錄( Active Directory ,AD)中管理。我們的舊架構需要通過 Debian 包安裝使用者組定義(從 AD 生成)的定期快照。這導致了全系統範圍的不一致現象,這種不一致是由包版本差異和安裝失敗引起的。

未被發現的不一致現象會持續數小時到數週,直到影響使用者為止。在過去 4 年多的時間裡,由於跨主機的使用者組資訊不一致引發的許可權問題和應用程式啟動失敗,讓我們遇到了不少麻煩。此外,這還導致了大量的手動除錯和修復工作。

Docker 容器中 YARN 的使用者組管理自身存在一系列 技術挑戰 。維護另一個守護程序 SSSD (如 Apache 文件中所建議的)會增加團隊的開銷。由於我們正在重新構建整個部署模型,因此我們花費了額外的精力來設計和構建用於使用者組管理的穩定系統。

圖 6:容器內的使用者組

我們的設計是利用一個經過內部強化、信譽良好的配置分發系統(Config Distribution System)將使用者組定義中繼到部署 YARN NodeManager 容器的所有主機上。NodeManager 容器執行使用者組程序(UserGroups Process),該程序觀察使用者組定義(在 配置分發系統內 )的更改,並將其寫入一個卷掛載,該掛載與所有應用程式容器(Application Container)以只讀方式共享。

應用程式容器使用一個自定義 NSS庫 (內部開發並安裝在 Docker 映象中)來查詢使用者組定義檔案。有了這套解決方案,我們能夠在 2 分鐘內實現使用者組在全系統範圍內的一致性,從而為客戶顯著提高可靠性。

配置生成

我們運營著 40 多個服務於不同用例的叢集。在舊系統中,我們在單個 Git 儲存庫中獨立管理每個叢集的配置(每個叢集一個目錄)。結果複製貼上配置和管理跨多個叢集的部署變得越來越困難。

通過新系統,我們改進了管理叢集配置的方式。新系統利用了以下 3 個概念:

  • 針對.xml 和.properties 檔案的 Jinja 模板,與叢集無關

  • Starlark 在部署前為不同類別/型別的叢集生成配置

  • 節點部署期間的執行時環境變數(磁碟掛載、JVM 設定等)注入

圖 7:Starlark 檔案定義不同叢集型別的配置

我們將模板和 Starlark 檔案中總共 66,000 多行的 200 多個.xml 配置檔案減少到了約 4,500 行(行數減少了 93%以上)。事實證明,這種新設定對團隊來說更具可讀性和可管理性,尤其是因為它與叢集管理系統整合得更好了。此外,該系統被證明有利於為批處理分析棧中的其他相關服務(例如 Presto)自動生成客戶端配置。

發現與路由

在以前,將 Hadoop 控制平面(NameNode 和 ResourceManager)移動到不同的主機一直是很麻煩的操作。這些遷移通常會導致整個 Hadoop 叢集滾動重啟,還需要與許多客戶團隊協調以重啟相關服務,因為客戶端要使用主機名來發現這些節點。更糟糕的是,某些客戶端傾向於快取主機 IP 並且不會在出現故障時重新解析它們——我們從一次重大事件中學到了這一點,該事件讓整個區域批處理分析棧降級了。

Uber 的微服務和線上儲存系統在很大程度上依賴於內部開發的服務網格來執行發現和路由任務。Hadoop 對服務網格的支援遠遠落後於其他 Apache 專案,例如 Apache Kafka。Hadoop 的用例以及將其與內部服務網格整合所涉及的複雜性無法滿足工程工作的投資回報率目標。取而代之的是,我們選擇利用基於 DNS 的解決方案,並計劃將這些更改逐步貢獻回開源社群( HDFS-14118HDFS-15785 )。

我們有 100 多個團隊每天都在與 Hadoop 互動。他們中的大多數都在使用過時的客戶端和配置。為了提高開發人員的生產力和使用者體驗,我們正在對整個公司的 Hadoop 客戶端進行標準化。作為這項工作的一部分,我們正在遷移到一箇中心化配置管理解決方案,其中客戶無需為初始化客戶端指定典型的*-site.xml 檔案。

利用上述配置生成系統,我們能夠為客戶端生成配置並將配置推送到我們的內部配置分發系統。配置分發系統以可控和安全的方式在整個系統範圍內部署它們。服務/應用程式使用的 Hadoop 客戶端將從主機本地配置快取(Config Cache)中獲取配置。

圖 8:客戶端配置管理

標準化客戶端(具有 DNS 支援)和中心化配置從 Hadoop 客戶那裡完全抽象出了發現和路由操作。此外,它還提供了一組豐富的可觀察性指標和日誌記錄,讓我們可以更輕鬆地進行除錯。這進一步改善了我們的客戶體驗,並使我們能夠在不中斷客戶應用程式的情況下輕鬆管理 Hadoop 控制平面。

心態的轉變

自從 Hadoop 於 2016 年首次部署在生產環境中以來,我們已經開發了很多(100 多個)鬆散耦合的 python 和 bash 指令碼來運維叢集。重新構建 Hadoop 的自動化技術棧意味著我們要重寫所有這些邏輯。這一過程意味著重新實現累積超過 4 年的邏輯,同時還要保證系統的可擴充套件性和可維護性。

對 21,000 多臺 Hadoop 主機大動干戈以遷移到容器化部署,同時放棄正常運維多年的指令碼積累,這樣的方案在一開始招致了不少懷疑聲。我們開始將該系統用於沒有 SLA 的新的開發級叢集,然後再用於整合測試。幾個月後,我們開始向我們的主要叢集(用於資料倉庫和分析)新增 DataNodes 和 NodeManagers,並逐漸建立了信心。

我們通過一系列內部演示和編寫良好的執行手冊幫助其他人學會使用新系統之後,大家開始意識到了轉移到容器化部署的好處。此外,新架構解鎖了舊系統無法支援的某些原語(提升效率和安全性)。團隊開始認可新架構的收益。很快,我們在新舊系統之間架起了幾個元件,以搭建一條從現有系統到新系統的遷移路徑。

搬運猛獁象

我們新架構採用的原則之一是機群中的每一臺主機都必須是可更換的。由舊架構管理的可變主機積累了多年的技術債務(陳舊的檔案和配置)。作為遷移的一部分,我們決定重新映象我們機群中的每臺主機。

目前,自動化流程在編排遷移時需要的人工干預是極少的。巨集觀來看,我們的遷移流程是一系列 Cadence 活動,迭代大量節點。這些活動執行各種檢查以確保叢集穩定,並會智慧地選擇和停用節點,為它們提供新配置,並將它們添加回叢集。

完成遷移任務的最初預期時間是 2 年以上。我們花了大量時間調整我們的叢集,以找到一個儘量提升遷移速度,同時不會損害我們 SLA 的甜點。在 9 個月內,我們成功遷移了約 60%(12,500/21,000 臺主機)。我們正走在快車道上,預計在接下來的 6 個月內完成大部分機群遷移工作。

圖 9:在大約 7 天內遷移 200 多臺主機

要記住的傷疤

如果我們聲稱我們的遷移非常平滑,那肯定是在撒謊。遷移的初始階段非常順利。然而,當我們開始遷移對更改更敏感的叢集時,發現了很多意想不到的問題。

  1. 丟失塊和新增更多防護措施

我們的一個最大的叢集有多個運維工作流同時執行。一個叢集範圍的 DataNode 升級與叢集另一部分發生的遷移一起觸發了 NameNode RPC 延遲的降級。後來發生了一系列意外事件,我們最後輸掉了戰鬥,導致叢集中丟失了一些塊,我們不得不從另一個區域恢復它們。這迫使我們為自動化和運維程式設定了更多的防護措施和安全機制。

  1. 使用目錄遍歷進行磁碟使用統計

叢集管理器代理定期執行磁碟使用情況統計以備使用率分析,並將其提供給公司範圍的效率計劃。不幸的是,該邏輯意味著“ 遍歷 ”儲存在 DataNode 上的 24x4TB 磁碟上的所有 HDFS 塊。這導致了大量的磁碟 i/o。它不會影響我們不太忙的叢集,但對我們最繁忙的一個叢集產生了負面影響,增加了 HDFS 客戶端讀/寫延遲,這促使我們增強了這一邏輯。

要點和未來計劃

在過去 2 年中,我們對 Hadoop 的執行方式做出了巨大的改變。我們升級了我們的部署,從一大堆指令碼和 Puppet 清單轉向了在 Docker 容器中執行大型 Hadoop 生產叢集。

從指令碼和工具過渡到通過成熟的 UI 運維 Hadoop,是團隊的重大文化轉變。我們花在叢集運維上的時間減少了 90%以上。我們讓自動化操作控制來整個系統的運維、更換損壞主機。我們不再讓 Hadoop 來管理我們的時間了。

以下是我們旅途中的主要收穫:

  • 如果沒有先進的卓越運維技術,Hadoop 可能會變成一個難以馴服的龐然大物。組織應該定期重新評估部署架構並定期償還技術債務,以免亡羊補牢。

  • 大規模基礎設施的重構需要時間。組織應該為此建立強大的工程團隊,專注於進步而不是追求完美,並隨時準備好應付生產環境中出現的問題。

  • 感謝所有參與各個架構團隊的工程師,我們的架構非常穩固。建議與 Hadoop 領域之外的人們合作以收集不同的觀點。

隨著我們的遷移任務逐漸步入尾聲,我們正在將重點轉移到更多令人興奮的工作上。利用底層容器編排的優勢,我們為未來制定了以下計劃:

  • 單擊一次開通多叢集和一個區域內跨多個專區的叢集平衡

  • 主動檢測和修復服務降級和故障的自動化解決方案

  • 通過橋接雲和本地容量來提供按需彈性儲存和計算資源

作者介紹

Mithun Mathew 是 Uber 資料團隊的二級高階軟體工程師。 他之前作為 Apache Ambari 的開發人員參與 Hadoop 部署工作。他目前在 Uber 專注於資料基礎設施的容器化。

Qifan Shi 是 Uber 資料基礎設施團隊的高階軟體工程師,也是 Hadoop 容器化的核心貢獻者。他一直在參與能夠高效編排大規模 HDFS 叢集的多個系統的研發工作。

Shuyi Zhang 是 Uber 資料基礎設施團隊的高階軟體工程師。她是 Hadoop 容器化的核心貢獻者。她目前專注於 Uber 的計算資源管理系統。

Jeffrey Zhong 是 Uber 資料基礎設施團隊的二級工程經理,負責管理資料湖儲存(Hadoop HDFS)和批量計算排程(Hadoop YARN)。在加入 Uber 之前,他曾參與 Apache HBase 和 Apache Phoenix 的工作。

原文連結: https://eng.uber.com/hadoop-container-blog/