Hadoop源代碼分析(完整圖文版) part 1

在網上看到了很多此文章的裝載,但是都是純文字,這篇文章在沒有圖片的情況下閱讀起來意義不大了。花了點時間上傳了100多張圖片,希望對大家學習hadoop有幫助。

Hadoop源代碼分析(一)

關鍵字: 分佈式 雲計算 


Google的核心競爭技術是它的計算平臺。Google的大牛們用了下面5篇文章,介紹了它們的計算設施。 
GoogleCluster: http://research.google.com/archive/googlecluster.html 
Chubby:http://labs.google.com/papers/chubby.html 
GFS:http://labs.google.com/papers/gfs.html 
BigTable:http://labs.google.com/papers/bigtable.html 
MapReduce:http://labs.google.com/papers/mapreduce.html 
很快,Apache上就出現了一個類似的解決方案,目前它們都屬於Apache的Hadoop項目,對應的分別是: 
Chubby-->ZooKeeper 
GFS-->HDFS 
BigTable-->HBase 
MapReduce-->Hadoop 
目前,基於類似思想的Open Source項目還很多,如Facebook用於用戶分析的Hive。 
HDFS作爲一個分佈式文件系統,是所有這些項目的基礎。分析好HDFS,有利於瞭解其他系統。由於Hadoop的HDFS和MapReduce是同一個項目,我們就把他們放在一塊,進行分析。

下圖是MapReduce整個項目的頂層包圖和他們的依賴關係。Hadoop包之間的依賴關係比較複雜,原因是HDFS提供了一個分佈式文件系統,該系統提供API,可以屏蔽本地文件系統和分佈式文件系統,甚至象Amazon S3這樣的在線存儲系統。這就造成了分佈式文件系統的實現,或者是分佈式文件系統的底層的實現,依賴於某些貌似高層的功能。功能的相互引用,造成了蜘蛛網型的依賴關係。一個典型的例子就是包conf,conf用於讀取系統配置,它依賴於fs,主要是讀取配置文件的時候,需要使用文件系統,而部分的文件系統的功能,在包fs中被抽象了。

Hadoop的關鍵部分集中於圖中藍色部分,這也是我們考察的重點。 

 

· 

· 大小: 78.3 KB 

Hadoop源代碼分析(二)

下面給出了Hadoop的包的功能分析。

 Package

Dependences

tool

提供一些命令行工具,如DistCp,archive

mapreduce

Hadoop的Map/Reduce實現

filecache

提供HDFS文件的本地緩存,用於加快Map/Reduce的數據訪問速度

fs

文件系統的抽象,可以理解爲支持多種文件系統實現的統一文件訪問接口

hdfs

HDFS,Hadoop的分佈式文件系統實現

ipc

一個簡單的IPC的實現,依賴於io提供的編解碼功能

參考:http://zhangyu8374.javaeye.com/blog/86306

io

表示層。將各種數據編碼/解碼,方便於在網絡上傳輸

net

封裝部分網絡功能,如DNS,socket

security

用戶和用戶組信息

conf

系統的配置參數

metrics

系統統計數據的收集,屬於網管範疇

util

工具類

record

根據DDL(數據描述語言)自動生成他們的編解碼函數,目前可以提供C++和Java

http

基於Jetty的HTTP Servlet,用戶通過瀏覽器可以觀察文件系統的一些狀態信息和日誌

log

提供HTTP訪問日誌的HTTP Servlet

Hadoop源代碼分析(三)

由於Hadoop的MapReduce和HDFS都有通信的需求,需要對通信的對象進行序列化。Hadoop並沒有採用Java的序列化,而是引入了它自己的系統。

org.apache.hadoop.io中定義了大量的可序列化對象,他們都實現了Writable接口。實現了Writable接口的一個典型例子如下:

</pre><pre name="code" class="java">1.public class MyWritable implements Writable {      
2.    // Some data           
3.    private int counter;      
4.    private long timestamp;      
5.     
6.    public void write(DataOutput out) throws IOException {      
7.        out.writeInt(counter);      
8.        out.writeLong(timestamp);      
9.    }      
10.         
11.    public void readFields(DataInput in) throws IOException {      
12.        counter = in.readInt();      
13.        timestamp = in.readLong();      
14.    }      
15.     
16.    public static MyWritable read(DataInput in) throws IOException {      
17.        MyWritable w = new MyWritable();      
18.        w.readFields(in);      
19.        return w;      
20.    }      
21.}   


 其中的write和readFields分別實現了把對象序列化和反序列化的功能,是Writable接口定義的兩個方法。下圖給出了龐大的org.apache.hadoop.io中對象的關係。

 

這裏,我把ObjectWritable標爲紅色,是因爲相對於其他對象,它有不同的地位。當我們討論Hadoop的RPC時,我們會提到RPC上交換的信息,必須是Java的基本類型,String和Writable接口的實現類,以及元素爲以上類型的數組。ObjectWritable對象保存了一個可以在RPC上傳輸的對象和對象的類型信息。這樣,我們就有了一個萬能的,可以用於客戶端/服務器間傳輸的Writable對象。例如,我們要把上面例子中的對象作爲RPC請求,需要根據MyWritable創建一個ObjectWritable,ObjectWritable往流裏會寫如下信息

對象類名長度,對象類名,對象自己的串行化結果

這樣,到了對端,ObjectWritable可以根據對象類名創建對應的對象,並解串行。應該注意到,ObjectWritable依賴於WritableFactories,那存儲了Writable子類對應的工廠。我們需要把MyWritable的工廠,保存在WritableFactories中(通過WritableFactories.setFactory)。

 

Hadoop源代碼分析(五)

介紹完org.apache.hadoop.io以後,我們開始來分析org.apache.hadoop.rpc。RPC採用客戶機/服務器模式。請求程序就是一個客戶機,而服務提供程序就是一個服務器。當我們討論HDFS的,通信可能發生在:

· Client-NameNode之間,其中NameNode是服務器 

· Client-DataNode之間,其中DataNode是服務器 

· DataNode-NameNode之間,其中NameNode是服務器 

· DataNode-DateNode之間,其中某一個DateNode是服務器,另一個是客戶端 

