艾位元組俱樂部:LinkedIn 在擴展 Hadoop 分散式檔案系統上的旅程

Scott Hsieh (史考特)
29 min readJun 13, 2021

--

此篇文章作者為 Konstantin V. Shvachko Chen Liang、以及 Simbarashe Dzinamarira ,於 2021 年 5 月 27 日撰寫於 LinkedIn 的工程部落格裡面。有需要的人可以從這裡閱讀原文— — The exabyte club: LinkedIn’s journey of scaling the Hadoop Distributed File System— — 。

LinkedIn 的大數據分析是在 Hadoop 上執行。在過去五年裡,這座分析架構經歷了許多大幅度的成長,在資料量級、運算量、 以及其他所有面向上幾乎是每年雙倍成長。這座系統最近達成了兩個重要的里程碑。

  1. 橫跨所有 Hadoop 叢集, LinkedIn 現在儲存了1 艾位元組 (exabyte,1 EB 等於 1 百萬 TB) 的總資料量。
  2. 我們最大有 10,000 個節點的叢集儲存了 500 拍位元組(petabyte,1 PB 等於1,000 TB)的資料,那座叢集在單一台提供遠程程序呼叫(RPCs) 的 NameNode 下以平均延遲低於 10 毫秒的水準維護著十億個物件(目錄、檔案、和區塊),讓它成為了業界裡面最大的 Hadoop 之一。

在 LinkedIn 早期,Apache Hadoop 是我們分析架構的基礎。許多團隊投入了努力讓 Hadoop 成為我們的標準大數據平台。

這篇部落格貼文主要著重在 Hadoop 儲存上,即 HDFS 。它能概括使得我們有目前水準擴展性的諸多面向的成就,我們將來聊聊協助我們擴展 HDFS 命名空間(namespace)服務的效能調校、談談現有 HDFS 的重要功能、並展示它們在我們環境中所帶來的好處。我們也會展示在維護一個很多小檔案的大命名空間時去橫向擴展衛星叢集的工程挑戰。我們會說說新功能的設計意涵,就是待命中的一致性讀取(Consistent Reads from Standby),這項功能是被開發來滿足命名空間呈指數成長工作量的需求的。最後,我們會回顧 HDFS 新的和現有的功能組件,像是可選擇性資訊加密(Selective Wire Encrption) 、靜態加密(Encrpyion at Rest)、以及被設計來延伸 HDFS 生態系統的蟲洞(Wormhole),用於新的使用情境並提供保護資料的法律合規性。

歷史性擴展

圖一追蹤了三項主要指標並展示出了我們最大 Hadoop 叢集呈指數性成長。

圖一、LinkedIn 最大 Hadoop 叢集( 2015 至 2020 )在資料、元資料(meatadata)、以及運算上的指數成長圖。在 2020 年,我們使用了 500 PB (1PB=1,000TB)的儲存容量、16 億件命名空間物件、以及每小時 5,000萬 GB 的運算量。
  1. 叢集上使用到的總資料儲存量——從 2015 年我們有 20 PB 的資料一直成長到 500 PB 的用量。
  2. 命名空件物件的數目(目錄、檔案、區塊)在 2015 年是 1.45 億個物件。 2020 年底,這座叢集有三個命名空間管理單位(namespace volume),當中總共有 16 億個物件,而最大的管理單位超過 10 億個物件。
  3. GbHr 量測了一天裡面這座叢集被所有應用程式使用到的隨機存取記憶體(RAM)的總使用量。運算成長在過去兩年內因著在 LinkedIn 裡機器學習的擴展運用有很大幅度地加速成長。

高可用性和滾動式更新

Hadoop 分散式系統(HDFS)裡面,檔案系統的元資料會從資料中被解耦。元資料涵括了目錄樹狀結構、檔案、以及區塊,由被稱作 NameNode 的專用伺服器裡頭的記憶體所維護著。檔案資料被切分到區塊裡面並被複寫到諸多資料節點(DataNode)中好達到可靠性和可用性。NameNode 伺服器在HDFS 叢集裡面曾是單點故障(SPOF,single point of failure)的狀態,在 Hadoop 2 中所推出的高可用性消除了這份限制。Hadoop 2 讓你可以運行多台被複製的 NameNode 伺服器,單一台使用中的 NameNode 接收所有客戶端的請求並發佈它的日誌交易紀錄(journal transaction)到日誌服務(Journal Service)裡面。多台待命中的 NameNode 伺服器消化著那些交易紀錄並根據交易紀錄更新它們裡頭的命名空間狀態,這項動作使它們總是在最新的狀態並且可隨時接管—如果使用中的 NameNode 伺服器掛掉的話 — 。

