用大白話告訴你小白都能看懂的Hadoop架構原理

架構師springboot發表於2019-01-26

Hadoop 是目前大資料領域最主流的一套技術體系,包含了多種技術,例如 HDFS(分散式檔案系統),YARN(分散式資源排程系統),MapReduce(分散式計算系統)等等。

有些朋友可能聽說過 Hadoop,但是卻不太清楚它到底是個什麼東西,這篇文章就用大白話給各位闡述一下。

假如你現在公司裡的資料都是放在 MySQL 裡的,那麼就全部放在一臺資料庫伺服器上,我們就假設這臺伺服器的磁碟空間有 2T 吧,大家先看下面這張圖。

現在問題來了,你不停的往這臺伺服器的 MySQL 裡放資料,結果資料量越來越大了,超過了 2T 的大小了,現在咋辦?

你說,我可以搞多臺 MySQL 資料庫伺服器,分庫分表啊!每臺伺服器放一部分資料不就得了。如上圖所示!

好,沒問題,那我們們搞 3 臺資料庫伺服器,3 個 MySQL 例項,然後每臺伺服器都可以 2T 的資料。

現在我問你一個問題,所謂的大資料是在幹什麼?我們來說一下大資料最初級的一個使用場景。

假設你有一個電商網站,要把這個電商網站裡所有的使用者在頁面和 App 上的點選、購買、瀏覽的行為日誌都存放起來分析。

你現在把這些資料全都放在了 3 臺 MySQL 伺服器上,資料量很大,但還是勉強可以放的下。

某天早上,你的 Boss 來了。要看一張報表,比如要看每天網站的 X 指標、Y 指標、Z 指標,等等,二三十個資料指標。

好了,兄弟,現在你嘗試去從那些點選、購買、瀏覽的日誌裡,通過寫一個 SQL 來分析出那二三十個指標試試看?

我跟你打賭,你絕對會寫出來一個幾百行起步,甚至上千行的超級複雜大 SQL。這個 SQL,你覺得他能執行在分庫分表後的 3 臺 MySQL 伺服器上麼?

如果你覺得可以的話,那你一定是不太瞭解 MySQL 分庫分表後有多坑,幾百行的大 SQL 跨庫 Join,各種複雜的計算,根本不現實。

所以說,大資料的儲存和計算壓根兒不是靠 MySQL 來搞的,因此 Hadoop、Spark 等大資料技術體系才應運而生。

本質上,Hadoop、Spark 等大資料技術,其實就是一系列的分散式系統。比如 Hadoop 中的 HDFS,就是大資料技術體系中的核心基石,負責分散式儲存資料,這是啥意思?別急,繼續往下看。

HDFS 全稱是 Hadoop Distributed File System,是 Hadoop 的分散式檔案系統。

它由很多機器組成,每臺機器上執行一個 DataNode 程式,負責管理一部分資料。

然後有一臺機器上執行了 NameNode 程式,NameNode 大致可以認為是負責管理整個 HDFS 叢集的這麼一個程式,它裡面儲存了 HDFS 叢集的所有後設資料。

然後有很多臺機器,每臺機器儲存一部分資料!好,HDFS 現在可以很好的儲存和管理大量的資料了。

這時候你肯定會有疑問:MySQL 伺服器不也是這樣的嗎?你要是這樣想,那就大錯特錯了。

這個事情不是你想的那麼簡單的,HDFS 天然就是分散式的技術,所以你上傳大量資料,儲存資料,管理資料,天然就可以用 HDFS 來做。

如果你硬要基於 MySQL 分庫分表這個事兒,會痛苦很多倍,因為 MySQL 並不是設計為分散式系統架構的,它在分散式資料儲存這塊缺乏很多資料保障的機制。

好,你現在用 HDFS 分散式儲存了資料,接著不就是要分散式來計算這些資料了嗎?

對於分散式計算:

  • 很多公司用 Hive 寫幾百行的大 SQL(底層基於 MapReduce)。

  • 也有很多公司開始慢慢的用 Spark 寫幾百行的大 SQL(底層是 Spark Core 引擎)。

總之就是寫一個大 SQL,然後拆分為很多的計算任務,放到各個機器上去,每個計算任務就負責計算一小部分資料,這就是所謂的分散式計算。

這個,絕對比你針對分庫分表的 MySQL 來跑幾百行大 SQL 要靠譜的多。

對於上述所說的分散式儲存與分散式計算,老規矩,同樣給大家來一張圖,大夥兒跟著圖來仔細捋一下整個過程。

HDFS 的 NameNode 架構原理

好了,前奏鋪墊完之後,進入正題。本文主要就是討論一下 HDFS 叢集中的 NameNode 的核心架構原理。