如果我們考慮Hadoop的Map/Reduce以後,這些系統間的通信就更復雜了。爲了解決這些客戶機/服務器之間的通信,Hadoop引入了一個RPC框架。該RPC框架利用的Java的反射能力,避免了某些RPC解決方案中需要根據某種接口語言(如CORBA的IDL)生成存根和框架的問題。但是,該RPC框架要求調用的參數和返回結果必須是Java的基本類型,String和Writable接口的實現類,以及元素爲以上類型的數組。同時,接口方法應該只拋出IOException異常。(參考自http://zhangyu8374.javaeye.com/blog/86306

既然是RPC,當然就有客戶端和服務器,當然,org.apache.hadoop.rpc也就有了類Client和類Server。但是類Server是一個抽象類,類RPC封裝了Server,利用反射,把某個對象的方法開放出來,變成RPC中的服務器。

下圖是org.apache.hadoop.rpc的類圖。




Hadoop源代碼分析(六)

既然是RPC,自然就有客戶端和服務器,當然,org.apache.hadoop.rpc也就有了類Client和類Server。在這裏我們來仔細考察org.apache.hadoop.rpc.Client。下面的圖包含了org.apache.hadoop.rpc.Client中的關鍵類和關鍵方法。

由於Client可能和多個Server通信,典型的一次HDFS讀,需要和NameNode打交道,也需要和某個/某些DataNode通信。這就意味着某一個Client需要維護多個連接。同時,爲了減少不必要的連接,現在Client的做法是拿ConnectionId(圖中最右側)來做爲Connection的ID。ConnectionId包括一個InetSocketAddress(IP地址+端口號或主機名+端口號)對象和一個用戶信息對象。這就是說,同一個用戶到同一個InetSocketAddress的通信將共享同一個連接。


連接被封裝在類Client.Connection中,所有的RPC調用,都是通過Connection,進行通信。一個RPC調用,自然有輸入參數,輸出參數和可能的異常,同時,爲了區分在同一個Connection上的不同調用,每個調用都有唯一的id。調用是否結束也需要一個標記,所有的這些都體現在對象Client.Call中。Connection對象通過一個Hash表,維護在這個連接上的所有Call:

private Hashtable<Integer, Call> calls = new Hashtable<Integer, Call>();


一個RPC調用通過addCall,把請求加到Connection裏。爲了能夠在這個框架上傳輸Java的基本類型,String和Writable接口的實現類,以及元素爲以上類型的數組,我們一般把Call需要的參數打包成爲ObjectWritable對象。

Client.Connection會通過socket連接服務器,連接成功後回校驗客戶端/服務器的版本號(Client.ConnectionwriteHeader()方法),校驗成功後就可以通過Writable對象來進行請求的發送/應答了。注意,每個Client.Connection會起一個線程,不斷去讀取socket,並將收到的結果解包,找出對應的Call,設置Call並通知結果已經獲取。

Call使用Obejct的wait和notify,把RPC上的異步消息交互轉成同步調用。

還有一點需要注意,一個Client會有多個Client.Connection,這是一個很自然的結果。

Hadoop源代碼分析(七)

聊完了Client聊Server,按慣例,先把類圖貼出來。

 

需要注意的是,這裏的Server類是個抽象類,唯一抽象的地方,就是

public abstract Writable call(Writable param, long receiveTime) throws IOException;


這表明,Server提供了一個架子,Server的具體功能,需要具體類來完成。而具體類,當然就是實現call方法。

我們先來分析Server.Call,和Client.Call類似,Server.Call包含了一次請求,其中,id和param的含義和Client.Call是一致的。不同點在後面三個屬性,connection是該Call來自的連接,當然,當請求處理結束時,相應的結果會通過相同的connection,發送給客戶端。屬性timestamp是請求到達的時間戳,如果請求很長時間沒被處理,對應的連接會被關閉,客戶端也就知道出錯了。最後的response是請求處理的結果,可能是一個Writable的串行化結果,也可能一個異常的串行化結果。

Server.Connection維護了一個來之客戶端的socket連接。它處理版本校驗,讀取請求並把請求發送到請求處理線程,接收處理結果並把結果發送給客戶端。

Hadoop的Server採用了Java的NIO,這樣的話就不需要爲每一個socket連接建立一個線程,讀取socket上的數據。在Server中,只需要一個線程,就可以accept新的連接請求和讀取socket上的數據,這個線程,就是上面圖裏的Listener。

請求處理線程一般有多個,它們都是Server.Handle類的實例。它們的run方法循環地取出一個Server.Call,調用Server.call方法,蒐集結果並串行化,然後將結果放入Responder隊列中。

對於處理完的請求,需要將結果寫回去,同樣,利用NIO,只需要一個線程,相關的邏輯在Responder裏。

Hadoop源代碼分析(八)

(注:本節需要用到一些Java反射的背景)

有了Client和Server,很自然就能RPC啦。下面輪到RPC.java啦。

一般來說,分佈式對象一般都會要求根據接口生成存根和框架。如CORBA,可以通過IDL,生成存根和框架。但是,在org.apache.hadoop.rpc,我們就不需要這樣的步驟了。上類圖。


爲了分析Invoker,我們需要介紹一些Java反射實現Dynamic Proxy的背景。

Dynamic Proxy是由兩個class實現的:java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler,後者是一個接口。所謂Dynamic Proxy是這樣一種class:它是在運行時生成的class,在生成它時你必須提供一組interface給它,然後該class就宣稱它實現了這些interface。

這個Dynamic Proxy其實就是一個典型的Proxy模式,它不會替你作實質性的工作,在生成它的實例時你必須提供一個handler,由它接管實際的工作。這個handler,在Hadoop的RPC中,就是Invoker對象。

我們可以簡單地理解:就是你可以通過一個接口來生成一個類,這個類上的所有方法調用,都會傳遞到你生成類時傳遞的InvocationHandler實現中。

在Hadoop的RPC中,Invoker實現了InvocationHandler的invoke方法(invoke方法也是InvocationHandler的唯一方法)。Invoker會把所有跟這次調用相關的調用方法名,參數類型列表,參數列表打包,然後利用前面我們分析過的Client,通過socket傳遞到服務器端。就是說,你在proxy類上的任何調用,都通過Client發送到遠方的服務器上。

Invoker使用Invocation。Invocation封裝了一個遠程調用的所有相關信息,它的主要屬性有: methodName,調用方法名,parameterClasses,調用方法參數的類型列表和parameters,調用方法參數。注意,它實現了Writable接口,可以串行化。

RPC.Server實現了org.apache.hadoop.ipc.Server,你可以把一個對象,通過RPC,升級成爲一個服務器。服務器接收到的請求(通過Invocation),解串行化以後,就變成了方法名,方法參數列表和參數列表。利用Java反射,我們就可以調用對應的對象的方法。調用的結果再通過socket,返回給客戶端,客戶端把結果解包後,就可以返回給Dynamic Proxy的使用者了。

Hadoop源代碼分析(九)

一個典型的HDFS系統包括一個NameNode和多個DataNode。NameNode維護名字空間;而DataNode存儲數據塊。

DataNode負責存儲數據,一個數據塊在多個DataNode中有備份;而一個DataNode對於一個塊最多隻包含一個備份。所以我們可以簡單地認爲DataNode上存了數據塊ID和數據塊內容,以及他們的映射關係。

一個HDFS集羣可能包含上千DataNode節點,這些DataNode定時和NameNode通信,接受NameNode的指令。爲了減輕NameNode的負擔,NameNode上並不永久保存那個DataNode上有那些數據塊的信息,而是通過DataNode啓動時的上報,來更新NameNode上的映射表。

DataNode和NameNode建立連接以後,就會不斷地和NameNode保持心跳。心跳的返回其還也包含了NameNode對DataNode的一些命令,如刪除數據庫或者是把數據塊複製到另一個DataNode。應該注意的是:NameNode不會發起到DataNode的請求,在這個通信過程中,它們是嚴格的客戶端/服務器架構。

DataNode當然也作爲服務器接受來自客戶端的訪問,處理數據塊讀/寫請求。DataNode之間還會相互通信,執行數據塊複製任務,同時,在客戶端做寫操作的時候,DataNode需要相互配合,保證寫操作的一致性。

下面我們就來具體分析一下DataNode的實現。DataNode的實現包括兩部分,一部分是對本地數據塊的管理,另一部分,就是和其他的實體打交道。我們先來看本地數據塊管理部分。

安裝Hadoop的時候,我們會指定對應的數據塊存放目錄,當我們檢查數據塊存放目錄目錄時,我們回發現下面有個叫dfs的目錄,所有的數據就存放在dfs/data裏面。

 

其中有兩個文件,storage裏存的東西是一些出錯信息,貌似是版本不對…云云。in_use.lock是一個空文件,它的作用是如果需要對整個系統做排斥操作,應用應該獲取它上面的一個鎖。

接下來是3個目錄,current存的是當前有效的數據塊,detach存的是快照(snapshot,目前沒有實現),tmp保存的是一些操作需要的臨時數據塊。

但我們進入current目錄以後,就會發現有一系列的數據塊文件和數據塊元數據文件。同時還有一些子目錄,它們的名字是subdir0到subdir63,子目錄下也有數據塊文件和數據塊元數據。這是因爲HDFS限定了每個目錄存放數據塊文件的數量,多了以後會創建子目錄來保存。

數據塊文件顯然保存了HDFS中的數據,數據塊最大可以到64M。每個數據塊文件都會有對應的數據塊元數據文件。裏面存放的是數據塊的校驗信息。下面是數據塊文件名和它的元數據文件名的例子:

blk_3148782637964391313
blk_3148782637964391313_242812.meta

上面的例子中,3148782637964391313是數據塊的ID號,242812是數據塊的版本號,用於一致性檢查。

在current目錄下還有下面幾個文件:

VERSION,保存了一些文件系統的元信息。 

dncp_block_verification.log.curr和dncp_block_verification.log.prev,它記錄了一些DataNode對文件系定時統做一致性檢查需要的信息。 

Hadoop源代碼分析(一零)

在繼續分析DataNode之前,我們有必要看一下系統的工作狀態。啓動HDFS的時候,我們可以選擇以下啓動參數:

· FORMAT("-format"):格式化系統 

· REGULAR("-regular"):正常啓動 

· UPGRADE("-upgrade"):升級 

· ROLLBACK("-rollback"):回滾 

· FINALIZE("-finalize"):提交 

· IMPORT("-importCheckpoint"):從Checkpoint恢復。 

作爲一個大型的分佈式系統,Hadoop內部實現了一套升級機制(http://wiki.apache.org/hadoop/Hadoop_Upgrade)。upgrade參數就是爲了這個目的而存在的,當然,升級可能成功,也可能失敗。如果失敗了,那就用rollback進行回滾;如果過了一段時間,系統運行正常,那就可以通過finalize,正式提交這次升級(跟數據庫有點像啊)。

importCheckpoint選項用於NameNode發生故障後,從某個檢查點恢復。

有了上面的描述,我們得到下面左邊的狀態圖:

 

大家應該注意到,上面的升級/回滾/提交都不可能一下就搞定,就是說,系統故障時,它可能處於上面右邊狀態中的某一個。特別是分佈式的各個節點上,甚至可能出現某些節點已經升級成功,但有些節點可能處於中間狀態的情況,所以Hadoop採用類似於數據庫事務的升級機制也就不是很奇怪。

大家先理解一下上面的狀態圖,它是下面我們要介紹DataNode存儲的基礎。

Hadoop源代碼分析(一一)

我們來看一下升級/回滾/提交時的DataNode上會發生什麼(在類DataStorage中實現)。

前面我們提到過VERSION文件,它保存了一些文件系統的元信息,這個文件在系統升級時,會發生對應的變化。

升級時,NameNode會將新的版本號,通過DataNode的登錄應答返回。DataNode收到以後,會將當前的數據塊文件目錄改名,從current改名爲previous.tmp,建立一個snapshot,然後重建current目錄。重建包括重建VERSION文件,重建對應的子目錄,然後建立數據塊文件和數據塊元數據文件到previous.tmp的硬連接。建立硬連接意味着在系統中只保留一份數據塊文件和數據塊元數據文件,current和previous.tmp中的相應文件,在存儲中,只保留一份。當所有的這些工作完成以後,會在current裏寫入新的VERSION文件,並將previous.tmp目錄改名爲previous,完成升級。

瞭解了升級的過程以後,回滾就相對簡單。因爲說有的舊版本信息都保存在previous目錄裏。回滾首先將current目錄改名爲removed.tmp,然後將previous目錄改名爲current,最後刪除removed.tmp目錄。

提交的過程,就是將上面的previous目錄改名爲finalized.tmp,然後啓動一個線程,將該目錄刪除。

下圖給出了上面的過程:

 

需要注意的是,HDFS的升級,往往只是支持從某一個特點的老版本升級到當前版本。回滾時能夠恢復到的版本,也是previous中記錄的版本。

下面我們繼續分析DataNode。

文字分析完DataNode存儲在文件上的數據以後,我們來看一下運行時對應的數據結構。從大到小,Hadoop中最大的結構是Storage,最小的結構,在DataNode上是block。

類Storage保存了和存儲相關的信息,它繼承了StorageInfo,應用於DataNode的DataStorage,則繼承了Storage,總體類圖如下:

 

 

StorageInfo包含了3個字段,分別是layoutVersion:版本號,如果Hadoop調整文件結構佈局,版本號就會修改,這樣可以保證文件結構和應用一致。namespaceID是Storage的ID,cTime,creation time。

和StorageInfo相比,Storage就是個大傢伙了。

Storage可以包含多個根(參考配置項dfs.data.dir的說明),這些根通過Storage的內部類StorageDirectory來表示。StorageDirectory中最重要的方法是analyzeStorage,它將根據系統啓動時的參數和我們上面提到的一些判斷條件,返回系統現在的狀態。StorageDirectory可能處於以下的某一個狀態(與系統的工作狀態一定的對應):

    NON_EXISTENT:指定的目錄不存在;
    NOT_FORMATTED:指定的目錄存在但未被格式化;

    COMPLETE_UPGRADE:previous.tmp存在,current也存在
    RECOVER_UPGRADE:previous.tmp存在,current不存在

    COMPLETE_FINALIZE:finalized.tmp存在,current也存在

    COMPLETE_ROLLBACK:removed.tmp存在,current也存在,previous不存在
    RECOVER_ROLLBACK:removed.tmp存在,current不存在,previous存在

    COMPLETE_CHECKPOINT:lastcheckpoint.tmp存在,current也存在
    RECOVER_CHECKPOINT:lastcheckpoint.tmp存在,current不存在

    NORMAL:普通工作模式。

 

StorageDirectory處於某些狀態是通過發生對應狀態改變需要的工作文件夾和正常工作的current夾來進行判斷。狀態改變需要的工作文件夾包括:

    previous:用於升級後保存以前版本的文件

    previous.tmp:用於升級過程中保存以前版本的文件

    removed.tmp:用於回滾過程中保存文件

    finalized.tmp:用於提交過程中保存文件

    lastcheckpoint.tmp:應用於從NameNode中,導入一個檢查點

    previous.checkpoint:應用於從NameNode中,結束導入一個檢查點

 

有了這些狀態,就可以對系統進行恢復(通過方法doRecover)。恢復的動作如下(結合上面的狀態轉移圖):

    COMPLETE_UPGRADE:mv previous.tmp -> previous
    RECOVER_UPGRADE:mv previous.tmp -> current

    COMPLETE_FINALIZE:rm finalized.tmp

    COMPLETE_ROLLBACK:rm removed.tmp
    RECOVER_ROLLBACK:mv removed.tmp -> current

    COMPLETE_CHECKPOINT:mv lastcheckpoint.tmp -> previous.checkpoint
    RECOVER_CHECKPOINT:mv lastcheckpoint.tmp -> current

 

我們以RECOVER_UPGRADE爲例,分析一下。根據升級的過程,

1. current->previous.tmp

2. 重建current

3. previous.tmp->previous

 

當我們發現previous.tmp存在,current不存在,我們知道只需要將previous.tmp改爲current,就能恢復到未升級時的狀態。

StorageDirectory還管理着文件系統的元信息,就是我們上面提過StorageInfo信息,當然,StorageDirectory還保存每個具體用途自己的信息。這些信息,其實都存儲在VERSION文件中,StorageDirectory中的read/write方法,就是用於對這個文件進行讀/寫。下面是某一個DataNode的VERSION文件的例子:

 

 

配置文件代碼 

1. #Fri Nov 14 10:27:35 CST 2008  

2. namespaceID=1950997968  

3. storageID=DS-697414267-127.0.0.1-50010-1226629655026  

4. cTime=0  

5. storageType=DATA_NODE   

6. layoutVersion=-16  

#Fri Nov 14 10:27:35 CST 2008

namespaceID=1950997968

storageID=DS-697414267-127.0.0.1-50010-1226629655026

cTime=0

storageType=DATA_NODE

layoutVersion=-16

 

 

對StorageDirectory的排他操作需要鎖,還記得我們在分析系統目錄時提到的in_use.lock文件嗎?它就是用來給整個系統加/解鎖用的。StorageDirectory提供了對應的lock和unlock方法。

分析完StorageDirectory以後,Storage類就很簡單了。基本上都是對一系列StorageDirectory的操作,同時Storage提供一些輔助方法。

DataStorage是Storage的子類,專門應用於DataNode。上面我們對DataNode的升級/回滾/提交過程,就是對DataStorage的doUpgrade/doRollback/doFinalize分析得到的。

DataStorage提供了format方法,用於創建DataNode上的Storage,同時,利用StorageDirectory,DataStorage管理存儲系統的狀態。

Hadoop源代碼分析(一二)

分析完Storage相關的類以後,我們來看下一個大傢伙,FSDataset相關的類。

上面介紹Storage時,我們並沒有涉及到數據塊Block的操作,所有和數據塊相關的操作,都在FSDataset相關的類中進行處理。下面是類圖:

 

Block是對一個數據塊的抽象,通過前面的討論我們知道一個Block對應着兩個文件,其中一個存數據,一個存校驗信息,如下:

blk_3148782637964391313
blk_3148782637964391313_242812.meta

上面的信息中,blockId是3148782637964391313,242812是數據塊的版本號,當然,系統還會保存數據塊的大小,在類中是屬性numBytes。Block提供了一系列的方法來操作對象的屬性。

DatanodeBlockInfo存放的是Block在文件系統上的信息。它保存了Block存放的卷(FSVolume),文件名和detach狀態。這裏有必要解釋一下detach狀態:我們前面分析過,系統在升級時會創建一個snapshot,snapshot的文件和current裏的數據塊文件和數據塊元文件是通過硬鏈接,指向了相同的內容。當我們需要改變current裏的文件時,如果不進行detach操作,那麼,修改的內容就會影響snapshot裏的文件,這時,我們需要將對應的硬鏈接解除掉。方法很簡單,就是在臨時文件夾裏,複製文件,然後將臨時文件改名成爲current裏的對應文件,這樣的話,current裏的文件和snapshot裏的文件就detach了。這樣的技術,也叫copy-on-write,是一種有效提高系統性能的方法。DatanodeBlockInfo中的detachBlock,能夠對Block對應的數據文件和元數據文件進行detach操作。

介紹完類Block和DatanodeBlockInfo後,我們來看FSVolumeSet,FSVolume和FSDir。我們知道在一個DataNode上可以指定多個Storage來存儲數據塊,由於HDFS規定了一個目錄能存放Block的數目,所以一個Storage上存在多個目錄。對應的,FSDataset中用FSVolume來對應一個Storage,FSDir對應一個目錄,所有的FSVolume由FSVolumeSet管理,FSDataset中通過一個FSVolumeSet對象,就可以管理它的所有存儲空間。

FSDir對應着HDFS中的一個目錄,目錄裏存放着數據塊文件和它的元文件。FSDir的一個重要的操作,就是在添加一個Block時,根據需要有時會擴展目錄結構,上面提過,一個Storage上存在多個目錄,所有的目錄,都對應着一個FSDir,目錄的關係,也由FSDir保存。FSDir的getBlockInfo方法分析目錄下的所有數據塊文件信息,生成Block對象,存放到一個集合中。getVolumeMap方法能,則會建立Block和DatanodeBlockInfo的關係。以上兩個方法,用於系統啓動時蒐集所有的數據塊信息,便於後面快速訪問。

FSVolume對應着是某一個Storage。數據塊文件,detach文件和臨時文件都是通過FSVolume來管理的,這個其實很自然,在同一個存儲系統上移動文件,往往只需要修改文件存儲信息,不需要搬數據。FSVolume有一個recoverDetachedBlocks的方法,用於恢復detach文件。和Storage的狀態管理一樣,detach文件有可能在複製文件時系統崩潰,需要對detach的操作進行回覆。FSVolume還會啓動一個線程,不斷更新FSVolume所在文件系統的剩餘容量。創建Block的時候,系統會根據各個FSVolume的容量,來確認Block的存放位置。

FSVolumeSet就不討論了,它管理着所有的FSVolume。

HDFS中,對一個chunk的寫會使文件處於活躍狀態,FSDataset中引入了類ActiveFile。ActiveFile對象保存了一個文件,和操作這個文件的線程。注意,線程有可能有多個。ActiveFile的構造函數會自動地把當前線程加入其中。

有了上面的基礎,我們可以開始分析FSDataset。FSDataset實現了接口FSDatasetInterface。FSDatasetInterface是DataNode對底層存儲的抽象。

下面給出了FSDataset的關鍵成員變量:

  FSVolumeSet volumes;
  private HashMap<Block,ActiveFile> ongoingCreates = new HashMap<Block,ActiveFile>();
  private HashMap<Block,DatanodeBlockInfo> volumeMap = null;

其中,volumes就是FSDataset使用的所有Storage,ongoingCreates是Block到ActiveFile的映射,也就是說,說有正在創建的Block,都會記錄在ongoingCreates裏。

下面我們討論FSDataset中的方法。

public long getMetaDataLength(Block b) throws IOException;
得到一個block的元數據長度。通過block的ID,找對應的元數據文件,返回文件長度。

 

public MetaDataInputStream getMetaDataInputStream(Block b) throws IOException;
得到一個block的元數據輸入流。通過block的ID,找對應的元數據文件,在上面打開輸入流。下面對於類似的簡單方法,我們就不再仔細討論了。

 

public boolean metaFileExists(Block b) throws IOException;
判斷block的元數據的元數據文件是否存在。簡單方法。

 

public long getLength(Block b) throws IOException;
block的長度。簡單方法。

 

public Block getStoredBlock(long blkid) throws IOException;
通過Block的ID,找到對應的Block。簡單方法。

 

public InputStream getBlockInputStream(Block b) throws IOException;
public InputStream getBlockInputStream(Block b, long seekOffset) throws IOException;
得到Block數據的輸入流。簡單方法。

 

public BlockInputStreams getTmpInputStreams(Block b, long blkoff, long ckoff) throws IOException;
得到Block的臨時輸入流。注意,臨時輸入流是指對應的文件處於tmp目錄中。新創建塊時,塊數據應該寫在tmp目錄中,直到寫操作成功,文件纔會被移動到current目錄中,如果失敗,就不會影響current目錄了。簡單方法。

 

public BlockWriteStreams writeToBlock(Block b, boolean isRecovery) throws IOException;
得到一個block的輸出流。BlockWriteStreams既包含了數據輸出流,也包含了元數據(校驗文件)輸出流,這是一個相當複雜的方法。

參數isRecovery說明這次寫是不是對以前失敗的寫的一次恢復操作。我們先看正常的寫操作流程:首先,如果輸入的block是個正常的數據塊,或當前的block已經有線程在寫,writeToBlock會拋出一個異常。否則,將創建相應的臨時數據文件和臨時元數據文件,並把相關信息,創建一個ActiveFile對象,記錄到ongoingCreates中,並創建返回的BlockWriteStreams。前面我們已經提過,建立新的ActiveFile時,當前線程會自動保存在ActiveFile的threads中。

我們以blk_3148782637964391313爲例,當DataNode需要爲Block ID爲3148782637964391313創建寫流時,DataNode創建文件tmp/blk_3148782637964391313做爲臨時數據文件,對應的meta文件是tmp/blk_3148782637964391313_XXXXXX.meta。其中XXXXXX是版本號。

isRecovery爲true時,表明我們需要從某一次不成功的寫中恢復,流程相對於正常流程複雜。如果不成功的寫是由於提交(參考finalizeBlock方法)後的確認信息沒有收到,先創建一個detached文件(備份)。接着,writeToBlock檢查是否有還有對文件寫的線程,如果有,則通過線程的interrupt方法,強制結束線程。這就是說,如果有線程還在寫對應的文件塊,該線程將被終止。同時,從ongoingCreates中移除對應的信息。接下來將根據臨時文件是否存在,創建/複用臨時數據文件和臨時數據元文件。後續操作就和正常流程一樣,根據相關信息,創建一個ActiveFile對象,記錄到ongoingCreates中……

由於這塊涉及了一些HDFS寫文件時的策略,以後我們還會繼續討論這個話題。

 

 

public void updateBlock(Block oldblock, Block newblock) throws IOException;
更新一個block。這也是一個相當複雜的方法。

updateBlock的最外層是一個死循環,循環的結束條件,是沒有任何和這個數據塊相關的寫線程。每次循環,updateBlock都會去調用一個叫tryUpdateBlock的內部方法。tryUpdateBlock發現已經沒有線程在寫這個塊,就會跟新和這個數據塊相關的信息,包括元文件和內存中的映射表volumeMap。如果tryUpdateBlock發現還有活躍的線程和該塊關聯,那麼,updateBlock會試圖結束該線程,並等在join上等待。

 

 

public void finalizeBlock(Block b) throws IOException;
提交(或叫:結束finalize)通過writeToBlock打開的block,這意味着寫過程沒有出錯,可以正式把Block從tmp文件夾放到current文件夾。

在FSDataset中,finalizeBlock將從ongoingCreates中刪除對應的block,同時將block對應的DatanodeBlockInfo,放入volumeMap中。我們還是以blk_3148782637964391313爲例,當DataNode提交Block ID爲3148782637964391313數據塊文件時,DataNode將把tmp/blk_3148782637964391313移到current下某一個目錄,以subdir12爲例,這是tmp/blk_3148782637964391313將會挪到current/subdir12/blk_3148782637964391313。對應的meta文件也在目錄current/subdir12下。

 

 

public void unfinalizeBlock(Block b) throws IOException;
取消通過writeToBlock打開的block,與finalizeBlock方法作用相反。簡單方法。

 

public boolean isValidBlock(Block b);
該Block是否有效。簡單方法。

 

public void invalidate(Block invalidBlks[]) throws IOException;
使block變爲無效。簡單方法。

 

public void validateBlockMetadata(Block b) throws IOException;
檢查block的有效性。簡單方法。

Hadoop源代碼分析(一三)

通過上面的一系列介紹,我們知道了DataNode工作時的文件結構和文件結構在內存中的對應對象。下面我們可以來開始分析DataNode上的動態行爲。首先我們來分析DataXceiverServer和DataXceiver。DataNode上數據塊的接受/發送並沒有採用我們前面介紹的RPC機制,原因很簡單,RPC是一個命令式的接口,而DataNode處理數據部分,往往是一種流式機制。DataXceiverServer和DataXceiver就是這個機制的實現。其中,DataXceiver還依賴於兩個輔助類:BlockSender和BlockReceiver。下面是類圖:

 

(爲了簡單起見,BlockSender和BlockReceiver的成員變量沒有進入UML模型中)

DataXceiverServer很簡單,它打開一個端口,然後每接收到一個連接,就創建一個DataXceiver,服務於該連接,並記錄該連接的socket,對應的實現在DataXceiverServer的run方法裏。當系統關閉時,DataXceiverServer將關閉監聽的socket和所有DataXceiver的socket,這樣就導致了DataXceiver出錯並結束線程。

DataXceiver纔是真正幹活的地方,目前,DataXceiver支持的操作總共有六條,分別是:

OP_WRITE_BLOCK (80):寫數據塊

OP_READ_BLOCK (81):讀數據塊

OP_READ_METADATA (82):讀數據塊元文件

OP_REPLACE_BLOCK (83):替換一個數據塊

OP_COPY_BLOCK (84):拷貝一個數據塊

OP_BLOCK_CHECKSUM (85):讀數據塊檢驗碼

DataXceiver首先讀取客戶端的版本號並檢驗,然後再讀取一個字節的操作碼,並轉入相關的子程序進行處理。我們先看一下讀數據塊的過程吧。

首先看輸入,下圖是讀數據塊時,客戶端發送過來的信息:

 

包括了要讀取的Block的ID,時間戳,開始偏移和讀取的長度,最後是客戶端的名字(貌似只是在寫日誌的時候用到了)。根據上面的信息,我們可以創建一個BlockSender,如果BlockSender沒有出錯,返回客戶端一個正確指示後,否則,返回錯誤碼。成功創建BlockSender以後,就可以開始通過BlockSender.sendBlock發送數據。

下面我們就來分析BlockSender。BlockSender的構造函數看似很複雜,其實就是根據需求(特別是在處理checksum上,因爲checksum是基於塊的),打開相應的數據流。close()用於釋放各種資源,如已經打開的數據流。sendBlock用於發送數據,數據發送包括應答頭和後續的數據包。應答頭如下(包含DataXceiver中發送的成功標識):

 

然後後面的數據就組織成數據包來發送,包結構如下:

 

各個字段含義:

packetLen:包長度,包括包頭
offset:偏移量
seqno:包序列號
tail:是否是最後一個包
len:數據長度
checksum:檢驗數據
data:數據塊數據

需要注意的,在寫數據前,BlockSender會校驗數據,保證數據包中的checksum和數據的一致性。同時,如果數據出錯,將會有ChecksumException拋出。

數據傳輸結束的標誌,是一個packetLen長度爲0的包。客戶端可以返回一個兩字節的應答OP_STATUS_CHECKSUM_OK(5)

Hadoop源代碼分析(一四)

繼續DataXceiver分析,下一塊硬骨頭是寫數據塊。HDFS的寫數據操作,比讀數據複雜N多倍。讀數據的時候,只需要在多個數據塊文件的選一個讀,就可以了;但是,寫數據需要同時寫到多個數據塊文件上,這就比較複雜了。HDFS實現了了Google寫文件時的機制,如下圖:

 

數據流從客戶端開始,流經一系列的節點,到達最後一個DataNode。圖中的所有DataNode只需要寫一次硬盤,DataNode1和DataNode2會將從socket上接受到的數據,直接寫到到下個節點的socket上。

我們來看一下寫數據塊的請求。

 

首先是客戶端的版本號和一個字節的操作碼,接下來是我們熟悉的blockId和generationStamp。參數pipelineSize是整個數據流鏈的長度,以上面爲例,pipelineSize=3。isRecovery指示這次寫是否是一次恢復操作,還記得我們在討論FSDataset.writeToBlock時的那個參數嗎?isRecovery來自客戶端。client是客戶端的名字,就是發起請求的節點名,需要特別注意的是,如果是從NameNode來的複製請求,client爲空。hasSrcDataNode是一個標誌位,如果被設置,表明源節點是個DataNode,接下來讀取的數據就是DataNode的信息。numTargets是目標節點的數目,包括當前節點,以上面的圖爲例,DataNode1上這個參數值爲3,到了DataNode3,就只有1了。targets包含了目標節點的相關信息,根據這些信息,就可以創建到它們上面的socket連接。targets後跟着的是校驗頭。

writeBlock最開始是處理上面提到的消息包,然後創建一個BlockReceiver。接下來就是創建一堆用於讀寫的流,如下圖(圖中除了in外,都是在writeBlock中創建,這個圖還不涉及在BlockReceiver對本地文件讀寫的流):

 

在進行實際的數據寫之前,上面的這些流會被建立起來(也就是說,DataNode1到DataNode3都可寫以後,纔開始處理寫數據)。如果其中某一個點出錯了,那麼,出錯的節點名會通過mirrorIn發送回來,一直沿着這條鏈,傳播到客戶端。

如果一切正常,那麼,BlockReceiver.receiveBlock就開始幹活了。

BlockReceiver的構造函數會創建寫數據塊和校驗數據的輸出流。剩下的就交給receiveBlock這個大傢伙了。首先receiveBlock會再啓動一個線程(一般來說,BlockReceiver就跑在它自己的線程上),用於處理應答(內部類PacketResponder定義了該線程),然後就不斷調用receivePacket讀數據。

數據是以分塊的形式傳送,格式和讀Block的時候是一樣的。如下圖(很奇怪,爲啥不抽象爲類):

 

注意:如果當前DataNode處於數據流的中間,該數據包會發送到下一個節點。

接下來的處理,就是處理數據和校驗,並分別寫到數據塊文件和數據塊元數據文件。如果出錯,拋出的異常會導致receiveBlock關閉相關的輸出流,並終止傳輸。注意,數據校驗出錯還會上報到NameNode上。

PacketResponder用於處理應答。也就是上面講的mirrorIn和replyOut。PacketResponder裏有一個隊列ackQueue,receivePacket每收到一個包,都會往隊列裏添加一項。PacketResponder的run方法,根據工作的DataNode所處的位置,行爲不一樣。

最後一個DataNode由於沒有後續節點,PacketResponder的ackQueue每收到一項,表明對應的數據塊已經處理完畢,那麼就可以發送成功應答。如果該應答是最後一個包的,PacketResponder會關閉相關的輸出流,並提交(前面講FSDataset時後我們討論過的finalizeBlock方法)。

如果DataNode有後續節點,那麼,它必須等到後續節點的成功應答,纔可以發送應答到它前面的節點。

PacketResponder的run方法還引入了心跳機制,用於檢測連接是否還存在。

 

注意:所有改變DataNode的操作,需要把信息更新到NameNode上,這是通過DataNode.notifyNamenodeReceivedBlock方法,然後通過DataNode統一發送到NameNode上。

Hadoop源代碼分析(一五)

DataXceiver支持的的6條操作,我們已經分析完最重要的兩條。剩下的分別是:

OP_READ_METADATA (82):讀數據塊元文件

OP_REPLACE_BLOCK (83):替換一個數據塊

OP_COPY_BLOCK (84):拷貝一個數據塊

OP_BLOCK_CHECKSUM (85):讀數據塊檢驗碼

我們逐個討論。

讀數據塊元文件的請求如圖(操作碼82):

 

應答很簡單,應答碼(如OP_STATUS_SUCCESS),文件長度(int),數據。

拷貝數據塊和替換數據塊是一對相對應操作。

替換數據塊的請求如圖(操作碼83)。這個比起上面的讀數據塊元文件請求,有點複雜。替換一個數據塊是系統平衡操作的一部分,用於接收一個數據塊。它和普通的數據塊寫的差別是,它只發生在兩個節點上,一個寫,一個讀,而不需要建立數據鏈。我們可以比較一下它們在創建BlockReceiver對象時的差別:

blockReceiver = new BlockReceiver(block, proxyReply,
          proxySock.getRemoteSocketAddress().toString(),
          proxySock.getLocalSocketAddress().toString(),
          false, "", null, datanode);  //OP_REPLACE_BLOCK
blockReceiver = new BlockReceiver(block, in, 
          s.getRemoteSocketAddress().toString(),
          s.getLocalSocketAddress().toString(),
          isRecovery, client, srcDataNode, datanode);  //OP_WRITE_BLOCK


首先,proxyReply和in不一樣,這是因爲發起請求的節點和提供數據的節點並不是同一個。寫數據塊發起請求方也提供數據,替換數據塊請求方不提供數據,而是提供了一個數據源(proxySource參數),由replaceBlock發起一個拷貝數據塊的請求,建立數據源。對於拷貝數據塊操作,isRecovery=false,client=””, srcDataNode=null。注意,我們在分析BlockReceiver是,討論過client=””的情況,就是應用於這種場景。

在創建BlockReceiver對象前,需要利用下面介紹的拷貝數據塊的請求建立到數據源的socket連接併發送拷貝數據塊請求。然後通過BlockReceiver.receiveBlock接收數據。任務成功後將結果通知notifyNamenodeReceivedBlock。

拷貝數據塊的請求如圖(操作碼84)。和讀數據塊操作請求類似,但是讀取的是整個數據塊,所以少了很多參數。

 

讀數據塊檢驗碼的請求如圖(操作碼85)。它能夠讀取某個數據塊的檢驗和的MD5結果,實現的方法很簡單。

Hadoop源代碼分析(一六)

通過上面的討論,DataNode上的讀/寫流程已經基本清楚了。我們來看下一個非主流流程,

DataBlockScanner用於定時對數據塊文件進行校驗。類圖如下:

 

DataBlockScanner擁有它單獨的線程,能定時地從目前DataNode管理的數據塊文件進行校驗。其實最重要的方法就是verifyBlock,我們來看這個方法最關鍵的地方:

blockSender = new BlockSender(block, 0, -1, false, false, true, datanode);
DataOutputStream out = new DataOutputStream(new IOUtils.NullOutputStream());
blockSender.sendBlock(out, null, throttler);

校驗利用了BlockSender,因爲我們知道BlockSender中,發送數據的同時,會對數據進行校驗。verifyBlock只需要讀一個Block到一個空輸出設備(NullOutputStream),如果有異常,那麼校驗失敗,如果正常,校驗成功。

DataBlockScanner其他的輔助方法用於對DataBlockScanner管理的數據塊文件信息進行增加/刪除,排序操作。同時,校驗的信息還會保持在Storage上,保存在dncp_block_verification.log.curr和dncp_block_verification.log.prev中。

Hadoop源代碼分析(一七)

周圍的障礙掃清以後,我們可以開始分析類DataNode。類圖如下:

 

public class DataNode extends Configured
    implements InterDatanodeProtocol, ClientDatanodeProtocol, FSConstants, Runnable

上面給出了DataNode的繼承關係,我們發現,DataNode實現了兩個通信接口,其中ClientDatanodeProtocol是用於和Client交互的,InterDatanodeProtocol,就是我們前面提到的DataNode間的通信接口。ipcServer(類圖的左下方)是DataNode的一個成員變量,它啓動了一個IPC服務,這樣,DataNode就能提供ClientDatanodeProtocol和InterDatanodeProtocol的能力了。

我們從main函數開始吧。這個函數很簡單,調用了createDataNode的方法,然後就等着DataNode的線程結束。createDataNode首先調用instantiateDataNode初始化DataNode,然後執行runDatanodeDaemon。runDatanodeDaemon會向NameNode註冊,如果成功,才啓動DataNode線程,DataNode就開始幹活了。

初始化DataNode的方法instantiateDataNode會讀取DataNode需要的配置文件,同時讀取配置的storage目錄(可能有多個,看storage的討論部分),然後把這兩參數送到makeInstance中,makeInstance會先檢查目錄(存在,是目錄,可讀,可寫),然後調用:

new DataNode(conf, dirs);

接下來控制流就到了構造函數上。構造函數調用startDataNode,完成和DataNode相關的初始化工作(注意,DataNode工作線程不在這個函數裏啓動)。首先是初始化一堆的配置參數,什麼NameNode地址,socket參數等等。然後,向NameNode請求配置信息(DatanodeProtocol.versionRequest),並檢查返回的NamespaceInfo和本地的版本是否一致。

正常情況的下一步是檢查文件系統的狀態並做必要的恢復,初始化FSDataset(到這個時候,上面圖中storage和data成員變量已經初始化)。

然後,找一個端口並創建DataXceiverServer(run方法裏啓動),創建DataBlockScanner(根據需要在offerService中啓動,只啓動一次),創建DataNode上的HttpServer,啓動ipcServer。這樣就結束了DataNode相關的初始化工作。

在啓動DataNode工作線程前,DataNode需要向NameNode註冊。註冊信息在初始化的時候已經構造完畢,包括DataXceiverServer端口,ipcServer端口,文件佈局版本號等重要信息。註冊成功後就可以啓動DataNode線程。

DataNode的run方法,循環裏有兩種選擇,升級(暫時不討論)/正常工作。我們來看正常工作的offerService方法。offerService也是個循環,在循環裏,offerService會定時向NameNode發送心跳,報告系統中Block狀態的變化,報告DataNode現在管理的Block狀態。發送心跳和Block狀態報告時,NameNode會返回一些命令,DataNode將執行這些命令。

心跳的處理比較簡單,以heartBeatInterval間隔發送。

Block狀態變化報告,會利用保存在receivedBlockList和delHints兩個列表中的信息。receivedBlockList表明在這個DataNode成功創建的新的數據塊,而delHints,是可以刪除該數據塊的節點。如在DataXceiver的replaceBlock中,有調用:

datanode.notifyNamenodeReceivedBlock(block, sourceID)

這表明,DataNode已經從sourceID上接收了一個Block,sourceID上對應的Block可以刪除了(這個場景出現在當系統需要做負載均衡時,Block在DataNode之間拷貝)。

Block狀態變化報告通過NameNode.blockReceived來報告。

Block狀態報告也比較簡單,以blockReportInterval間隔發送。

心跳和Block狀態報告可以返回命令,這也是NameNode先DataNode發起請求的唯一方法。我們來看一下都有那些命令:

  DNA_TRANSFER:拷貝數據塊到其他DataNode

  DNA_INVALIDATE:刪除數據塊(簡單方法)

  DNA_SHUTDOWN:關閉DataNode(簡單方法)

  DNA_REGISTER:DataNode重新註冊(簡單方法)

  DNA_FINALIZE:提交升級(簡單方法)

  DNA_RECOVERBLOCK:恢復數據塊

拷貝數據塊到其他DataNode由transferBlocks方法執行。注意,返回的命令可以包含多個數據塊,每一個數據塊可以包含多個目標地址。transferBlocks方法將爲每一個Block啓動一個DataTransfer線程,用於傳輸數據。

DataTransfer是一個DataNode的內部類,它利用我們前面介紹的OP_WRITE_BLOCK寫數據塊操作,發送數據到多個目標上面。

恢復數據塊和NameNode的租約(lease)恢復有關,我們後面再討論。

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