HDFS 提供了許多選項讓你可以設置一座高可用性的叢集,我們使用了投票性日誌管理者(QJM,Quorum Journal Manager)作為日誌服務和 IP 容錯的機制,讓客戶端可以知道哪一台 NameNode 伺服器是正常運轉中。投票性日誌管理者(QJM)包含了許多伺服器—我們的應用裡是三台—,這些伺服器組成了投票性系統,這座系統可靠地儲存了 NameNode 伺服器交易紀錄並把這些內容服務給待命中的 NameNode 伺服器。在 IP 容錯的幫助下,所有客戶端可以使用同一份虛擬 IP 位址(VIP,Virtual IP)和運轉中的 NameNode 伺服器溝通,不用考慮 NameNode 伺服器的實體位址。VIP 故障轉移由 Linux 系統工具來處理,允許多台 NameNode 伺服器之間的移轉對客戶端具備透明性地發生。

除了消除單點故障的限制,高可用性對於滾動式更新也同樣重要。在高可用性被推出前,只要是任何軟體或硬體的更新,都會需要 NameNode 伺服器重啟。這是相當有干擾性和破壞性的程序,因為 NameNode 伺服器的啟動可以花費到一小時之久,特別是在大型叢集上。這段期間,所有叢集上的工作(任務)都必須延宕。在高可用性叢集中,更新就能夠以滾動的方式被執行。首先,其中一台待命中的 NameNode 伺服器被更新了軟體並且被重新啟動。然後,運轉中的 NameNode 伺服器容錯到已更新過的待命 NameNode 伺服器並在之後被更新加重啟。接著,資料節點偕同新軟體被重新啟動。資料節點的重啟能夠以批次形式去執行,只要每一個批次屬於同一個機架(rack),畢竟 HDFS 是被設計可承受機架故障而不會遺失資料。滾動式更新的過程是透過專門的腳本檔作為 Jenkins 任務去執行而達到自動化。

高可用性和滾動更新讓我們能夠消除有干擾性的系統更新,改善對使用者(客戶端)的叢集可用性。

Java 調教

日益增長的檔案系統命名空間需要在 NameNode 伺服器上週期性的增加 Java 堆記憶體大小(heap size),因為伺服器為了低延遲存取將所有元資料都保存在了隨機存取記憶體裡頭。在撰寫當下,我們最大的 NameNode 伺服器設定了 380 GB 的堆記憶體(heap)去維護 11 億個命名空間物件。如此大的記憶體用量需要縝密的調教好提供高效能並且還需要避免因為整體性的垃圾回收(garbage collection)而有的長時間暫停。

Java 堆記憶體世代

Java 堆記憶體通常被分成兩個空間:年輕一代(young generation)和終身(老年)一代(tenured generation)。年輕一代被用於存活時間不長的物件而終身一代則是用於會在好幾回垃圾回收中存活的長時間物件,像是檔案 inode 和 NameNode 伺服器的區塊。當 NameNode 伺服器中的工作量增加,伺服器就會在年輕一代空間中產生較多的暫時物件,而命名空間的成長則會增加終身空間(tenured space)。實務上,命名空間的成長與增加更多工作者節點的叢集擴展有相關性。因此,在兩種空間中保持一個最佳的比例是相當重要的事情。我們目前是讓年輕一代和永久一代的比例保持在大約 1 比 4 的比例。透過合適地安排年輕和終身空間的大小,完全避免整體性的垃圾回收就會是有可能的,畢竟整體性的垃圾回收在如此大的堆記憶體大小可是有機會造成好幾分鐘長的停用時間的。

非公平性上鎖

NameNode 是高度多執行緒的應用程式。這台伺服器使用了全域讀寫鎖(global read-write lock)保護了整個命名空間並控制了並行性(concurrency)。傳統上,寫入鎖(write lock)是獨一無二的—只有一個執行緒能夠拿取—而讀取鎖(read lock)則是共享的,允許多個讀取執行緒擁有這把鎖的情況下執行。Java 中的鎖機制支援兩種模式:

  1. 公平模式:鎖的取用以先進先出順序去執行(Java 預設)
  2. 非公平模式:鎖的取用能夠不透過順序去索要

公平性上鎖會嘗試以請求的順序安排一個執行緒對鎖的取用。所以,當一個寫入執行緒在等待它能獲取寫入鎖時,它實際上會擋住在它之後、所有索取讀取鎖的讀取執行緒,即便那些讀取執行緒能夠和當前的讀取執行緒並行執行。因此,非公平性模式就是在寫入執行緒的情況下允許讀取執行緒通行。這種模式乍看之下對寫入執行緒是不利的但實務上這樣的做法很大程度地改善了 NameNode 伺服器整體的效能,因為工作量很大程度地偏向到了讀取請求,它佔了所有命名空間操作的 95 %。