NameNode 有一個很核心的功能:管理整個 HDFS 叢集的後設資料,比如說檔案目錄樹、許可權的設定、副本數的設定,等等。

下面就用最典型的檔案目錄樹的維護,來給大家舉例說明,我們看看下面的圖。現在有一個客戶端系統要上傳一個 1TB 的大檔案到 HDFS 叢集裡

此時它會先跟 NameNode 通訊,說:大哥,我想建立一個新的檔案,它的名字叫“/usr/hive/warehouse/access_20180101.log”,大小是 1TB,你看行不?

然後 NameNode 就會在自己記憶體的檔案目錄樹裡,在指定的目錄下搞一個新的檔案物件,名字就是“access_20180101.log”。

這個檔案目錄樹不就是 HDFS 非常核心的一塊後設資料,維護了 HDFS 這個分散式檔案系統中,有哪些目錄,有哪些檔案,對不對?

但是有個問題,這個檔案目錄樹是在 NameNode 的記憶體裡的啊!這可坑爹了,你把重要的後設資料都放在記憶體裡,萬一 NameNode 不小心當機了可咋整?後設資料不就全部丟失了?

可你要是每次都頻繁的修改磁碟檔案裡的後設資料,效能肯定是極低的啊!畢竟這是大量的磁碟隨機讀寫!

沒關係,我們來看看 HDFS 優雅的解決方案。每次記憶體裡改完了,寫一條 edits log,後設資料修改的操作日誌存到磁碟檔案裡,不修改磁碟檔案內容,就是順序追加,這個效能就高多了。

每次 NameNode 重啟的時候,把 edits log 裡的操作日誌讀到記憶體裡回放一下,不就可以恢復後設資料了?

大家順著上面的文字,把整個過程,用下面這張圖跟著走一遍:

但是問題又來了,那 edits log 如果越來越大的話,豈不是每次重啟都會很慢?因為要讀取大量的 edits log 回放恢復後設資料!

所以 HDFS 說,我可以這樣子啊,我引入一個新的磁碟檔案叫做 fsimage,然後呢,再引入一個 JournalNodes 叢集,以及一個 Standby NameNode(備節點)。

每次 Active NameNode(主節點)修改一次後設資料都會生成一條 edits log,除了寫入本地磁碟檔案,還會寫入 JournalNodes 叢集。

然後 Standby NameNode 就可以從 JournalNodes 叢集拉取 edits log,應用到自己記憶體的檔案目錄樹裡,跟 Active NameNode 保持一致。

然後每隔一段時間,Standby NameNode 都把自己記憶體裡的檔案目錄樹寫一份到磁碟上的 fsimage,這可不是日誌,這是完整的一份後設資料。這個操作就是所謂的 checkpoint 檢查點操作。

然後把這個 fsimage 上傳到 Active NameNode,接著清空掉 Active NameNode 的舊的 edits log 檔案,這裡可能都有 100 萬行修改日誌了!

然後 Active NameNode 繼續接收修改後設資料的請求,再寫入 edits log,寫了一小會兒,這裡可能就幾十行修改日誌而已!

如果說此時,Active NameNode 重啟了,Bingo!沒關係,只要把 Standby NameNode 傳過來的 fsimage 直接讀到記憶體裡,這個 fsimage 直接就是後設資料,不需要做任何額外操作,純讀取,效率很高!

然後把新的 edits log 裡少量的幾十行的修改日誌回放到記憶體裡就 OK 了!

這個過程的啟動速度就快的多了!因為不需要回放大量上百萬行的 edits log 來恢復後設資料了!如下圖所示。

此外,大家看看上面這張圖,現在我們們有倆 NameNode:

  • 一個是主節點對外提供服務接收請求。
  • 另外一個純就是接收和同步主節點的 edits log 以及執行定期 checkpoint 的備節點。

大家有沒有發現!他們倆記憶體裡的後設資料幾乎是一模一樣的啊!所以呢,如果 Active NameNode 掛了,是不是可以立馬切換成 Standby NameNode 對外提供服務?

這不就是所謂的 NameNode 主備高可用故障轉移機制麼!接下來大家再想想,HDFS 客戶端在 NameNode 記憶體裡的檔案目錄樹,新加了一個檔案。

但是這個時候,人家要把資料上傳到多臺 DataNode 機器上去啊,這可是一個 1TB 的大檔案!咋傳呢?

很簡單,把 1TB 的大檔案拆成 N 個 block,每個 block 是 128MB。1TB = 1024GB = 1048576MB,一個 block 是 128MB,那麼就是對應著 8192 個 block。

這些 block 會分佈在不同的機器上管理著,比如說一共有 100 臺機器組成的叢集,那麼每臺機器上放 80 個左右的 block 就 OK 了。

但是問題又來了,那如果這個時候 1 臺機器當機了,不就導致 80 個 block 丟失了?

