關(guān)于Hadoop技術(shù)的HA機(jī)制學(xué)習(xí)
導(dǎo)語(yǔ)
最近分享過(guò)一次關(guān)于Hadoop技術(shù)主題的演講,由于接觸時(shí)間不長(zhǎng),很多技術(shù)細(xì)節(jié)認(rèn)識(shí)不夠,也沒講清楚,作為一個(gè)技術(shù)人員,本著追根溯源的精神,還是有必要吃透,也為自己的工作沉淀一些經(jīng)驗(yàn)總結(jié)。網(wǎng)上關(guān)于Hadoop HA的資料多集中于怎么搭建HA,對(duì)于HA為什么要這么做描述甚少,所以本文對(duì)于HA是如何搭建的暫不介紹,主要是介紹HA是怎么運(yùn)作,QJM又是怎么發(fā)揮功效的。
一、Hadoop 系統(tǒng)架構(gòu)
1.1 Hadoop1.x和Hadoop2.x 架構(gòu)
在介紹HA之前,我們先來(lái)看下Hadoop的系統(tǒng)架構(gòu),這對(duì)于理解HA是至關(guān)重要的。Hadoop 1.x之前,其官方架構(gòu)如圖1所示:
[ 圖1.Hadoop 1.x架構(gòu)圖 ]
從圖中可看出,1.x版本之前只有一個(gè)Namenode,所有元數(shù)據(jù)由惟一的Namenode負(fù)責(zé)管理,可想而之當(dāng)這個(gè)NameNode掛掉時(shí)整個(gè)集群基本也就不可用。
Hadoop 2.x的架構(gòu)與1.x有什么區(qū)別呢。我們來(lái)看下2.x的架構(gòu):
[ 圖2.Hadoop 2.x架構(gòu)圖 ]
2.x版本中,HDFS架構(gòu)解決了單點(diǎn)故障問(wèn)題,即引入雙NameNode架構(gòu),同時(shí)借助共享存儲(chǔ)系統(tǒng)來(lái)進(jìn)行元數(shù)據(jù)的同步,共享存儲(chǔ)系統(tǒng)類型一般有幾類,如:Shared NAS+NFS、BookKeeper、BackupNode 和 Quorum Journal Manager(QJM),上圖中用的是QJM作為共享存儲(chǔ)組件,通過(guò)搭建奇數(shù)結(jié)點(diǎn)的JournalNode實(shí)現(xiàn)主備NameNode元數(shù)據(jù)操作信息同步。Hadoop的元數(shù)據(jù)包括哪些信息呢,下面介紹下關(guān)于元數(shù)據(jù)方面的知識(shí)。
1.2 Hadoop 2.x元數(shù)據(jù)
Hadoop的元數(shù)據(jù)主要作用是維護(hù)HDFS文件系統(tǒng)中文件和目錄相關(guān)信息。元數(shù)據(jù)的存儲(chǔ)形式主要有3類:內(nèi)存鏡像、磁盤鏡像(FSImage)、日志(EditLog)。在Namenode啟動(dòng)時(shí),會(huì)加載磁盤鏡像到內(nèi)存中以進(jìn)行元數(shù)據(jù)的管理,存儲(chǔ)在NameNode內(nèi)存;磁盤鏡像是某一時(shí)刻HDFS的元數(shù)據(jù)信息的快照,包含所有相關(guān)Datanode節(jié)點(diǎn)文件塊映射關(guān)系和命名空間(Namespace)信息,存儲(chǔ)在NameNode本地文件系統(tǒng);日志文件記錄client發(fā)起的每一次操作信息,即保存所有對(duì)文件系統(tǒng)的修改操作,用于定期和磁盤鏡像合并成***鏡像,保證NameNode元數(shù)據(jù)信息的完整,存儲(chǔ)在NameNode本地和共享存儲(chǔ)系統(tǒng)(QJM)中。
如下所示為NameNode本地的EditLog和FSImage文件格式,EditLog文件有兩種狀態(tài): inprocess和finalized, inprocess表示正在寫的日志文件,文件名形式:editsinprocess[start-txid],finalized表示已經(jīng)寫完的日志文件,文件名形式:edits[start-txid][end-txid]; FSImage文件也有兩種狀態(tài), finalized和checkpoint, finalized表示已經(jīng)持久化磁盤的文件,文件名形式: fsimage_[end-txid], checkpoint表示合并中的fsimage, 2.x版本checkpoint過(guò)程在Standby Namenode(SNN)上進(jìn)行,SNN會(huì)定期將本地FSImage和從QJM上拉回的ANN的EditLog進(jìn)行合并,合并完后再通過(guò)RPC傳回ANN。
- data/hbase/runtime/namespace
- ├── current
- │ ├── VERSION
- │ ├── edits_0000000003619794209-0000000003619813881
- │ ├── edits_0000000003619813882-0000000003619831665
- │ ├── edits_0000000003619831666-0000000003619852153
- │ ├── edits_0000000003619852154-0000000003619871027
- │ ├── edits_0000000003619871028-0000000003619880765
- │ ├── edits_0000000003619880766-0000000003620060869
- │ ├── edits_inprogress_0000000003620060870
- │ ├── fsimage_0000000003618370058
- │ ├── fsimage_0000000003618370058.md5
- │ ├── fsimage_0000000003620060869
- │ ├── fsimage_0000000003620060869.md5
- │ └── seen_txid
- └── in_use.lock
上面所示的還有一個(gè)很重要的文件就是seen_txid,保存的是一個(gè)事務(wù)ID,這個(gè)事務(wù)ID是EditLog***的一個(gè)結(jié)束事務(wù)id,當(dāng)NameNode重啟時(shí),會(huì)順序遍歷從edits_0000000000000000001到seen_txid所記錄的txid所在的日志文件,進(jìn)行元數(shù)據(jù)恢復(fù),如果該文件丟失或記錄的事務(wù)ID有問(wèn)題,會(huì)造成數(shù)據(jù)塊信息的丟失。
HA其本質(zhì)上就是要保證主備NN元數(shù)據(jù)是保持一致的,即保證fsimage和editlog在備NN上也是完整的。元數(shù)據(jù)的同步很大程度取決于EditLog的同步,而這步驟的關(guān)鍵就是共享文件系統(tǒng),下面開始介紹一下關(guān)于QJM共享存儲(chǔ)機(jī)制。
二、QJM原理
2.1 QJM背景
在QJM出現(xiàn)之前,為保障集群的HA,設(shè)計(jì)的是一種基于NAS的共享存儲(chǔ)機(jī)制,即主備NameNode間通過(guò)NAS進(jìn)行元數(shù)據(jù)的同步。該方案有什么缺點(diǎn)呢,主要有以下幾點(diǎn):
- 定制化硬件設(shè)備:必須是支持NAS的設(shè)備才能滿足需求
- 復(fù)雜化部署過(guò)程:在部署好NameNode后,還必須額外配置NFS掛載、定制隔離腳本,部署易出錯(cuò)
- 簡(jiǎn)陋化NFS客戶端:Bug多,部署配置易出錯(cuò),導(dǎo)致HA不可用
所以對(duì)于替代方案而言,也必須解決NAS相關(guān)缺陷才能讓HA更好服務(wù)。即設(shè)備無(wú)須定制化,普通設(shè)備即可配置HA,部署簡(jiǎn)單,相關(guān)配置集成到系統(tǒng)本身,無(wú)需自己定制,同時(shí)元數(shù)據(jù)的同步也必須保證完全HA,不會(huì)因client問(wèn)題而同步失敗。
2.2 QJM原理
2.2.1 QJM介紹
QJM全稱是Quorum Journal Manager, 由JournalNode(JN)組成,一般是奇數(shù)點(diǎn)結(jié)點(diǎn)組成。每個(gè)JournalNode對(duì)外有一個(gè)簡(jiǎn)易的RPC接口,以供NameNode讀寫EditLog到JN本地磁盤。當(dāng)寫EditLog時(shí),NameNode會(huì)同時(shí)向所有JournalNode并行寫文件,只要有N/2+1結(jié)點(diǎn)寫成功則認(rèn)為此次寫操作成功,遵循Paxos協(xié)議。其內(nèi)部實(shí)現(xiàn)框架如下:
[ 圖3.QJM內(nèi)部實(shí)現(xiàn)框架 ]
從圖中可看出,主要是涉及EditLog的不同管理對(duì)象和輸出流對(duì)象,每種對(duì)象發(fā)揮著各自不同作用:
- FSEditLog:所有EditLog操作的入口
- JournalSet: 集成本地磁盤和JournalNode集群上EditLog的相關(guān)操作
- FileJournalManager: 實(shí)現(xiàn)本地磁盤上 EditLog 操作
- QuorumJournalManager: 實(shí)現(xiàn)JournalNode 集群EditLog操作
- AsyncLoggerSet: 實(shí)現(xiàn)JournalNode 集群 EditLog 的寫操作集合
- AsyncLogger:發(fā)起RPC請(qǐng)求到JN,執(zhí)行具體的日志同步功能
- JournalNodeRpcServer:運(yùn)行在 JournalNode 節(jié)點(diǎn)進(jìn)程中的 RPC 服務(wù),接收 NameNode 端的 AsyncLogger 的 RPC 請(qǐng)求。
- JournalNodeHttpServer:運(yùn)行在 JournalNode 節(jié)點(diǎn)進(jìn)程中的 Http 服務(wù),用于接收處于 Standby 狀態(tài)的 NameNode 和其它 JournalNode 的同步 EditLog 文件流的請(qǐng)求。
下面具體分析下QJM的讀寫過(guò)程。
2.2.2 QJM 寫過(guò)程分析
上面提到EditLog,NameNode會(huì)把EditLog同時(shí)寫到本地和JournalNode。寫本地由配置中參數(shù)dfs.namenode.name.dir控制,寫JN由參數(shù)dfs.namenode.shared.edits.dir控制,在寫EditLog時(shí)會(huì)由兩個(gè)不同的輸出流來(lái)控制日志的寫過(guò)程,分別為:EditLogFileOutputStream(本地輸出流)和QuorumOutputStream(JN輸出流)。寫EditLog也不是直接寫到磁盤中,為保證高吞吐,NameNode會(huì)分別為EditLogFileOutputStream和QuorumOutputStream定義兩個(gè)同等大小的Buffer,大小大概是512KB,一個(gè)寫B(tài)uffer(buffCurrent),一個(gè)同步Buffer(buffReady),這樣可以一邊寫一邊同步,所以EditLog是一個(gè)異步寫過(guò)程,同時(shí)也是一個(gè)批量同步的過(guò)程,避免每寫一筆就同步一次日志。
這個(gè)是怎么實(shí)現(xiàn)邊寫邊同步的呢,這中間其實(shí)是有一個(gè)緩沖區(qū)交換的過(guò)程,即bufferCurrent和buffReady在達(dá)到條件時(shí)會(huì)觸發(fā)交換,如bufferCurrent在達(dá)到閾值同時(shí)bufferReady的數(shù)據(jù)又同步完時(shí),bufferReady數(shù)據(jù)會(huì)清空,同時(shí)會(huì)將bufferCurrent指針指向bufferReady以滿足繼續(xù)寫,另外會(huì)將bufferReady指針指向bufferCurrent以提供繼續(xù)同步EditLog。上面過(guò)程用流程圖就是表示如下:
[ 圖4.EditLog輸出流程圖 ]
這里有一個(gè)問(wèn)題,既然EditLog是異步寫的,怎么保證緩存中的數(shù)據(jù)不丟呢,其實(shí)這里雖然是異步,但實(shí)際所有日志都需要通過(guò)logSync同步成功后才會(huì)給client返回成功碼,假設(shè)某一時(shí)刻N(yùn)ameNode不可用了,其內(nèi)存中的數(shù)據(jù)其實(shí)是未同步成功的,所以client會(huì)認(rèn)為這部分?jǐn)?shù)據(jù)未寫成功。
第二個(gè)問(wèn)題是,EditLog怎么在多個(gè)JN上保持一致的呢。下面展開介紹。
1.隔離雙寫:
在ANN每次同步EditLog到JN時(shí),先要保證不會(huì)有兩個(gè)NN同時(shí)向JN同步日志。這個(gè)隔離是怎么做的。這里面涉及一個(gè)很重要的概念Epoch Numbers,很多分布式系統(tǒng)都會(huì)用到。Epoch有如下幾個(gè)特性:
- 當(dāng)NN成為活動(dòng)結(jié)點(diǎn)時(shí),其會(huì)被賦予一個(gè)EpochNumber
- 每個(gè)EpochNumber是惟一的,不會(huì)有相同的EpochNumber出現(xiàn)
- EpochNumber有嚴(yán)格順序保證,每次NN切換后其EpochNumber都會(huì)自增1,后面生成的EpochNumber都會(huì)大于前面的EpochNumber
QJM是怎么保證上面特性的呢,主要有以下幾點(diǎn):
- ***步,在對(duì)EditLog作任何修改前,QuorumJournalManager(NameNode上)必須被賦予一個(gè)EpochNumber
- 第二步, QJM把自己的EpochNumber通過(guò)newEpoch(N)的方式發(fā)送給所有JN結(jié)點(diǎn)
- 第三步, 當(dāng)JN收到newEpoch請(qǐng)求后,會(huì)把QJM的EpochNumber保存到一個(gè)lastPromisedEpoch變量中并持久化到本地磁盤
- 第四步, ANN同步日志到JN的任何RPC請(qǐng)求(如logEdits(),startLogSegment()等),都必須包含ANN的EpochNumber
- 第五步,JN在收到RPC請(qǐng)求后,會(huì)將之與lastPromisedEpoch對(duì)比,如果請(qǐng)求的EpochNumber小于lastPromisedEpoch,將會(huì)拒絕同步請(qǐng)求,反之,會(huì)接受同步請(qǐng)求并將請(qǐng)求的EpochNumber保存在lastPromisedEpoch
這樣就能保證主備NN發(fā)生切換時(shí),就算同時(shí)向JN同步日志,也能保證日志不會(huì)寫亂,因?yàn)榘l(fā)生切換后,原ANN的EpochNumber肯定是小于新ANN的EpochNumber,所以原ANN向JN的發(fā)起的所有同步請(qǐng)求都會(huì)拒絕,實(shí)現(xiàn)隔離功能,防止了腦裂。
2. 恢復(fù)in-process日志
為什么要這步呢,如果在寫過(guò)程中寫失敗了,可能各個(gè)JN上的EditLog的長(zhǎng)度都不一樣,需要在開始寫之前將不一致的部分恢復(fù)?;謴?fù)機(jī)制如下:
- ANN先向所有JN發(fā)送getJournalState請(qǐng)求;
- JN會(huì)向ANN返回一個(gè)Epoch(lastPromisedEpoch);
- ANN收到大多數(shù)JN的Epoch后,選擇***的一個(gè)并加1作為當(dāng)前新的Epoch,然后向JN發(fā)送新的newEpoch請(qǐng)求,把新的Epoch下發(fā)給JN;
- JN收到新的Epoch后,和lastPromisedEpoch對(duì)比,若更大則更新到本地并返回給ANN自己本地一個(gè)***EditLogSegment起始事務(wù)Id,若小則返回NN錯(cuò)誤;
- ANN收到多數(shù)JN成功響應(yīng)后認(rèn)為Epoch生成成功,開始準(zhǔn)備日志恢復(fù);
- ANN會(huì)選擇一個(gè)***的EditLogSegment事務(wù)ID作為恢復(fù)依據(jù),然后向JN發(fā)送prepareRecovery; RPC請(qǐng)求,對(duì)應(yīng)Paxos協(xié)議2p階段的Phase1a,若多數(shù)JN響應(yīng)prepareRecovery成功,則可認(rèn)為Phase1a階段成功;
- ANN選擇進(jìn)行同步的數(shù)據(jù)源,向JN發(fā)送acceptRecovery RPC請(qǐng)求,并將數(shù)據(jù)源作為參數(shù)傳給JN。
- JN收到acceptRecovery請(qǐng)求后,會(huì)從JournalNodeHttpServer下載EditLogSegment并替換到本地保存的EditLogSegment,對(duì)應(yīng)Paxos協(xié)議2p階段的Phase1b,完成后返回ANN請(qǐng)求成功狀態(tài)。
- ANN收到多數(shù)JN的響應(yīng)成功請(qǐng)求后,向JN發(fā)送finalizeLogSegment請(qǐng)求,表示數(shù)據(jù)恢復(fù)完成,這樣之后所有JN上的日志就能保持一致。
數(shù)據(jù)恢復(fù)后,ANN上會(huì)將本地處于in-process狀態(tài)的日志更名為finalized狀態(tài)的日志,形式如edits[start-txid][stop-txid]。
3.日志同步
這個(gè)步驟上面有介紹到關(guān)于日志從ANN同步到JN的過(guò)程,具體如下:
- 執(zhí)行l(wèi)ogSync過(guò)程,將ANN上的日志數(shù)據(jù)放到緩存隊(duì)列中
- 將緩存中數(shù)據(jù)同步到JN,JN有相應(yīng)線程來(lái)處理logEdits請(qǐng)求
- JN收到數(shù)據(jù)后,先確認(rèn)EpochNumber是否合法,再驗(yàn)證日志事務(wù)ID是否正常,將日志刷到磁盤,返回ANN成功碼
- ANN收到JN成功請(qǐng)求后返回client寫成功標(biāo)識(shí),若失敗則拋出異常
通過(guò)上面一些步驟,日志能保證成功同步到JN,同時(shí)保證JN日志的一致性,進(jìn)而備NN上同步日志時(shí)也能保證數(shù)據(jù)是完整和一致的。
2.2.3 QJM讀過(guò)程分析
這個(gè)讀過(guò)程是面向備NN(SNN)的,SNN定期檢查JournalNode上EditLog的變化,然后將EditLog拉回本地。SNN上有一個(gè)線程StandbyCheckpointer,會(huì)定期將SNN上FSImage和EditLog合并,并將合并完的FSImage文件傳回主NN(ANN)上,就是所說(shuō)的Checkpointing過(guò)程。下面我們來(lái)看下Checkpointing是怎么進(jìn)行的。
在2.x版本中,已經(jīng)將原來(lái)的由SecondaryNameNode主導(dǎo)的Checkpointing替換成由SNN主導(dǎo)的Checkpointing。下面是一個(gè)CheckPoint的流向圖:
[ 圖5.Checkpointing流向圖 ]
總的來(lái)說(shuō),就是在SNN上先檢查前置條件,前置條件包括兩個(gè)方面:距離上次Checkpointing的時(shí)間間隔和EditLog中事務(wù)條數(shù)限制。前置條件任何一個(gè)滿足都會(huì)觸發(fā)Checkpointing,然后SNN會(huì)將***的NameSpace數(shù)據(jù)即SNN內(nèi)存中當(dāng)前狀態(tài)的元數(shù)據(jù)保存到一個(gè)臨時(shí)的fsimage文件( fsimage.ckpt)然后比對(duì)從JN上拉到的***EditLog的事務(wù)ID,將fsimage.ckpt_中沒有,EditLog中有的所有元數(shù)據(jù)修改記錄合并一起并重命名成新的fsimage文件,同時(shí)生成一個(gè)md5文件。將***的fsimage再通過(guò)HTTP請(qǐng)求傳回ANN。通過(guò)定期合并fsimage有什么好處呢,主要有以下幾個(gè)方面:
可以避免EditLog越來(lái)越大,合并成新fsimage后可以將老的EditLog刪除
可以避免主NN(ANN)壓力過(guò)大,合并是在SNN上進(jìn)行的
可以保證fsimage保存的是一份***的元數(shù)據(jù),故障恢復(fù)時(shí)避免數(shù)據(jù)丟失
三、主備切換機(jī)制
要完成HA,除了元數(shù)據(jù)同步外,還得有一個(gè)完備的主備切換機(jī)制,Hadoop的主備選舉依賴于ZooKeeper。下面是主備切換的狀態(tài)圖:
[ 圖6.Failover流程圖 ]
從圖中可以看出,整個(gè)切換過(guò)程是由ZKFC來(lái)控制的,具體又可分為HealthMonitor、ZKFailoverController和ActiveStandbyElector三個(gè)組件。
- ZKFailoverController: 是HealthMontior和ActiveStandbyElector的母體,執(zhí)行具體的切換操作
- HealthMonitor: 監(jiān)控NameNode健康狀態(tài),若狀態(tài)異常會(huì)觸發(fā)回調(diào)ZKFailoverController進(jìn)行自動(dòng)主備切換
- ActiveStandbyElector: 通知ZK執(zhí)行主備選舉,若ZK完成變更,會(huì)回調(diào)ZKFailoverController相應(yīng)方法進(jìn)行主備狀態(tài)切換
在故障切換期間,ZooKeeper主要是發(fā)揮什么作用呢,有以下幾點(diǎn):
- 失敗保護(hù):集群中每一個(gè)NameNode都會(huì)在ZooKeeper維護(hù)一個(gè)持久的session,機(jī)器一旦掛掉,session就會(huì)過(guò)期,故障遷移就會(huì)觸發(fā)
- Active NameNode選擇:ZooKeeper有一個(gè)選擇ActiveNN的機(jī)制,一旦現(xiàn)有的ANN宕機(jī),其他NameNode可以向ZooKeeper申請(qǐng)排他成為下一個(gè)Active節(jié)點(diǎn)
- 防腦裂: ZK本身是強(qiáng)一致和高可用的,可以用它來(lái)保證同一時(shí)刻只有一個(gè)活動(dòng)節(jié)點(diǎn)
那在哪些場(chǎng)景會(huì)觸發(fā)自動(dòng)切換呢,從HDFS-2185中歸納了以下幾個(gè)場(chǎng)景:
- ActiveNN JVM奔潰:ANN上HealthMonitor狀態(tài)上報(bào)會(huì)有連接超時(shí)異常,HealthMonitor會(huì)觸發(fā)狀態(tài)遷移至SERVICE_NOT_RESPONDING, 然后ANN上的ZKFC會(huì)退出選舉,SNN上的ZKFC會(huì)獲得Active Lock, 作相應(yīng)隔離后成為Active結(jié)點(diǎn)。
- ActiveNN JVM凍結(jié):這個(gè)是JVM沒奔潰,但也無(wú)法響應(yīng),同奔潰一樣,會(huì)觸發(fā)自動(dòng)切換。
- ActiveNN 機(jī)器宕機(jī):此時(shí)ActiveStandbyElector會(huì)失去同ZK的心跳,會(huì)話超時(shí),SNN上的ZKFC會(huì)通知ZK刪除ANN的活動(dòng)鎖,作相應(yīng)隔離后完成主備切換。
- ActiveNN 健康狀態(tài)異常: 此時(shí)HealthMonitor會(huì)收到一個(gè)HealthCheckFailedException,并觸發(fā)自動(dòng)切換。
- Active ZKFC奔潰:雖然ZKFC是一個(gè)獨(dú)立的進(jìn)程,但因設(shè)計(jì)簡(jiǎn)單也容易出問(wèn)題,一旦ZKFC進(jìn)程掛掉,雖然此時(shí)NameNode是OK的,但系統(tǒng)也認(rèn)為需要切換,此時(shí)SNN會(huì)發(fā)一個(gè)請(qǐng)求到ANN要求ANN放棄主結(jié)點(diǎn)位置,ANN收到請(qǐng)求后,會(huì)觸發(fā)完成自動(dòng)切換。
- ZooKeeper奔潰:如果ZK奔潰了,主備NN上的ZKFC都會(huì)感知斷連,此時(shí)主備NN會(huì)進(jìn)入一個(gè)NeutralMode模式,同時(shí)不改變主備NN的狀態(tài),繼續(xù)發(fā)揮作用,只不過(guò)此時(shí),如果ANN也故障了,那集群無(wú)法發(fā)揮Failover, 也就不可用了,所以對(duì)于此種場(chǎng)景,ZK一般是不允許掛掉到多臺(tái),至少要有N/2+1臺(tái)保持服務(wù)才算是安全的。
五、總結(jié)
上面介紹了下關(guān)于HadoopHA機(jī)制,歸納起來(lái)主要是兩塊:元數(shù)據(jù)同步和主備選舉。元數(shù)據(jù)同步依賴于QJM共享存儲(chǔ),主備選舉依賴于ZKFC和Zookeeper。整個(gè)過(guò)程還是比較復(fù)雜的,如果能理解Paxos協(xié)議,那也能更好的理解這個(gè)。希望這篇文章能讓大家更深入了解關(guān)于HA方面的知識(shí)。