Dynamometer 是一款能基於實際工作負載追蹤對 HDFS NameNode 伺服器做效能比較和壓力測試、由 LinkedIn 開源的 Hadoop 標準工具。我們用了 Dynamometer 以公平性上鎖和非公平性上鎖去模擬生產環境的工作負載然後拿來和生產環境比較。圖二的中間顯示出當非公平性上鎖被部署時的延遲下降,勾勒出非公平性模式給出了一個數量級優於公平性模式的效能表現。

圖二、非公平性上鎖讓 NameNode 伺服器的遠程程序呼叫延遲改善了 10 倍。

衛星叢集

小檔案問題

HDFS 是被優化來維護大檔案並提供了高吞吐量給批次資料處理系統相當基本的序列性讀取和寫入。然而,小檔案對大部分的檔案系統都會造成問題,HDFS 也不沒在例外內。這個問題已經被挖掘地相當深入,可以參考例如《HDFS 的可擴展性:成長上的限制》和後續的《Apache Hadoop:可擴展性更新》。

日誌目錄

我們以約莫 1.1 的磁碟區塊對檔案的比例開啟了我們的衛星叢集專案,意思就是在我們的系統裡面有 90 % 的檔案都是小檔案。我們的檔案大小分佈分析揭示了系統目錄 /system ,主要由許多平均大小少於 1 MB 、很小的檔案所構成,它們有設定檔、諸如 YARN、 Spark 歷史伺服器(history server)、和其它系統應用程式的 Hadoop 服務所管理的日誌等等。這個目錄包含整個命名空間一半的檔案但只佔用了叢集資料容量的很小一部分,0.07 %。因為系統目錄是專特給 Hadoop 內部系統所使用,我們可以在不帶來任何面向使用者的變動的情況下將它給移到不同的叢集裡面。這項事實促使我們以和主要叢集一樣的小的 NameNode 伺服器建立了一個新的(衛星)叢集,不過少了大概 100 多倍的資料節點。

啟動衛星叢集

啟動節點我們需要將日誌資料從主要叢集移到衛星叢集,雖然資料量(60 TB)相對小了許多,但檔案的數量(1億個)卻仍舊是大的,而它就帶來了第一項挑戰。

起初,我們嘗試透過 DistCp (分散式複製)相當直觀的方法複製這全部 1 億個檔案—給叢集內或叢集間平行複製資料的標準 Hadoop 工具—。因為我們有這麼大數量的檔案,以致 DistCp 任務在嘗試搜集它需要複製的所有檔案路徑的初始化階段就卡住了。這項任務在 NameNode 伺服器上產生了極耗算力的負擔使得 NameNode 伺服器在那段期間對其它客戶端變為無法回應和無法存取。我們估算即便我們將複製任務分割成許多可控管的步驟,這項任務仍舊需要超過 12 小時來完成。在那段時間裡,新資料鐵定會進來,而一些被複製的資料就會變得過時。這個解決方案也就變為了不可行。

我們決定建立一個客製化、相容 HDFS 的檔案系統驅動器,稱作 FailoverFS,它會總是將新的日誌寫入到衛星叢集裡面,同時呈現出結合兩座叢集檔案的讀取檢視。這個功能讓新工作可以寫入它們的日誌到衛星叢集時,也能讓存取這些日誌的服務,像是 Spark 歷史伺服器,能夠從兩座中叢集讀取日誌。日誌保留政策服務,被設定在大於 1 週後即刪掉檔案,至終能從主要從集中刪除掉這些日誌。

相當大的區塊報告

在 HDFS 裡的資料節點會傳送週期性的區塊報告給 NameNode 伺服器讓它知道資料節點擁有的所有複製區塊。對於每一個區塊,區塊報告包含了區塊 ID、它的生成戳記、以及長度。區塊報告會根據來自資料節點同樣磁碟區區塊被分片成最小區塊(chunk)。NameNode 伺服器會以鄰近的方式處理每一份磁碟區報告,並持有全域命名空間鎖(global namespace lock)。

衛星叢集有 32 座資料節點,因為資料量是小的。因此,我們有很大的區塊數目(粗略地等同檔案數量)分佈在小數量的資料節點中,這造就了每個節點的複寫數目,也因而讓區塊報告的大小,變得非常大。為了能比較,在衛星叢集上的一座資料節點含有 9 百萬個區塊複寫實體而主要叢集上是 20 萬個左右的複寫實體。這就帶來了另一項挑戰,因著 NameNode 伺服器上區塊報告的處理變得來勢洶洶,使得在處理期間對其它操作又讓 NameNode 變為無法回應。