也就是說上傳上去的 1TB 的大檔案,會丟失一小部分資料啊。沒關係!HDFS 都考慮好了!

它會預設給每個 block 搞 3 個副本,一模一樣的副本,分放在不同的機器上,如果一臺機器當機了,同一個 block 還有另外兩個副本在其他機器上呢!

大夥兒看看下面這張圖。每個 block 都在不同的機器上有 3 個副本,任何一臺機器當機都沒事!還可以從其他的機器上拿到那個 block。

這下子,你往 HDFS 上傳一個 1TB 的大檔案,可以高枕無憂了吧!

OK,上面就是大白話加上一系列手繪圖,給大家先聊聊小白都能聽懂的 Hadoop 的基本架構原理。

大規模叢集下 Hadoop NameNode 如何承載每秒上千次的高併發訪問

上面我們已經初步給大家解釋了 Hadoop HDFS 的整體架構原理,相信大家都有了一定的認識和了解。

下面我們來看看,如果大量客戶端對 NameNode 發起高併發(比如每秒上千次)訪問來修改後設資料,此時 NameNode 該如何抗住?

問題源起

我們先來分析一下,高併發請求 NameNode 會遇到什麼樣的問題。

大家現在都知道了,每次請求 NameNode 修改一條後設資料(比如說申請上傳一個檔案,那麼就需要在記憶體目錄樹中加入一個檔案),都要寫一條 edits log。

包括如下兩個步驟:

  • 寫入本地磁碟。
  • 通過網路傳輸給 JournalNodes 叢集。

但是如果對 Java 有一定了解的同學都該知道多執行緒併發安全問題吧?

NameNode 在寫 edits log 時的第一條原則:必須保證每條 edits log 都有一個全域性順序遞增的 transactionId(簡稱為 txid),這樣才可以標識出來一條一條的 edits log 的先後順序。

那麼如果要保證每條 edits log 的 txid 都是遞增的,就必須得加鎖。

每個執行緒修改了後設資料,要寫一條 edits log 的時候,都必須按順序排隊獲取鎖後,才能生成一個遞增的 txid,代表這次要寫的 edits log 的序號。

好了,那麼問題來了,大家看看下面的圖。如果每次都是在一個加鎖的程式碼塊裡,生成 txid,然後寫磁碟檔案 edits log,網路請求寫入 JournalNodes 一條 edits log,會咋樣?

不用說,這個絕對完蛋了!NameNode 本身用多執行緒接收多個客戶端傳送過來的併發的請求,結果多個執行緒居然修改完記憶體中的後設資料之後,排著隊寫 edits log!

而且你要知道,寫本地磁碟 + 網路傳輸給 JournalNodes,都是很耗時的啊!效能兩大殺手:磁碟寫 + 網路寫!

如果 HDFS 的架構真要是這麼設計的話,基本上 NameNode 能承載的每秒的併發數量就很少了,可能就每秒處理幾十個併發請求處理撐死了!

HDFS 優雅的解決方案

所以說,針對這個問題,人家 HDFS 是做了不少的優化的!

首先大家想一下,既然我們們不希望每個執行緒寫 edits log 的時候,序列化排隊生成 txid + 寫磁碟 + 寫 JournalNode,那麼是不是可以搞一個記憶體緩衝?

也就是說,多個執行緒可以快速的獲取鎖,生成 txid,然後快速的將 edits log 寫入記憶體緩衝。

接著就快速的釋放鎖,讓下一個執行緒繼續獲取鎖後,生成 id + 寫 edits log 進入記憶體緩衝。

然後接下來有一個執行緒可以將記憶體中的 edits log 刷入磁碟,但是在這個過程中,還是繼續允許其他執行緒將 edits log 寫入記憶體緩衝中。

但是這裡又有一個問題了,如果針對同一塊記憶體緩衝,同時有人寫入,還同時有人讀取後寫磁碟,那也有問題,因為不能併發讀寫一塊共享記憶體資料!

所以 HDFS 在這裡採取了 double-buffer 雙緩衝機制來處理!將一塊記憶體緩衝分成兩個部分:

  • 其中一個部分可以寫入。
  • 另外一個部分用於讀取後寫入磁碟和 JournalNodes。

大家可能感覺文字敘述不太直觀,老規矩,我們們來一張圖,按順序給大家闡述一下。

①分段加鎖機制 + 記憶體雙緩衝機制

首先各個執行緒依次第一次獲取鎖,生成順序遞增的 txid,然後將 edits log 寫入記憶體雙緩衝的區域 1,接著就立馬第一次釋放鎖了。

趁著這個空隙,後面的執行緒就可以再次立馬第一次獲取鎖,然後立即寫自己的 edits log 到記憶體緩衝。