解決方案是將每一座資料節點磁碟機分割成十份虛擬磁碟區。這個方案將區塊報告分片為數量上多、諸多尺寸較小的磁碟區最小區塊,讓 NameNode 伺服器上的報告處理變得更為細緻。

整體而言,衛星叢集的引進讓我們能夠管理另一回合基礎設施的雙倍成長。它帶來了改善後、區塊對檔案 1.4 的比例。

待命中節點的一致性讀取

動機與需求

摩爾定律急遽趨緩—並且會在 2025 年劃下句點—的時代裡,HDFS 的可擴展性主要的限制因素變為了 NameNode 伺服器效能,因為 CPU 速度的限制。在預期叢集成長的下個循環時,我們了解到即使我們能夠藉由增加 NameNode 伺服器的堆大小(heap size)來維持住元資料物件的成長,我們依舊無法以單一台元資料伺服器對抗元資料操作工作負載的增加。工作負載應該要在許多台伺服器間並行處理。

一座有啟動高可用性的 HDFS 通常會有兩台 NameNode 伺服器,一台用於運轉而另一台則是待命狀態。任何待命節點都是運轉中節點的複寫實體。運轉和待命節點之間的元資料狀態管理是由投票式日誌管理者(Quorum Journal Manager)所處理。運轉中的 NameNode 發佈日誌交易給 QJM,而待命節點則是從 QJM 以同樣在運作節點上被執行的順序跟隨這些交易。這造就了從待命節點讀取元資料而不是運轉中節點的機會。運轉中的節點會保持唯一一台 NameNode 伺服器來服務寫入需求,作為命名空間更新的唯一信任來源。

我們在生產環境裡的元資料工作負載分析顯示出讀取請求佔了所有命名空間操作的 95 %,因此,讀取的平衡負載應當要很大幅度地改善元資料操作的總吞吐量。

然而,HDFS 中傳統的待命 NameNode 伺服器只被用於容錯並拒絕任何客戶端的操作。為了克服這項議題,我們引進了觀察者(Observer)節點的概念,它是個也能服務讀取請求的待命節點。觀察者節點並同所有相關的邏輯作為 HDFS-12943 的一部分被完全的實作出來。我們把它部署到了我們的叢集裡並且已經在生產環境執行了超過一年的時間。

定義我們設計決策的主要需求包括了:

  1. 強型一致性。客戶端應該要總是能取得 HDFS 的一致性檢視不論他們正連著哪一座 NameNode 伺服器。
  2. 元資料操作的高吞吐量。
  3. 現有應用程式的透明度。客戶端 APIs 應該要將 NameNode 伺服器看成單一服務。

一致性模型

在高可用性設定中,運轉中、待命中、與觀察者節點都遵循了同樣順序的事件群,當中每一條事件皆會修改命名空間的狀態。「命名空間狀態 ID 」標示了每當命名空間透過修改轉化後的狀態。狀態 ID 是一個隨著每一次修改呈單調性遞增的數字。每一次在 NameNode 伺服器上的修改都會後一次日誌交易匹配,所以狀態 ID 是以那次交易的 ID 來實化。

運轉中和待命中節點的狀態在它們的狀態 ID 一樣時就代表它們是完全同步的狀態。但待命節點是它領導者,運轉節點,的跟隨者,因此待命節點會總是在運轉節點之後,因為它總是在消化運轉節點已經執行過的交易。

過時讀取問題:從客戶端的角度來看,觀察者的命名空間狀態和運轉節點是一樣的,除了一些有限群集、才剛被修改的「新」物件。雖然觀察者節點可以從任一 NameNode 節點中大部分的「舊」檔案和目錄獲得正確的資訊,觀察者節點是有可能回傳「新物件」的過時資訊的。

一致性原則傳神地描述了客戶端不該看見過去的狀態。這是一條常識假設,它保證了客戶端總是從事件的歷史中在向前走,即便它們在多台 NameNode 伺服器之間轉換。更正式地說:

  • 如果客戶端 C 在時間點 t1 看見或是修改了一個在狀態 s1 的物件,那麼在未來任何時間點 t2 > t1 的情況下,客戶端 C 都會是看見物件的狀態 s2 ≥ s1。

我們列出兩種主要場景,當中一致性原則會被違反:

  1. 讀取自身寫入(RYOW,read your own writes)
    如果 HDFS 客戶端在運轉節點上修改了命名空間,接著讓觀察者給讀取了,觀察者應該要能看見同樣或是較晚之後的命名空間的狀態,而不是較早先時候的狀態。
  2. 第三方通訊(3PC,third-party communication)
    如果一個客戶端修改了命名空間並且給另一個客戶端傳遞了這份資訊,後者應該要能從觀察者節點讀取命名空間同樣的狀態或是較為之後的狀態但不會是早先時候的狀態。

一個 RYOW 的例子就是當一個客戶端在運轉節點上建立了一個目錄並且嘗試從觀察者節點 ls目錄。如果觀察者尚未處理相關的 mkdir 交易,它就會回應說目錄並不存在。這就違反了一致性原則。

3PC 場景也是有機會發生的,舉例來說,MapReduce 或是 Spark 的任務遞交》。首先,工作客戶端在 HDFS 上建立了工作設定與 JAR 檔案。然後,客戶端通知了 YARN 資源管理者(ResourceManager)去調度工作任務。每一個任務在啟動期間會讀取工作設定和 JAR 檔案。如果檔案在觀察者節點上因為延遲的交易仍舊無法存取,工作就可能會失敗。工作客戶端和資源管理者(ResourceManager)的第三方通訊在這裡就在 HDFS 協定外發生了。

以下段落我們要來說明我們的設計如何解決這些場景。

日誌尾隨:快速路徑

為了從觀察者節點保證一致性和讀取的效能,日誌尾隨(journal tailing)—從 QJM 取得新交易的過程—應該要迅速,待命節點和運轉節點間的狀態更新延遲要降到最低。透過日誌尾隨的傳統實作,延遲會是在分鐘級的程度上—預設為 2 分鐘—但在我們多座大叢集上,它有時會來到 8 分鐘。這會增加讀取操作的延遲至數分鐘等級,而最大可忍受的閾值則不超過 50 毫秒。

運轉中的 NameNode 伺服器發布日誌交易給投票性、構成 QJM 服務的日誌節點。當日誌節點收到交易,將這些交易永久性地保留在磁碟上就會是必要的,為了防止故障發生而導致的資料遺失。交易被分段分段地寫入,其中一份分段檔案在可設定的時間區間上(預設是 2 分鐘)會被回滾或是根據接受到的交易數量。在傳承的實作中,待命中節點只會尾隨全部的分段,經由 HTTP 呼叫從磁碟中讀取最後一塊分段,只要分段檔案被一個日誌節點所完成(關閉)。

HDFS-13150 透過下列關鍵修改提出並實作了快速路徑(Fast Path)尾隨:

  1. 這份提議引進了最近日誌交易的內存快取。日誌節點服務從記憶體中而不是磁碟中給待命中的節點快取過的交易。
  2. 一個待命中的節點可以索取一系列從 startTxId 開始的交易。日誌節點接著就會從起始 ID 到所設定的最大批次大小回傳所有已知的交易。這允許了細度控制的日誌尾隨,因為通常這類的請求只會回傳一些最近未被應用的交易。
  3. 最後一次交易的請求就是 RPC 呼叫,這比透過 HTTP 傳送分段檔案還快。RPC 呼叫是透過日誌節點的投票性讀取去實作,保證了待命中節點只會看見被確認的交易。

快速路徑尾隨的使用是一個可設定的選項,它被觀察者節點所需要。在啟動時間或是待命節點從 QJM 快取讀取上落後太多,快速路徑尾隨會自動切回讀取被永久持有的分段,這會有較高的延遲但也有較高的吞吐量。

快速路徑的效能評估從原本的 2 分鐘延遲上展現出了可觀的改善。

  • 一個觀察者節點在 RYOW 情境中的平均客戶端觀察到的延遲(客戶端從運轉中節點上採用後在觀察者上能看到交易的時間)低於 6 毫秒。
  • 平均的交易處理時間,從一份交易從運轉節點上被遞交到它被觀察者採用,是 25–30 毫秒。

快速路徑尾隨讓觀察者節點能夠保持它們的狀態到極為接近運轉中節點,但它並不會消除從觀察者節點的過時讀取問題,我們接下來就要來討論這個問題。

讀取你自己的寫入

NameNode 伺服器以 LastWrittenStateId 維護著它們最後一份狀態 id ,它能對應到命名空間的最後更新與各自寫進 NameNode 日誌的相對應交易。

我們給每個 HDFS 客戶端引進 LastSeenStateId 去指出被客戶端觀察到的命名空間的最後狀態。這份資訊會在每一次對 NameNode 伺服器的呼叫自動被更新而且會被設置到各自的 LastWrittenStateId 。舉例而言,當客戶端 C 傳送了寫入請求給運轉中節點,曾被執行的請求也會在運轉中節點設定成 C.LastSeenStateId = A.LastWrittenStateId 。現在 C 傳送了讀取請求給觀察者並傳入了它的 C.LastSeenStateId 。觀察者節點會驗證它在自己的 O.LastWrittenStateId 是匹配了或是超過了客戶端的 C.LastSeenStateId 並延後請求執行直到狀態值有被追上。對觀察者節點的呼叫也會重新設置 C.LastSeenStateId = O.LastWrittenStateId ,所以後續對觀察者節點的呼叫對 C 並不會被延遲。客戶端能總是不帶有延遲地切換到運轉中節點是因為:

A.LastWrittenStateId >= O.LastWrittenStateId >= C.LastSeenStateId

LastSeenStateId 作為 RPC 的標頭被無縫地在伺服器和客戶端間傳送。

如我們已看過的,使用快速路徑尾隨,觀察者節點對運轉中節點的延遲狀態已經小到能夠和一個客戶端從一個節點切換到另一個節點所需的時間做比較。如果一個觀察者落後客戶端的狀態遠超被允許的閾值。它就會通知客戶端,然後這個客戶端就會切換到另一個觀察者或是運轉中的 NameNode 伺服器。

LastSeenStateId 保證了客戶端永遠不會在 RYOW 情境中看到命名空間過去的狀態。

第三方通訊:msync()

在 3PC 情境中,一客戶端 C1 在運轉中 NameNode 伺服器上建立了,譬如說,一個檔案而另一個客戶端 C2 預期能在觀察者節點上看到這份檔案。 C2 知道那份檔案被建立了但並不知道 C1 的 LastSeenStateId ,這就會發生過時的資訊被讀取。我們推出了新的 HDFS API FileSystem.msync() 保證了 C2 的一致性讀取,並夾帶了一系列的增強功能好避免在一般情境裡面去呼叫到 msync()

msync() 和 HDFS 的 hsync() 很相似,後者保證了資料對其它客戶端是可以讀取的而 msync() 則是對元資料提供了相同的保證。當 C2 呼叫了 mysnc() ,客戶端就會聯繫運轉中的 NameNode 伺服器,帶著純粹強制更新它自己的 C2.LastSeenStateId 的目的。現在,C2 得知了命名空間的當下狀態,這個狀態被保證至少是 C1.LastSeenStateId ,C2 現在就能夠從觀察者節點安全地讀取這份檔案。