寫記憶體那麼快,可能才耗時幾十微妙,接著就立馬第一次釋放鎖了。所以這個併發優化絕對是有效果的,大家有沒有感受到?

接著各個執行緒競爭第二次獲取鎖,有執行緒獲取到鎖之後,就看看,有沒有誰在寫磁碟和網路?

如果沒有,好,那麼這個執行緒是個幸運兒!直接交換雙緩衝的區域 1 和區域 2,接著第二次釋放鎖。這個過程相當快速,記憶體裡判斷幾個條件,耗時不了幾微秒。

好,到這一步為止,記憶體緩衝已經被交換了,後面的執行緒可以立馬快速的依次獲取鎖,然後將 edits log 寫入記憶體緩衝的區域 2,區域 1 中的資料被鎖定了,不能寫。

怎麼樣,是不是又感受到了一點點多執行緒併發的優化?

②多執行緒併發吞吐量的百倍優化

接著,之前那個幸運兒執行緒,將記憶體緩衝的區域 1 中的資料讀取出來(此時沒人寫區域 1 了,都在寫區域 2),將裡面的 edtis log 都寫入磁碟檔案,以及通過網路寫入 JournalNodes 叢集。

這個過程可是很耗時的!但是沒關係啊,人家做過優化了,在寫磁碟和網路的過程中,是不持有鎖的!

因此後面的執行緒可以噼裡啪啦的快速的第一次獲取鎖後,立馬寫入記憶體緩衝的區域 2,然後釋放鎖。

這個時候大量的執行緒都可以快速的寫入記憶體,沒有阻塞和卡頓!怎麼樣?併發優化的感覺感受到了沒有!

③緩衝資料批量刷磁碟 + 網路的優化

那麼在幸運兒執行緒吭哧吭哧把資料寫磁碟和網路的過程中,排在後面的大量執行緒,快速的第一次獲取鎖,寫記憶體緩衝區域 2,釋放鎖,之後,這些執行緒第二次獲取到鎖後會幹嘛?

他們會發現有人在寫磁碟啊,兄弟們!所以會立即休眠 1 秒,釋放鎖。

此時大量的執行緒併發過來的話,都會在這裡快速的第二次獲取鎖,然後發現有人在寫磁碟和網路,快速的釋放鎖,休眠。

怎麼樣,這個過程沒有人長時間的阻塞其他人吧!因為都會快速的釋放鎖,所以後面的執行緒還是可以迅速的第一次獲取鎖後寫記憶體緩衝!

Again!併發優化的感覺感受到了沒有?

而且這時,一定會有很多執行緒發現,好像之前那個幸運兒執行緒的 txid 是排在自己之後的,那麼肯定就把自己的 edits log 從緩衝裡寫入磁碟和網路了。

這些執行緒甚至都不會休眠等待,直接就會返回後去幹別的事情了,壓根兒不會卡在這裡。這裡又感受到併發的優化沒有?

然後那個幸運兒執行緒寫完磁碟和網路之後,就會喚醒之前休眠的那些執行緒。

那些執行緒會依次排隊再第二次獲取鎖後進入判斷,咦!發現沒有人在寫磁碟和網路了!

然後就會再判斷,有沒有排在自己之後的執行緒已經將自己的 edtis log 寫入磁碟和網路了:

  • 如果有的話,就直接返回了。
  • 沒有的話,那麼就成為第二個幸運兒執行緒,交換兩塊緩衝區,區域 1 和區域 2 交換一下。
  • 然後釋放鎖,自己開始吭哧吭哧的將區域 2 的資料寫入磁碟和網路。

但是這個時候沒有關係啊,後面的執行緒如果要寫 edits log 的,還是可以第一次獲取鎖後立馬寫記憶體緩衝再釋放鎖。以此類推。

總結

這套機制還是挺複雜的,涉及到了分段加鎖以及記憶體雙緩衝兩個機制。

通過這套機制,NameNode 保證了多個執行緒在高併發的修改後設資料之後寫 edits log 的時候,不會說一個執行緒一個執行緒的寫磁碟和網路,那樣效能實在太差,併發能力太弱了!

所以通過上述那套複雜的機制,盡最大的努力保證,一個執行緒可以批量的將一個緩衝中的多條 edits log 刷入磁碟和網路。

在這個漫長的吭哧吭哧的過程中,其他的執行緒可以快速的高併發寫入 edits log 到記憶體緩衝裡,不會阻塞其他的執行緒寫 edits log。

所以,正是依靠以上機制,最大限度優化了 NameNode 處理高併發訪問修改後設資料的能力!

感興趣的可以自己來我的Java架構群,可以獲取免費的學習資料,群號:855801563對Java技術,架構技術感興趣的同學,歡迎加群,一起學習,相互討論。



相關文章