增加顯性的 msync() 呼叫給現存的應用程式違背了我們透明度上面所列的需求(#3)。期待所有應用程式在新功能被部署進生產環境能改變是相當不切實際的,這促使我們決定讓客戶端自動同步它們的狀態,也因而避免了在大部分場景中顯性的 msync()

  1. 啟動時無成本的自動客戶端狀態同步化。當一個 HDFS 客戶端啟動了,它就會發現現有 NameNode 伺服器的 HA 狀態。命名空間狀態 ID 會在這些呼叫中被背著。
  2. 週期性同步化強制客戶端在一段可設定的時間之後自動呼叫 msync()
  3. 總是 msync 模式是前者當時間週期等於零的一個特殊情境,這迫使客戶端在每次讀取請求前會自動呼叫 msync()
  4. 總是運轉模式。客戶端被設定為讀取時不使用觀察者節點。

啟動時的同步化(1)特別解決了工作遞交框架下的過時讀取問題,如早先時候討論到的。當一個 MapReduce 或是 Spark 任務開啟,它們會初始化一個 HDFS 客戶端,它會自動跟上運轉中節點上的最後狀態進而避免了觀察者的過時讀取問題。

總是 msync (3)是個昂貴的選項,因為每一個元資料操作都會產生兩次 RPC 呼叫。

上面的增強功能避免了大部分使用情境中對 msync() 的顯性使用。少見類型的長時執行唯讀的客戶端仍舊是有需要的,這類的客戶端只從觀察者做讀取並且能夠被拉到觀察者後面跟著。它們要不是在應用程式邏輯控制的關鍵時刻直接呼叫 msync() 或是使用總是運轉模式(4)。

圖三、觀察者節點一致性讀取的HDFS 叢集架構

效能結果

我們使用 Dynamometer 的初步效能評估反應出獲取了 2 倍量的元資料操作吞吐量,偕同觀察者的一致性讀取。最後結果超過了這些期待。在我們最大的生產環境叢集,命名空間操作的總吞吐量增加了 3 倍,平均延遲降低到原先時候的三分之一。現在,NameNode 伺服器平均每秒執行 15 萬次操作,高峰是每秒 25 萬次操作,而平均延遲為 1 到 2 毫秒。

超越 Hadoop 生態系統的擴展

這個段落主要著重在 HDFS 生態系統的延伸以及藉由引進現存的 HDFS 的功能並透過內部工具整合它們的使用情境、藉由開發新的 HDFS 功能、以及藉由建立新的內部系統的使用情境。這表徵了另一個領域的可擴展性。

基於通訊埠的可選擇性資訊加密(port-based selective wire encryption)

近幾年來,一般資料保護規範(GDPR,General Data Protection Regulation)和加州消費者隱私保護法(CCPA,California Consumer Privacy Act)給個人資料收集的模式和存取放上了更嚴格的請求。網絡安全性是資料保護的一個重要環節。這方面常見的實務做法是區域網路(LAN,local-area network)流量,像是資料中心內,被建議要加密並且「透過廣域網路連結時,因為保證這些渠道實體安全性的困難度,應該總是要加密」。

通過 WAN 或是LAN 的機敏資料通訊要總是被加密。然而,在資料中心內加密流量會產生效能懲罰。我們設計了一個在 HDFS NameNode 伺服器上給客戶端存取諸多通訊埠的方法來幫助解決這個挑戰,透過這種方式,一個通訊埠是用來執行加密過的通訊而另一個通訊埠則是接受未加密的請求。我們後續引進了防火牆規則,其中只公開對外部客戶端加密流量的通訊埠,同時讓資料中心之間的通訊保持有效率。

WebHDFS 是一款標準的 Hadoop 工具,它提供了 REST APIs 透過 HTTP 或是 HTTPS 去存取 HDFS 的資料。早先以前,我們借力了透過安全 HTTPS 的 WebHDFS 去達成這項目標。然而,WebHDFS 比 RPC 和直接的資料傳送還要慢,而且它還包括了開發成本,畢竟我們需要讓內部和外部的客戶使用不同的 API ,所以實作了同樣的邏輯兩次。

我們完整地評估了來自開源的不同解決方案,包括選擇性加密 HADOOP-10335 基於白名單的現有解決方案。那個方法的主要顧慮是一致性和可擴展性,因為它會需要維護所有資料節點被允許的客戶端清單。維護橫跨數千個資料節點如此的設定檔一致是很困難的事,因為增加或是移除一個單一節點會需要改變叢集上的所有資料節點。

我們的方法取而代之實作了下列更動:

  1. 這個方法泛化 NameNode RPC 伺服器在諸多通訊埠的監聽。每一個通訊埠都有自己的安全性設定,定義了是否要執行加密。
  2. 客戶端可以和不同的 NameNode 通訊埠溝通,這讓一個客戶端連線到加密過的通訊埠時會啟用 RPC 呼叫加密。
  3. NameNode 在區塊存取標記(token)中設定了一個欄位指出它要執行的安全性政策。這份標記會被返回給客戶端。
  4. 客戶端如此一來會將區塊存取標記呈現給資料節點。資料節點會檢查標記然後進一步執行和 NameNode 一樣的安全性政策。這擔保了資料傳送加密和 RPC 政策相聯繫。

除此之外,我們採用了下列的作業程序:

  1. 防火牆規則阻擋了未加密要跨連線存取的 NameNode 通訊埠。
  2. 客戶端根據它們在資料中心外面或裡面被設置了連線到不同的 NameNode 通訊埠。

這個方法承受了最小的運作開銷成本。這個方法只需要啟動一些設置改動。我們的效能比較顯示出基於通訊埠的選擇性加密勝過了透過 HTTPS 的 WebHDFS。我們能看見 36–46 % 在讀寫延遲上的降低以及 56–68 % 的讀寫吞吐量。

這項功能是在 HDFS-13541 這張票下所開發,也可以參考我們在 ApacheCon 2019 的分享

靜態加密

資訊加密(wire encryption)和 HDFS 靜態加密加強了我們與資料保護法令的合規性。客戶端透明的靜態加密是 HDFS 提供的標準功能,它允許了使用者設置特定的目錄為「加密區」(encryption zone)。任何寫進加密區的檔案都會以資料加密鑰匙(DEK,data encryption key)被自動加密,HDFS 客戶端要讀取它們的時候會透明地解密檔案資料。

加密區和專用的加密區鑰匙是綁在一起的,鑰匙是在區域被建立時所產生並且被用來加密檔案的資料加密鑰匙。加密區鑰匙本身是儲存在鑰匙管理服務(KMS,Key Management Service)裡面而且絕不會離開這個服務。

當一個客戶端建立了一份檔案,它會要求 KMS 建立一把新的資料加密鑰時,它在之後會被客戶端用來加密檔案資料。KMS 也會用相關的加密區鑰匙去加密資料加密鑰匙,而加密過的 DEK 就會在 NameNode 伺服器裡面被存作一種檔案屬性。當要讀取檔案時,HDFS客戶端在開啟檔案時會接收到加密過的 DEK,要求 KMS 去解密,然後使用這把 DEK 去解密檔案資料。透過客戶端加密,資料會在傳送時和儲存時都被加密。

LinkedIn 有自己管理的鑰匙管理服務,LiKMS,是唯一一個合法且被認可用來內部管理密碼鑰匙和機密的服務。我們使用了 HDFS 所支援如 KeyProider 的可插拔介面將 LiKMS 與具透明性的靜態加密整合在一起。

靜態加密的主要目標是要在永久性儲存層中從未授權的存取保護個人和機密資料,有幾種方法可以滿足這個目標。

  • 作業系統層級上的整體磁碟加密似乎是最直觀的方式。不過雖然它能從未授權的存取到硬體設備上提供足夠的保護,透過 HDFS 被存取時,資料人就是透明的。
  • 應用層級加密的缺點包括缺乏合一性(non-uniformity):每個團隊需要在他們系統中實作相同的密碼學技術。它也傳播了資料產生者和資料消費者到應用程式間鑰匙共享的問題。
  • 欄位層級加密在所儲存的資料集中鎖定了包含機敏資訊的特定欄位。這種細度顆粒的加密很有效也方便,被鎖定要加密的欄位通常會在資料集綱要裡透過註釋被標註。然而,這種作法被證實很容易出錯。隨著欄位綱要進化,人為疏失會造成不恰當的註釋進而導致機密資料非有心的洩漏。

我們選擇了資料集層級的加密,它保護了整份可能含有機密或私人資訊的資料集。這個方法使用了HDFS 靜態加密與 LiKMS 並且針對所有主要威脅模型一視同仁地去保護所有應用程式。

蟲洞(Wormhole)

有許多在 HDFS 上產生的資料至終需要被傳送到其它環境裡面,像是處理會員查詢語法的線上服務。這份資料可能屬於機器學習模型、搜尋索引等等。幾年前,我們注意到了這些來自不同團隊執行這類傳送的管道激增了。不同的團隊都會有多種不同的方法,其中一個是使用像是 BitTorrent 的端到端檔案分享技術。雖然可運作,支援負擔和效能限制都和想像中的差有點大。

這些管道的共同目標是從一座 HDFS 叢集發布資料到一個地理分佈式服務的多台實體中。我們建立了蟲洞(Wormhole),一個可以促進這種通用模式的單一程式庫。一名使用者只需要從他們離線的工作中簡單地執行 PUSH 到蟲洞裡面然後從他們線上 app 的每一個實體執行一個 FETCH 就可以了。資料傳送的細節都被隱藏到了蟲洞裡面。

圖四、蟲洞簡化了從 HDFS 到線上服務的資料傳送流程

蟲洞有兩個主要成分來簡化資料傳輸。首先,我們利用位於每座資料中心裡較小的 HDFS 叢集,稱作投遞箱叢集(drop box cluster)。當數據被推送到 蟲洞裡時,我們將它複製到要求資料的服務所在的資料中心裡設置的投遞箱裡面。服務實體,每座資料中心裡可能達到數百個,就會從位於同一位置的投遞箱叢集中取得資料,而不是通過資料中心間的網絡獲得資料。這最大限度地減少了跨資料中心的流量,並在服務獲得資料時保持了低延遲。第二個要素是利用 Apache Gobblin 執行快速分散式複製。 Gobblin 能決定適當的平行等級,處理失敗場景,並在需要時重新嘗試。

除了上述用於高效資料傳輸的基礎設施外,蟲洞還具有一個抽象層,能幫助使用者管理駐留在其中的資料。這層 shim 為資料集提供了簡單的版本控制。 蟲洞也會將相關的資料集組合成我們稱為命名空間的集合。訪問控制可以在命名空間或資料集層級完成。構建蟲洞極大地改善了需要從 HDFS 獲取資料的線上服務的入門體驗。它提供了一個單一的管道,我們可以在其中投入足夠的資源來強化和優化。作為抽象層的蟲洞以最低限度的使用者參與給了我們對資料集交換底層儲存系統的彈性,而它也是我們入雲之旅的關鍵。

感謝

打造出艾位元組規模的基礎設施涵括了許多團隊和個人在建立新功能和使用情境,以及在這方面的支持與發展的持續努力,向所有人至上大大的敬意。我們想要謝謝每一位檢視和幫助改善這篇文章的:Ganesh AnathakrishnanSunitha BeeramKeqiu HuJonathan HungVirajith Jalaparti、和 Erik Krogen

Photo by Umer Sayyam on Unsplash

--

--

Scott Hsieh (史考特)
Scott Hsieh (史考特)

Written by Scott Hsieh (史考特)

10 x AWS-certified, Data Architect in the 104 Corporation. An AWS Data Hero

No responses yet