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的關鍵部分集中於圖中藍色部分,這也是我們考察的重點。
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介面的一個典型例子如下:
- public class MyWritable implements Writable {
- // Some data
- private int counter;
- private long timestamp;
- public void write(DataOutput out) throws IOException {
- out.writeInt(counter);
- out.writeLong(timestamp);
- }
- public void readFields(DataInput in) throws IOException {
- counter = in.readInt();
- timestamp = in.readLong();
- }
- public static MyWritable read(DataInput in) throws IOException {
- MyWritable w = new MyWritable();
- w.readFields(in);
- return w;
- }
- }
public class MyWritable implements Writable {
// Some data
private int counter;
private long timestamp;
public void write(DataOutput out) throws IOException {
out.writeInt(counter);
out.writeLong(timestamp);
}
public void readFields(DataInput in) throws IOException {
counter = in.readInt();
timestamp = in.readLong();
}
public static MyWritable read(DataInput in) throws IOException {
MyWritable w = new MyWritable();
w.readFields(in);
return w;
}
}
其中的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>();
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;
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,後者是一個介面。所謂DynamicProxy是這樣一種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:mvprevious.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檔案的例子:
- #Fri Nov 14 10:27:35 CST2008
- namespaceID=1950997968
- storageID=DS-697414267-127.0.0.1-50010-1226629655026
- cTime=0
- storageType=DATA_NODE
- 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;
privateHashMap<Block,ActiveFile>ongoingCreates= newHashMap<Block,ActiveFile>();
privateHashMap<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 booleanmetaFileExists(Block b) throwsIOException;
判斷block的後設資料的後設資料檔案是否存在。簡單方法。
public longgetLength(Block b) throwsIOException;
block的長度。簡單方法。
public Block getStoredBlock(longblkid) throwsIOException;
通過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 voidupdateBlock(Block oldblock, Block newblock) throws IOException;
更新一個block。這也是一個相當複雜的方法。
updateBlock的最外層是一個死迴圈,迴圈的結束條件,是沒有任何和這個資料塊相關的寫執行緒。每次迴圈,updateBlock都會去呼叫一個叫tryUpdateBlock的內部方法。tryUpdateBlock發現已經沒有執行緒在寫這個塊,就會跟新和這個資料塊相關的資訊,包括元檔案和記憶體中的對映表volumeMap。如果tryUpdateBlock發現還有活躍的執行緒和該塊關聯,那麼,updateBlock會試圖結束該執行緒,並等在join上等待。
public voidfinalizeBlock(Block b) throwsIOException;
提交(或叫:結束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 voidunfinalizeBlock(Block b) throwsIOException;
取消通過writeToBlock開啟的block,與finalizeBlock方法作用相反。簡單方法。
public booleanisValidBlock(Block b);
該Block是否有效。簡單方法。
public voidinvalidate(Block invalidBlks[]) throwsIOException;
使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
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 = 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
implementsInterDatanodeProtocol, 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)恢復有關,我們後面再討論。
Hadoop原始碼分析(一八)
DataNode的介紹基本告一段落。我們開始來分析NameNode。相比於DataNode,NameNode比較複雜。系統中只有一個NameNode,作為系統檔案目錄的管理者和“inode表”(熟悉UNIX的同學們應該瞭解inode)。為了高可用性,系統中還存在著從NameNode。
先前我們分析DataNode的時候,關注的是資料塊。NameNode作為HDFS中檔案目錄和檔案分配的管理者,它儲存的最重要資訊,就是下面兩個對映:
檔名à資料塊
資料塊àDataNode列表
其中,檔名à資料塊儲存在磁碟上(持久化);但NameNode上不儲存資料塊àDataNode列表,該列表是通過DataNode上報建立起來的。
下圖包含了NameNode和DataNode往外暴露的介面,其中,DataNode實現了InterDatanodeProtocol和ClientDatanodeProtocol,剩下的,由NameNode實現。
ClientProtocol提供給客戶端,用於訪問NameNode。它包含了檔案角度上的HDFS功能。和GFS一樣,HDFS不提供POSIX形式的介面,而是使用了一個私有介面。一般來說,程式設計師通過org.apache.hadoop.fs.FileSystem來和HDFS打交道,不需要直接使用該介面。
DatanodeProtocol:用於DataNode向NameNode通訊,我們已經在DataNode的分析過程中,瞭解部分介面,包括:register,用於DataNode註冊;sendHeartbeat/blockReport/blockReceived,用於DataNode的offerService方法中;errorReport我們沒有討論,它用於向NameNode報告一個錯誤的Block,用於BlockReceiver和DataBlockScanner;nextGenerationStamp和commitBlockSynchronization用於lease管理,我們在後面討論到lease時,會統一說明。
NamenodeProtocol用於從NameNode到NameNode的通訊。
下圖補充了介面裡使用的資料的關係。
Hadoop原始碼分析(一九)
我們先分析INode*.java,類INode*抽象了檔案層次結構。如果我們對檔案系統進行物件導向的抽象,一定會得到和下面一樣類似的結構圖(類INode*):
INode是一個抽象類,它的兩個字類,分別對應著目錄(INodeDirectory)和檔案(INodeFile)。INodeDirectoryWithQuota,如它的名字隱含的,是帶了容量限制的目錄。INodeFileUnderConstruction,抽象了正在構造的檔案,當我們需要在HDFS中建立檔案的時候,由於建立過程比較長,目錄系統會維護對應的資訊。
INode中的成員變數有:name,目錄/檔名;modificationTime和accessTime是最後的修改時間和訪問時間;parent指向了父目錄;permission是訪問許可權。HDFS採用了和UNIX/Linux類似的訪問控制機制。系統維護了一個類似於UNIX系統的組表(group)和使用者表(user),並給每一個組和使用者一個ID,permission在INode中是long型,它同時包含了組和使用者資訊。
INode中存在大量的get和set方法,當然是對上面提到的屬性的操作。匯出屬性,比較重要的有:collectSubtreeBlocksAndClear,用於收集這個INode所有後繼中的Block;computeContentSummary用於遞迴計算INode包含的一些相關資訊,如檔案數,目錄數,佔用磁碟空間。
INodeDirectory是HDFS管理的目錄的抽象,它最重要的成員變數是:
privateList<INode> children;
就是這個目錄下的所有目錄/檔案集合。INodeDirectory也是有大量的get和set方法,都很簡單。INodeDirectoryWithQuota進一步加強了INodeDirectory,限制了INodeDirectory可以使用的空間(包括NameSpace和磁碟空間)。
INodeFile是HDFS中的檔案,最重要的成員變數是:
protectedBlockInfo blocks[] = null;
這是這個檔案對應的Block列表,BlockInfo增強了Block類。
INodeFileUnderConstruction儲存了正在構造的檔案的一些資訊,包括clientName,這是目前擁有租約的節點名(建立檔案時,只有一個節點擁有租約,其他節點配合這個節點工作)。clientMachine是構造該檔案的客戶端名稱,如果構造請求由DataNode發起,clientNode會保持相應的資訊,targets儲存了配合構造檔案的所有節點。
上面描述了INode*類的關係。下面我們順便考察一下一些NameNode上的資料類。
BlocksMap儲存了Block和它在NameNode上一些相關的資訊。其核心是一個map:Map<Block, BlockInfo>。BlockInfo擴充套件了Block,儲存了該Block歸屬的INodeFile和DatanodeDescriptor,同時還包括了它的前繼和後繼Block。有了BlocksMap,就可以通過Block找對應的檔案和這個Block存放的DataNode的相關資訊。
接下來我們來分析類Datanode*。DatanodeInfo和DatanodeID都定義在包org.apache.hadoop.hdfs.protocol。DatanodeDescriptor是DatanodeInfo的子類,包含了NameNode需要的附加資訊。DatanodeID只包含了一些配置資訊,DatanodeInfo增加了一些動態資訊,DatanodeDescriptor更進一步,包含了DataNode上一些Block的動態資訊。DatanodeDescriptor包含了內部類BlockTargetPair,它儲存Block和對應DatanodeDescriptor的關聯,BlockQueue是BlockTargetPair佇列。
DatanodeDescriptor包含了兩個BlockQueue,分別記錄了該DataNode上正在複製(replicateBlocks)和Lease恢復(recoverBlocks)的Block。同時還有一個Block集合,儲存的是該DataNode上已經失效的Block。DatanodeDescriptor提供一系列方法,用於操作上面儲存的佇列和集合。也提供get*Command方法,用於生成傳送到DataNode的命令。
當NameNode收到DataNode對現在管理的Block狀態的彙報是,會呼叫reportDiff,找出和現在NameNode上的資訊差別,以供後續處理用。
readFieldsFromFSEditLog方法用於從日誌中恢復DatanodeDescriptor。
Hadoop原始碼分析(二零)
前面我們提過關係:檔名à資料塊持久化在磁碟上,所有對目錄樹的更新和檔名à資料塊關係的修改,都必須能夠持久化。為了保證每次修改不需要從新儲存整個結構,HDFS使用操作日誌,儲存更新。
現在我們可以得到NameNode需要儲存在Disk上的資訊了,包括:
[hadoop@localhostdfs]$ ls -R name
name:
current image in_use.lock
name/current:
edits fsimage fstime VERSION
name/image:
fsimage
in_use.lock的功能和DataNode的一致。fsimage儲存的是檔案系統的目錄樹,edits則是檔案樹上的操作日誌,fstime是上一次新開啟一個操作日誌的時間(long型)。
image/fsimage是一個保護檔案,防止0.13以前的版本啟動(0.13以前版本將fsimage存放在name/image目錄下,如果用0.13版本啟動,顯然在讀fsimage會出錯J)。
我們可以開始討論FSImage了,類FSImage如下圖:
分析FSImage,不免要跟DataStorage去做比較(上圖也保留了類DataStorage)。前面我們已經分析過DataStorage的狀態變化,包括升級/回滾/提交,FSImage也有類似的升級/回滾/提交動作,而且這部分的行為和DataStorage是比較一致,如下狀態轉移圖。圖中update方法和DataStorage的差別比較大,是因為處理資料庫和處理檔案系統名字空間不一樣,其他的地方都比較一致。FSImage也能夠管理多個Storage,而且還能夠區分Storage為IMAGE(目錄結構)/EDITS(日誌)/IMAGE_AND_EDITS(前面兩種的組合)。
我們可以看到,FSImage和DataStorage都有recoverTransitionRead方法。FSImage的recoverTransitionRead方法主要步驟是檢查系統一致性(analyzeStorage)並嘗試恢復,初始化新的storage,然後根據啟動NameNode的引數,做升級/回滾等操作。
FSImage需要支援引數-importCheckpoint,該引數用於在某一個checkpoint目錄里載入HDFS的目錄資訊,並更新到當前系統,該引數的主要功能在方法doImportCheckpoint中。該方法很簡單,通過讀取配置的checkpoint目錄來載入fsimage檔案和日誌檔案,然後利用saveFSImage(下面討論)儲存到當前的工作目錄,完成匯入。
loadFSImage(File curFile)用於在fsimage中讀入NameNode持久化的資訊,是FSImage中最重要的方法之一,該檔案的結構如下:
最開始是版本號(注意,各版本檔案佈局不一樣,文中分析的樣本是0.17的),然後是名稱空間的ID號,檔案個數和最高檔案版本號(就是說,下一次產生檔案版本號的初始值)。接下來就是檔案的資訊啦,首先是檔名,然後是該檔案的副本數,接下來是修改時間/訪問時間,資料塊大小,資料塊數目。資料塊數目如果大於0,表明這是個檔案,那麼接下來就是numBlocks個資料塊(淺藍),如果資料塊數目等於0,那該條目是目錄,接下來是應用於該目錄的quota。最後是訪問控制的一些資訊。檔案資訊一共有numFiles個,接下來是處於構造狀態的檔案的資訊。(有些版本可能還會保留DataNode的資訊,但0.17已經不儲存這樣的資訊啦)。loadFSImage(File curFile)的對應方法是saveFSImage(FilenewFile),FSImage中還有一系列的方法(大概7,8個)用於配合這兩個方法工作,我們就不再深入討論了。
loadFSEdits(StorageDirectory sd)用於載入日誌檔案,並把日誌檔案記錄的內容應用到NameNode,loadFSEdits只是簡單地呼叫FSEditLog中對應的方法。
loadFSImage()和saveFSImage()是另外一對重要的方法。
loadFSImage()會在所有的Storage中,讀取最新的NameNode持久化資訊,並應用相應的日誌,當loadFSImage()呼叫返回以後,記憶體中的目錄樹就是最新的。loadFSImage()會返回一個標記,如果Storage中有任何和記憶體中最終目錄樹中不一致的Image(最常見的情況是日誌檔案不為空,那麼,記憶體中的Image應該是Storage的Image加上日誌,當然還有其它情況),那麼,該標記為true。
saveFSImage()的功能正好相反,它將記憶體中的目錄樹持久化,很自然,目錄樹持久化後就可以把日誌清空。saveFSImage()會建立edits.new,並把當前記憶體中的目錄樹持久化到fsimage.ckpt(fsimage現在還存在),然後重新開啟日誌檔案edits和edits.new,這會導致日誌檔案edits和edits.new被清空。最後,saveFSImage()呼叫rollFSImage()方法。
rollFSImage()上來就把所有的edits.new都改為edits(經過了方法saveFSImage,它們都已經為空),然後再把fsimage.ckpt改為fsimage。如下圖:
為了防止誤呼叫rollFSImage(),系統引入了狀態CheckpointStates.UPLOAD_DONE。
有了上面的狀態轉移圖,我們就很好理解方法recoverInterruptedCheckpoint了。
圖中存在另一條路徑,應用於GetImageServlet中。GetImageServlet是和從NameNode進行檔案通訊的介面,這個場景留到我們分析從NameNode時再進行分析。
最後我們分析一下和檢查點相關的一個類,rollFSImage()會返回這個類的一個例項。CheckpointSignature用於標識一個日誌的檢查點,它是StorageInfo的子類,同時實現了WritableComparable介面,出了StorageInfo的資訊,它還包括了兩個屬性:editsTime和checkpointTime。editsTime是日誌的最後修改時間,checkpointTime是日誌建立時間。在和從NameNode節點的通訊中,需要用CheckpointSignature,來保證從NameNode獲得的日誌是最新的。
Hadoop原始碼分析(二一)
不好意思,突然間需要忙專案的其他事情了,更新有點慢下來,爭取月底搞定HDFS吧。
我們來分析FSEditLog.java,該類提供了NameNode操作日誌和日誌檔案的相關方法,相關類圖如下:
首先是FSEditLog依賴的輸入/輸出流。輸入流基本上沒有新新增功能;輸出流在開啟的時候,會寫入日誌的版本號(最前面的4位元組),同時,每次將記憶體刷到硬碟時,會為日誌尾部寫入一個特殊的標識(OP_INVALID)。
FSEditLog有開啟/關閉的方法,它們都是很簡單的方法,就是關閉的時候,要等待所有正在寫日誌的操作都完成寫以後,才能關閉。processIOError用於處理IO出錯,一般這會導致對於的Storage的日誌檔案被關閉(還記得loadFSImage要找出最後寫的日誌檔案吧,這也是提高系統可靠性的一個方法),如果系統再也找不到可用的日誌檔案,NameNode將會退出。
loadFSEdits是個大傢伙,它讀取日誌檔案,並把日誌應用到記憶體中的目錄結構中。這傢伙大是因為它需要處理所有型別的日誌記錄,其實就一大case語句。logEdit的作用和loadFSEdits相反,它向日志檔案中寫入日誌記錄。我們來分析一下什麼操作需要寫log,還有就是需要log那些引數:
logOpenFile(OP_ADD):申請lease
path(路徑)/replication(副本數,文字形式)/modificationTime(修改時間,文字形式)/accessTime(訪問時間,文字形式)/preferredBlockSize(塊大小,文字形式)/BlockInfo[](增強的資料塊資訊,陣列)/permissionStatus(訪問控制資訊)/clientName(客戶名)/clientMachine(客戶機器名)
logCloseFile(OP_CLOSE):歸還lease
path/replication/modificationTime/accessTime/preferredBlockSize/BlockInfo[]/permissionStatus
logMkDir(OP_MKDIR):建立目錄
path/modificationTime/accessTime/permissionStatus
logRename(OP_RENAME):改檔名
src(原檔名)/dst(新檔名)/timestamp(時間戳)
logSetReplication(OP_SET_REPLICATION):更改副本數
src/replication
logSetQuota(OP_SET_QUOTA):設定空間額度
path/nsQuota(檔案空間額度)/dsQuota(磁碟空間額度)
logSetPermissions(OP_SET_PERMISSIONS):設定檔案許可權位
src/permissionStatus
logSetOwner(OP_SET_OWNER):設定檔案組和主
src/username(所有者)/groupname(所在組)
logDelete(OP_DELETE):刪除檔案
src/timestamp
logGenerationStamp(OP_SET_GENSTAMP):檔案版本序列號
genstamp(序列號)
logTimes(OP_TIMES):更改檔案更新/訪問時間
src/modificationTime/accessTime
通過上面的分析,我們應該清楚日誌檔案裡記錄了那些資訊。
rollEditLog()我們在前面已經提到過(配合saveFSImage和rollFSImage),它用於關閉edits,開啟日誌到edits.new。purgeEditLog()的作用正好相反,它刪除老的edits檔案,然後把edits.new改名為edits。這也是Hadoop在做更新修改時經常採用的策略。
Hadoop原始碼分析(二二)
我們開始對租約Lease進行分析,下面是類圖。Lease可以認為是一個檔案寫鎖,當客戶端需要寫檔案的時候,它需要申請一個Lease,NameNode負責記錄那個檔案上有Lease,Lease的客戶是誰,超時時間(分散式處理的一種常用技術)等,所有這些工作由下面3個類完成。至於租約過期NameNode需要採取什麼動作,並不是這部分code要完成的功能。
LeaseManager(左)管理著系統中的所有Lease(右),同時,LeaseManager有一個執行緒Monitor,用於檢查是否有Lease到期。
一個租約由一個holder(客戶端名),lastUpdate(上次更新時間)和paths(該客戶端操作的檔案集合)構成。瞭解了這些屬性,相關的方法就很好理解了。LeaseManager的方法也就很好理解,就是對Lease進行操作。注意,LeaseManager的addLease並沒有檢查檔案上是否已經有Lease,這個是由LeaseManager的呼叫者來保證的,這使LeaseManager跟簡單。內部類Monitor通過對Lease的最後跟新時間來檢測Lease是否過期,如果過期,簡單呼叫FSNamesystem的internalReleaseLease方法。
這部分的程式碼比我想象的簡單,主要是大部分的一致性邏輯都存在於LeaseManager的使用者。在開始分析FSNamesystem.java這個4.5k多行的龐然大物之前,我們繼續來掃除外圍的障礙。下面是關於訪問控制的一些類:
Hadoop檔案保護採用的UNIX的機制,檔案使用者分檔案屬主、檔案組和其他使用者,許可權讀,寫和執行(FsAction中抽象了所有組合)。
我們先分析包org.apache.hadoop.fs.permission的幾個類吧。FsAction抽象了操作許可權,FsPermission記錄了某檔案/路徑的允許情況,分檔案屬主、檔案組和其他使用者,同時提供了一系列的轉換方法,applyUMask用於去掉某些許可權,如某些操作需要去掉檔案的寫許可權,那麼可以通過該方法,生成對應的去掉寫許可權的FsPermission物件。PermissionStatus用於描述一個檔案的檔案屬主、檔案組和它的FsPermission。
INode在儲存PermissionStatus時,用了不同的方法,它用一個long變數,和SerialNumberManager配合,儲存了PermissionStatus的所有資訊。
SerialNumberManager儲存了檔案主和檔案主號,使用者組和使用者組號的對應關係。注意,在持久化資訊FSImage中,不儲存檔案主號和使用者組號,它們只是SerialNumberManager分配的,只儲存在記憶體的資訊。通過SerialNumberManager得到某檔案主的檔案主號時,如果找不到檔案主號,會往對應關係中新增一條記錄。
INode的long變數作為一個位串,分組儲存了FsPermission(MODE),檔案主號(USER)和使用者組號(GROUP)。
PermissionChecker用於許可權檢查。
Hadoop原始碼分析(二三)
下面我們來分析FSDirectory。其實分析FSDirectory最好的地方,應該是介紹完INode*以後,FSDirectory在INode*的基礎上,儲存了HDFS的檔案目錄狀態。系統載入FSImage時,FSImage會在FSDirectory物件上重建檔案目錄狀態,HDFS檔案目錄狀態的變化,也由FSDirectory寫日誌,同時,它儲存了檔名à資料塊的對映關係。
FSDirectory只有很少的成員變數,如下:
finalFSNamesystem namesystem;
finalINodeDirectoryWithQuotarootDir;
FSImage fsImage;
booleanready =false;
其中,namesystem,fsImage是指向FSNamesystem物件和FSImage物件的引用,rootDir是檔案系統的根,ready初值為false,當系統成功載入FSImage以後,ready會變成true,FSDirectory的使用者就可以呼叫其它FSDirectory功能了。
FSDirectory中剩下的,就是一堆的方法(我們不討論和MBean相關的類,方法和過程)。
loadFSImage用於載入目錄樹結構,它會去呼叫FSImage的方法,完成持久化資訊的匯入以後,它會把成員變數ready置為true。系統呼叫loadFSImage是在FSNamesystem.java的initialize方法,那是系統初始化重要的一步。
addFile用於建立檔案或追加資料時建立INodeFileUnderConstruction,下圖是它的Call Hierachy圖:
addFile首先會試圖在系統中建立到檔案的路徑,如果檔案為/home/hadoop/Hadoop.tar,addFile會呼叫mkdirs(建立路徑為/home/hadoop,這也會涉及到一系列方法),保證檔案路徑存在,然後建立INodeFileUnderConstruction節點,並把該節點加到目錄樹中(通過addNode,也是需要呼叫一系列方法),如果成功,就寫操作日誌(logOpenFile)。
unprotectedAddFile也用於在系統中建立一個目錄或檔案(非UnderConstruction),如果是檔案,還會建立對應的block。FSDirectory中還有好幾個unprotected*方法,它們不檢查成員變數ready,不寫日誌,它們大量用於loadFSEdits中(這個時候ready當然是false,而且因為正在恢復日誌,也不需要寫日誌)。
addToParent新增一個INode到目錄樹中,並返回它的上一級目錄,它的實現和unprotectedAddFile是類似的。
persistBlocks比較有意思,用於往日誌裡記錄某inode的block資訊,其實並沒有一個對應於persistBlocks的寫日誌方法,它用的是logOpenFile。這個大家可以去檢查一下logOpenFile記錄的資訊。closeFile對應了logCloseFile。
addBlock和removeBlock對應,用於新增/刪除資料塊資訊,同時它們還需要更新FSNamesystem.java中對應的資訊。
unprotectedRenameTo和renameTo實現了UNIX的mv命令,主要的功能都在unprotectedRenameTo中完成,複雜的地方在於對各種各樣情況的討論。
setReplication和unprotectedSetReplication用於更新資料塊的副本數,很簡單的方法,注意,改變產生的對資料塊的刪除/複製是在FSNamesystem.java中實現。
setPermission,unprotectedSetPermission,setOwner和unprotectedSetOwner都是簡單的方法。
Delete和unprotectedDelete又是一對方法,刪除如果需要刪除資料塊,將通過FSNamesystem的removePathAndBlocks進行。
……(後續的方法和前面介紹的,都比較類似,都是一些過程性的東西,就不再討論了)
Hadoop原始碼分析(二四)
下面輪到FSNamesystem出場了。FSNamesystem.java一共有4573行,而整個namenode目錄下所有的Java程式總共也只有16876行,把FSNamesystem搞定了,NameNode也就基本搞定。
FSNamesystem是NameNode實際記錄資訊的地方,儲存在FSNamesystem中的資料有:
l 檔名à資料塊列表(存放在FSImage和日誌中)
l 合法的資料塊列表(上面關係的逆關係)
l 資料塊àDataNode(只儲存在記憶體中,根據DataNode發過來的資訊動態建立)
l DataNode上儲存的資料塊(上面關係的逆關係)
l 最近傳送過心跳資訊的DataNode(LRU)
我們先來分析FSNamesystem的成員變數。
private booleanisPermissionEnabled;
是否開啟許可權檢查,可以通過配置項dfs.permissions來設定。
privateUserGroupInformation fsOwner;
本地檔案的使用者檔案屬主和檔案組,可以通過hadoop.job.ugi設定,如果沒有設定,那麼將使用啟動HDFS的使用者(通過whoami獲得)和該使用者所在的組(通過groups獲得)作為值。
private Stringsupergroup;
對應配置項dfs.permissions.supergroup,應用在defaultPermission中,是系統的超級組。
privatePermissionStatus defaultPermission;
預設許可權,預設使用者為fsOwner,預設使用者組為supergroup,預設許可權為0777,可以通過dfs.upgrade.permission修改。
private long capacityTotal,capacityUsed, capacityRemaining;
系統總容量/已使用容量/剩餘容量
private int totalLoad = 0;
系統總連線數,根據DataNode心跳資訊跟新。
private long pendingReplicationBlocksCount, underReplicatedBlocksCount,scheduledReplicationBlocksCount;
分別是成員變數pendingReplications(正在複製的資料塊),neededReplications(需要複製的資料塊)的大小,scheduledReplicationBlocksCount是當前正在處理的複製工作數目。
public FSDirectorydir;
指向系統使用的FSDirectory物件。
BlocksMap blocksMap= newBlocksMap();
儲存資料塊到INode和DataNode的對映關係
public CorruptReplicasMap corruptReplicas = newCorruptReplicasMap();
儲存損壞(如:校驗沒通過)的資料塊到對應DataNode的關係,CorruptReplicasMap類圖如下,類只有一個成員變數,儲存Block到一個DatanodeDescriptor的集合的對映和這個對映上的一系列操作:
Map<String, DatanodeDescriptor> datanodeMap = newTreeMap<String, DatanodeDescriptor>();
儲存了StorageID à DatanodeDescriptor的對映,用於保證DataNode使用的Storage的一致性。
privateMap<String, Collection<Block>> recentInvalidateSets
儲存了每個DataNode上無效但還存在的資料塊(StorageIDà ArrayList<Block>)。
Map<String,Collection<Block>> recentInvalidateSets
儲存了每個DataNode上有效,但需要刪除的資料塊(StorageIDà TreeSet<Block>),這種情況可能發生在一個DataNode故障後恢復後,上面的資料塊在系統中副本數太多,需要刪除一些資料塊。
HttpServer infoServer;
int infoPort;
Date startTime;
用於內部資訊傳輸的HTTP請求伺服器(Servlet的容器)。現在有/fsck,/getimage,/listPaths/*,/data/*和/fileChecksum/*,我們後面還會繼續討論。
ArrayList<DatanodeDescriptor>heartbeats;
所有目前活著的DataNode,執行緒HeartbeatMonitor會定期檢查。
privateUnderReplicatedBlocks neededReplications
需要進行復制的資料塊。UnderReplicatedBlocks的類圖如下,它其實是一個陣列,陣列的下標是優先順序(0的優先順序最高,如果資料塊只有一個副本,它的優先順序是0),陣列的內容是一個Block集合。UnderReplicatedBlocks提供一些方法,對Block進行增加,修改,查詢和刪除。
privatePendingReplicationBlocks pendingReplications;
儲存正在複製的資料塊的相關資訊。PendingReplicationBlocks的類圖如下:
其中,pendingReplications儲存了所有正在進行復制的資料塊,使用Map是需要一些附加的資訊PendingBlockInfo。這些資訊包括時間戳,用於檢測是否已經超時,和現在進行復制的數目numReplicasInProgress。timedOutItems是超時的複製項,超時的複製項在FSNamesystem的processPendingReplications方法中被刪除,並從新複製。timerThread是用於檢測複製超時的執行緒的控制程式碼,對應的執行緒是PendingReplicationMonitor的一個例項,它的run方法每隔一段會檢查是否有超時的複製項,如果有,將該資料塊加到timedOutItems中。Timeout是run方法的檢查間隔,defaultRecheckInterval是預設值。PendingReplicationBlocks和PendingBlockInfo的方法都很簡單。
public LeaseManagerleaseManager = newLeaseManager(this);
租約管理器。
Hadoop原始碼分析(二五)
繼續對FSNamesystem進行分析。
Daemonhbthread = null; // HeartbeatMonitor thread
public Daemonlmthread = null; // LeaseMonitor thread
Daemon smmthread = null; // SafeModeMonitor thread
public Daemon replthread = null; // Replication thread
NameNode上的執行緒,分別對應DataNode心跳檢查,租約檢查,安全模式檢查和資料塊複製,我們會在後面介紹這些執行緒對應的功能。
volatile boolean fsRunning = true;
long systemStart =0;
系統執行標誌和系統啟動時間。
接下來是一堆系統的引數,比方說系統每個DataNode節點允許的最大資料塊數,心跳檢查間隔時間等… …
// The maximum number ofreplicates we should allow for a single block
private int maxReplication;
// How many outgoing replicationstreams a given node should have at one time
private intmaxReplicationStreams;
//MIN_REPLICATION is how many copies we need in place or else we disallow thewrite
private int minReplication;
//Default replication
private intdefaultReplication;
//heartbeatRecheckInterval is how often namenode checks for expired datanodes
private longheartbeatRecheckInterval;
//heartbeatExpireInterval is how long namenode waits for datanode to report
//heartbeat
private longheartbeatExpireInterval;
//replicationRecheckInterval is how often namenode checks for newreplication work
private longreplicationRecheckInterval;
//decommissionRecheckInterval is how often namenode checks if a node hasfinished decommission
private longdecommissionRecheckInterval;
//default block size of a file
private longdefaultBlockSize = 0;
private int replIndex =0;
和neededReplications配合,記錄下一個進行復制的資料塊位置。
public staticFSNamesystem fsNamesystemObject;
哈哈,不用介紹了,還是static的。
private String localMachine;
private int port;
本機名字和RPC埠。
private SafeModeInfo safeMode; //safe mode information
記錄安全模式的相關資訊。
安全模式是這樣一種狀態,系統處於這個狀態時,不接受任何對名字空間的修改,同時也不會對資料塊進行復制或刪除資料塊。NameNode啟動的時候會自動進入安全模式,同時也可以手工進入(不會自動離開)。系統啟動以後,DataNode會報告目前它擁有的資料塊的資訊,當系統接收到的Block資訊到達一定門檻,同時每個Block都有dfs.replication.min個副本後,系統等待一段時間後就離開安全模式。這個門檻定義的引數包括:
l dfs.safemode.threshold.pct:接受到的Block的比例,預設為95%,就是說,必須DataNode報告的資料塊數目佔總數的95%,才到達門檻;
l dfs.replication.min:預設為1,即每個副本都存在系統中;
l dfs.replication.min:等待時間,預設為0,單位秒。
SafeModeInfo的類圖如下:
threshold,extension和safeReplication儲存的是上面說的3個引數。Reached等於-1表明安全模式是關閉的,0表示安全模式開啟但是系統還沒達到threshold。blockTotal是計算threshold時的分母,blockSafe是分子,lastStatusReport用於控制寫日誌的間隔。
SafeModeInfo(Configuration conf)使用配置檔案的引數,是NameNode正常啟動時使用的建構函式,SafeModeInfo()中,this.threshold = 1.5f使得系統用於處於安全模式。
enter()使系統進入安全模式,leave()會使系統離開安全模式,canLeave()用於檢查是否能離開安全模式而needEnter(),則判斷是否應該進入安全模式。checkMode()檢查系統狀態,如果必要,則進入安全模式。其他的方法都比價簡單,大多為對成員變數的訪問。
討論完類SafeModeInfo,我們來分析一下SafeModeMonitor,它用於定期檢查系統是否能夠離開安全模式(smmthread就是它的一個例項)。系統離開安全模式後,smmthread會被重新賦值為null。
Hadoop原始碼分析(二六)
(沒想到需要分頁啦)
private Host2NodesMap host2DataNodeMap = new Host2NodesMap();
儲存了主機名(String)到DatanodeDescriptor陣列的對映(Host2NodesMap唯一的成員變數為HashMap<String,DatanodeDescriptor[]> map,它的方法都是對這個map進行操作)。
NetworkTopology clusterMap = newNetworkTopology();
privateDNSToSwitchMapping dnsToSwitchMapping;
定義了HDFS的網路拓撲,網路拓撲對應選擇資料塊副本的位置很重要。如在一個層次型的網路中,接到同一個交換機的兩個節點間的網路速度,會比跨越多個交換機的兩個節點間的速度快,但是,如果某交換機故障,那麼它對接到它上面的兩個節點會同時有影響,但跨越多個交換機的兩個節點,這種影響會小得多。下面是NetworkTopology相關的類圖:
Hadoop實現了一個樹狀的拓撲結構抽象,其中,Node介面,定義了網路節點的一些方法,NodeBase是Node的一個實現,提供了葉子節點的一些方法(明顯它沒有子節點),而InnerNode則實現了樹的內部節點,如果我們考慮一個網路部署的話,那麼葉子節點是伺服器,而InnerNode則是伺服器所在的機架或交換機或路由器。Node提供了對網路位置資訊(採用類似檔案樹的方式),節點名稱和Node所在的樹的深度的方法。NodeBase提供了一個簡單的實現。InnerNode是NetworkTopology的內部類,對比NodeBase,它的clildren儲存了所有的子節點,這樣的話,就可以構造一個拓撲樹。這棵樹的葉子可能是伺服器,也可能是機架,內部則是機架或者是路由器等裝置,InnerNode提供了一系列的方法區分處理這些資訊。
NetworkTopology的add方法和remove用於在拓撲結構中加入節點和刪除節點,同時也給出一些get*方法,用於獲取一些物件內部的資訊,如getDistance,可以獲取兩個節點的距離,而isOnSameRack可以判斷兩個節點是否處於同一個機架。chooseRandom有兩個實現,用於在一定範圍內(另一個還有一個排除選項)隨機選取一個節點。chooseRandom在選擇資料塊副本位置的時候呼叫。
DNSToSwitchMapping配合上面NetworkTopology,用於確定某一個節點的網路位置資訊,它的唯一方法,可以通過一系列機器的名字找出它們對應的網路位置資訊。目前有支援兩種方法,一是通過命令列方式,將節點名作為輸入,輸出為網路位置資訊(RawScriptBasedMapping執行命令CachedDNSToSwitchMapping快取結果),還有一種就是利用配置引數hadoop.configured.node.mapping靜態配置(StaticMapping)。
ReplicationTargetChooser replicator;
用於為資料塊備份選擇目標,例如,使用者寫檔案時,需要選擇一些DataNode,作為資料塊的存放位置,這時候就利用它來選擇目標地址。chooseTarget是ReplicationTargetChooser中最重要的方法,它通過內部的一個NetworkTopology物件,計算出一個DatanodeDescriptor陣列,該陣列就是選定的DataNode,同時,順序就是最佳的資料流順序(還記得我們討論DataXceiver些資料的那個圖嗎?)。
privateHostsFileReader hostsReader;
儲存了系統中允許/不允許連線到NameNode的機器列表。
private Daemondnthread = null;
執行緒控制程式碼,該執行緒用於檢測DataNode上的Decommission程式。例如,某節點被列入到不允許連線到NameNode的機器列表中(HostsFileReader),那麼,該節點會進入Decommission狀態,它上面的資料塊會被複制到其它節點,複製結束後機器進入DatanodeInfo.AdminStates.DECOMMISSIONED,這臺機器就可以從HDFS中撤掉。
private long maxFsObjects =0; // maximum number of fsobjects
系統能擁有的INode最大數(配置項dfs.max.objects,0為無限制)。
private finalGenerationStamp generationStamp = newGenerationStamp();
系統的時間戳生產器。
private intblockInvalidateLimit = FSConstants.BLOCK_INVALIDATE_CHUNK;
傳送給DataNode刪除資料塊訊息中,能包含的最大資料塊數。比方說,如果某DataNode上有250個Block需要被刪除,而這個引數是100,那麼一共會有3條刪除資料塊訊息訊息,前面兩條包含了100個資料塊,最後一條是50個。
private longaccessTimePrecision = 0;
用於控制檔案的access時間的精度,也就是說,小於這個精度的兩次對檔案訪問,後面的那次就不做記錄了。
Hadoop原始碼分析(二七)
我們接下來分析NameNode.java的成員變數,然後兩個類綜合起來,分析它提供的介面,並配合說明介面上請求對應的處理流程。
前面已經介紹過了,NameNode實現了介面ClientProtocol,DatanodeProtocol和NamenodeProtocol,分別提供給客戶端/DataNode/從NameNode訪問。由於NameNode的大部分功能在類FSNamesystem中實現,那麼NameNode.java的成員變數就很少了。
public FSNamesystemnamesystem;
指向FSNamesystem物件。
private Serverserver;
NameNode的RPC伺服器例項。
private Threademptier;
處理回收站的執行緒控制程式碼。
private int handlerCount =2;
還記得我們分析RPC的伺服器時提到的伺服器請求處理執行緒(Server.Handle)嗎?這個引數給出了server中伺服器請求處理執行緒的數目,對應配置引數為dfs.namenode.handler.count。
private booleansupportAppends = true;
是否支援append操作,對應配置引數為dfs.support.append。
privateInetSocketAddress nameNodeAddress = null;
NameNode的地址,包括IP地址和監聽埠。
下面我們來看NameNode的啟動過程。main方法是系統的入口,它會呼叫createNameNode建立NameNode例項。createNameNode分析命令列引數,如果是FORMAT或FINALIZE,呼叫對應的方法後退出,如果是其他的引數,將建立NameNode物件。NameNode的建構函式會呼叫initialize,初始化NameNode的成員變數,包括建立RPC伺服器,初始化FSNamesystem,啟動RPC伺服器和回收站執行緒。
FSNamesystem的建構函式會呼叫initialize方法,去初始化上面我們分析過的一堆成員變數。幾個重要的步驟包括載入FSImage,設定系統為安全模式,啟動各個工作執行緒和HTTP伺服器。系統的一些引數是在setConfigurationParameters中初始化的,其中一些值的計算比較麻煩,而且也可能被其它部分的code引用的,就獨立出來了,如getNamespaceDirs和getNamespaceEditsDirs。initialize對應的是close方法,很簡單,主要是停止initialize中啟動的執行緒。
對應於initialize方法,NameNode也提供了對應的stop方法,用於初始化時出錯系統能正確地退出。
NameNode的format和finalize操作,都是先構造FSNamesystem,然後利用FSNamesystem的FSImage提供的對應方法完成的。我們在分析FSImage.java時,已經瞭解了這部分的功能。
Hadoop原始碼分析(二八)
萬事俱備,我們可以來分析NameNode上的流程啦。
首先我們來看NameNode上實現的ClientProtocol,客戶端通過這個介面,可以對目錄樹進行操作,開啟/關閉檔案等。
getBlockLocations用於確定檔案內容的位置,它的輸入引數為:檔名,偏移量,長度,返回值是一個LocatedBlocks物件(如下圖),它攜帶的資訊很多,大部分欄位我們以前都討論過。
getBlockLocations直接呼叫NameSystem的同名方法。NameSystem中這樣的方法首先會檢查許可權和對引數進行檢查(如偏移量和長度要大於0),然後再呼叫實際的方法。找LocatedBlocks先找src對應的INode,然後通過INode的getBlocks方法,可以拿到該節點的Block列表,如果返回為空,表明該INode不是檔案,返回null;如果Block列表長度為0,以空的Block陣列構造返回的LocatedBlocks。
如果Block陣列不為空,則通過請求的偏移量和長度,就可以把這個區間涉及的Block找出來,對於每一個block,執行:
l 通過BlocksMap我們可以找到它存在於幾個DataNode上(BlocksMap.numNodes方法);
l 計算包含該資料塊但資料塊是壞的DataNode的數目(通過NameSystem.countNodes方法,間接訪問CorruptReplicasMap中的資訊);
l 計算壞資料塊的數目(CorruptReplicasMap.numCorruptReplicas方法,應該和上面的數相等);
l 通過上面的計算,我們得到現在還OK的資料塊數目;
l 從BlocksMap中找出所有OK的資料塊對應的DatanodeDescriptor(DatanodeInfo的父類);
l 建立對應的LocatedBlock。
收集到每個資料塊的LocatedBlock資訊後,很自然就能構造LocatedBlocks物件。getBlockLocations其實只是一個讀的方法,請求到了NameNode以後只需要查表就行了。
create方法,該方法用於在目錄樹上建立檔案(建立目錄使用mkdir),需要的引數比較多,包括檔名,許可權,客戶端名,是否覆蓋已存在檔案,副本數和塊大小。NameNode的create呼叫NameSystem的startFile方法(startFile需要的引數clientMachine從執行緒區域性變數獲取)。
startFile方法先呼叫startFileInternal完成操作,然後呼叫logSync,等待日誌寫完後才返回。
startFileInternal不但服務於startFile,也被appendFile呼叫(通過引數append區分)。方法的最開始是一堆檢查,包括:安全模式,檔名src是否正確,許可權,租約,replication引數,overwrite引數(對append操作是判斷src指向是否存在並且是檔案)。租約檢查很簡單,如果通過FSDirectory.getFileINode(src)得到的檔案是出於構造狀態,表明有客戶正在操作該檔案,這時會丟擲異常AlreadyBeingCreatedException。
如果對於建立操作,會通過FSDirectory的addFile往目錄樹上新增一個檔案並在租約管理器裡新增一條記錄。
對於append操作,執行的是構造一個新的INodeFileUnderConstruction並替換原有的節點,然後在租約管理器裡新增一條記錄。
總的來說,最簡單的create流程就是在目錄樹上建立一個INodeFileUnderConstruction物件並往租約管理器裡新增一條記錄。
我們順便分析一下append吧,它的返回值是LocatedBlock,比起getBlockLocations,它只需要返回陣列的一項。appendFile是NameSystem的實現方法,它首先呼叫上面討論的startFileInternal方法(已經在租約管理器裡新增了一條記錄)然後寫日誌。然後尋找對應檔案INodeFile中記錄的最後一個block,並通過BlocksMap.getStoredBlock()方法得到BlockInfo,然後再從BlocksMap中獲得所有的DatanodeDescriptor,就可以構造LocatedBlock了。需要注意的,如果該Block在需要被複制的集合(UnderReplicatedBlocks)中,移除它。
如果檔案剛被建立或者是最後一個資料塊已經寫滿,那麼append會返回null,這是客戶端需要使用addBlock,為檔案新增資料塊。
Hadoop原始碼分析(二九)
public booleansetReplication(String src,
short replication
) throws IOException;
setReplication,設定檔案src的副本數為replication,返回值為boolean,在FSNameSystem中,呼叫方法setReplicationInternal,然後寫日誌。
setReplicationInternal上來自然是檢查引數了,然後通過FSDirectory的setReplication,設定新的副本數,並獲取老的副本數。根據新舊數,決定刪除/複製資料塊。
增加副本數通過呼叫updateNeededReplications,為了獲取UnderReplicatedBlocks. update需要的引數,FSNameSystem提供了內部方法countNodes和getReplication,獲得對應的數值(這兩個函式都很簡單)。
proccessOverReplicatedBlock用於減少副本數,它被多個方法呼叫:
主要引數有block,副本數,目標DataNode,源DataNode(用於刪除)。proccessOverReplicatedBlock首先找出block所在的,處於非Decommission狀態的DataNode的資訊,然後呼叫chooseExcessReplicates。chooseExcessReplicates執行:
l 按機架位置,對DatanodeDescriptor進行分組;
l 將DataNode分為兩個集合,分別是一個機架包含一個以上的資料塊的和剩餘的;
l 選擇可以刪除的資料塊(順序是:源DataNode,同一個機架上的,剩餘的),把它加到recentInvalidateSets中。
public voidsetPermission(String src, FsPermission permission
) throwsIOException;
setPermission,用於設定檔案的訪問許可權。非常簡單,首先檢查是否有許可權,然後呼叫FSDirectory.setPermission修改檔案訪問許可權。
public void setOwner(Stringsrc, String username, String groupname
) throws IOException;
public voidsetTimes(String src, long mtime,long atime) throws IOException;
public void setQuota(Stringpath, longnamespaceQuota,longdiskspaceQuota)
throws IOException;
setOwner,設定檔案的檔案主和檔案組,setTimes,設定檔案的訪問時間和修改時間,setQuota,設定某路徑的空間限額和空間額度,和setPermission類似,呼叫FSDirectory的對應方法,簡單。
public booleansetSafeMode(FSConstants.SafeModeAction action)throws IOException;
前面我們已經介紹了NameNode的安全模式,客戶端通過上面的方法,可以讓NameNode進入(SAFEMODE_ENTER)/退出(SAFEMODE_LEAVE)安全模式或查詢(SAFEMODE_GET)狀態。FSNamesystem的setSafeMode處理這個命令,對於進入安全模式的請求,如果系統現在不處於安全模式,那麼建立一個SafeModeInfo物件(建立的這個物件有別於啟動時建立的那個SafeModeInfo,它不會自動退出,因為threshold=1.5f),這標誌著系統進入安全模式。退出安全模式很簡單,將safeMode賦空就可以啦。
public FileStatus[]getListing(String src) throwsIOException;
分析完set*以後,我們來看get*。getListing對應於UNIX系統的ls命令,返回值是FileStatus陣列,FileStatus的類圖如下,它其實給出了檔案的詳細資訊,如大小,檔案主等等。其實,這些資訊都存在INode*中,我們只需要把這些資訊搬到FileStatus中就OK啦。FSNamesystem和FSDirectory中都有同名方法,真正幹活的地方在FSDirectory中。getListing不需要寫日誌。
public long[] getStats() throws IOException;
getStatus得到的是檔案系統的資訊,UNIX對應命令為du,它的實現更簡單,所有的資訊都存放在FSNamesystem物件裡。
publicDatanodeInfo[] getDatanodeReport(FSConstants.DatanodeReportType type)
throws IOException;
getDatanodeReport,獲取當前DataNode的狀態,可能的選項有DatanodeReportType.ALL, IVE和DEAD。FSNamesystem的同名方法呼叫getDatanodeListForReport,通過HostsFileReader讀取對應資訊。
public longgetPreferredBlockSize(String filename) throwsIOException;
getPreferredBlockSize,返回INodeFile.preferredBlockSize,資料塊大小。
public FileStatusgetFileInfo(String src) throwsIOException;
和getListing類似,不再分析。
publicContentSummary getContentSummary(String path) throws IOException;
得到檔案樹的一些資訊,如下圖:
public void metaSave(Stringfilename) throwsIOException;
這個也很簡單,它把系統的metadata輸出/新增到指定檔案上(NameNode所在的檔案系統)。
Hadoop原始碼分析(三零)
軟柿子都捏完了,我們開始啃硬骨頭。前面已經分析過getBlockLocations,create,append,setReplication,setPermission和setOwner,接下來我們繼續回來討論和檔案內容相關的操作。
public voidabandonBlock(Block b, String src, String holder
) throws IOException;
abandonBlock用於放棄一個資料塊。普通的檔案系統中並沒有“放棄”操作,HDFS出現放棄資料塊的原因,如下圖所示。當客戶端通過其他操作(如下面要介紹的addBlock方法)獲取LocatedBlock後,可以開啟到一個block的輸出流,由於從DataNode出錯到NameNode發現這個資訊,需要有一段時間(NameNode長時間收到DataNode心跳),開啟輸出流可能出錯,這時客戶端可以向NameNode請求放棄這個資料塊。
abandonBlock的處理不是很複雜,首先檢查租約(呼叫checkLease方法。block對應的檔案存在,檔案處於構造狀態,租約擁有者匹配),如果通過檢查,呼叫FSDirectory的removeBlock,從INodeFileUnderConstruction/BlocksMap/CorruptReplicasMap中刪除block,然後通過logOpenFile()記錄變化(logOpenFile真是萬能啊)。
public LocatedBlockaddBlock(String src, String clientName) throwsIOException;
寫HDFS的檔案時,如果資料塊被寫滿,客戶端可以通過addBlock建立新的資料塊。具體的建立工作由FSNamesystem的getAdditionalBlock方法完成,當然上來就是一通檢查(是否安全模式,命名/儲存空間限額,租約,資料塊副本數,保證DataNode已經上報資料塊狀態),然後通過ReplicationTargetChooser,選擇複製的目標(如果目標數不夠副本數,又是一個異常),然後,就可以分配資料塊了。allocateBlock建立一個新的Block物件,然後呼叫addBlock,檢查引數後把資料塊加到BlocksMap物件和對應的INodeFile物件中。allocateBlock返回後,getAdditionalBlock還會繼續更新一些需要記錄的資訊,最後返回一個新構造的LocatedBlock。
public booleancomplete(String src, String clientName) throwsIOException;
當客戶端完成對資料塊的寫操作後,呼叫complete完成寫操作。方法complete如果返回是false,那麼,客戶端需要繼續呼叫complete方法。
FSNamesystem的同名方法呼叫completeFileInternal,它會:
l 檢查環境;
l 獲取src對應的INode;
l 如果INode存在,並且處於構造狀態,獲取資料塊;
l 如果獲取資料塊返回空,返回結果CompleteFileStatus.OPERATION_FAILED,FSNamesystem的complete會拋異常返回;
l 如果上報檔案完成的DataNode數不夠系統最小的副本數,返回STILL_WAITING;
l 呼叫finalizeINodeFileUnderConstruction;
l 返回成功COMPLETE_SUCCESS
其中,對finalizeINodeFileUnderConstruction的處理包括:
l 釋放租約;
l 將對應的INodeFileUnderConstruction物件轉換為INodeFile物件,並在FSDirectory進行替換;
l 呼叫FSDirectory.closeFile關閉檔案,其中會寫日誌logCloseFile(path, file)。
l 檢查副本數,如果副本數小於INodeFile中的目標數,那麼新增資料塊複製任務。
我們可以看到,complete一個檔案還是比較複雜的,需要釋放很多的資源。
public voidreportBadBlocks(LocatedBlock[] blocks) throwsIOException;
呼叫reportBadBlocks的地方比較多,客戶端可能呼叫,DataNode上也可能呼叫。
由於上報的是個陣列,reportBadBlocks會迴圈處理,呼叫FSNamesystem的markBlockAsCorrupt方法。markBlockAsCorrupt方法需要兩個引數,blk(資料塊)和dn(所在的DataNode資訊)。如果系統目前副本數大於要求,那麼直接呼叫invalidateBlock方法。
方法invalidateBlock很簡單,在檢查完系統環境以後,先呼叫addToInvalidates方法往FSNamesystem.recentInvalidateSets新增一項,然後呼叫removeStoredBlock方法。
removeStoredBlock被多個方法呼叫,它會執行:
l 從BlocksMap中刪除記錄removeNode(block,node);
l 如果目前系統中還有其他副本,呼叫decrementSafeBlockCount(可能的調整安全模式引數)和updateNeededReplications(跟新可能存在的block複製資訊,例如,現在系統中需要複製1個資料塊,那麼更新後,需要複製2個資料塊);
l 如果目前系統中有多餘資料塊等待刪除(在excessReplicateMap中),那麼移除對應記錄;
l 刪除在CorruptReplicasMap中的記錄(可能有)。
removeStoredBlock其實也是涉及了多處表操作,包括BlocksMap,excessReplicateMap和CorruptReplicasMap。
我們回到markBlockAsCorrupt,如果系統目前副本數小於要求,那麼很顯然,我們需要對資料塊進行復制。首先將現在的資料塊加入到CorruptReplicasMap中,然後呼叫updateNeededReplications,跟新複製資訊。
markBlockAsCorrupt這個流程太複雜了,我們還是畫個圖吧:
Hadoop原始碼分析(三一)
下面是和目錄樹相關的方法。
public booleanrename(String src, String dst) throwsIOException;
更改檔名。呼叫FSNamesystem的renameTo,幹活的是renameToInternal,最終呼叫FSDirectory的renameTo方法,如果成功,更新租約的檔名,如下:
changeLease(src, dst, dinfo);
public booleandelete(String src) throwsIOException;
public booleandelete(String src, booleanrecursive)throwsIOException;
第一個已經廢棄不用,使用第二個方法。
最終使用deleteInternal,該方法呼叫FSDirectory.delete()。
public booleanmkdirs(String src, FsPermission masked) throwsIOException;
在做完一系列檢查以後,呼叫FSDirectory.mkdirs()。
publicFileStatus[] getListing(String src) throws IOException;
前面我們已經討論了。
下面是其它和系統維護管理的方法。
public voidrenewLease(String clientName) throws IOException;
就是呼叫了一下leaseManager.renewLease(holder),沒有其他的事情需要做,簡單。
public void refreshNodes() throws IOException;
還記得我們前面分析過NameNode上有個DataNode線上列表和DataNode離線列表嗎,這個命令可以讓NameNode從新讀這兩個檔案。當然,根據前後DataNode的狀態,一共有4種情況,其中有3種需要修改。
對於從工作狀態變為離線的,需要將上面的DataNode複製到其他的DataNode,需要呼叫updateNeededReplications方法(前面我們已經討論過這個方法了)。
對於從離線變為工作的DataNode,只需要改變一下狀態。
public voidfinalizeUpgrade() throwsIOException;
finalize一個升級,確認客戶端有超級使用者許可權以後,呼叫FSImage.finalizeUpgrade()。
public void fsync(Stringsrc, String client) throwsIOException;
將檔案資訊持久化。在檢查租約資訊後,呼叫FSDirectory的persistBlocks,將檔案的原資訊通過logOpenFile(path, file)寫日誌。
Hadoop原始碼分析(三二)
搞定ClientProtocol,接下來是DatanodeProtocol部分。介面如下:
public DatanodeRegistration register(DatanodeRegistration nodeReg
) throws IOException
用於DataNode向NameNode登記。輸入和輸出引數都是DatanodeRegistration,類圖如下:
前面討論DataNode的時候,我們已經講過了DataNode的註冊過程,我們來看NameNode的過程。下面是主要步驟:
l 檢查該DataNode是否能接入到NameNode;
l 準備應答,更新請求的DatanodeID;
l 從datanodeMap(儲存了StorageID à DatanodeDescriptor的對映,用於保證DataNode使用的Storage的一致性)得到對應的DatanodeDescriptor,為nodeS;
l 從Host2NodesMap(主機名到DatanodeDescriptor陣列的對映)中獲取DatanodeDescriptor,為nodeN;
l 如果nodeN!=null同時nodeS!=nodeN(後面的條件表明表明DataNode上使用的Storage發生變化),那麼我們需要先在系統中刪除nodeN(removeDatanode,下面再討論),並在Host2NodesMap中刪除nodeN;
l 如果nodeS存在,表明前面已經註冊過,則:
1. 更新網路拓撲(儲存在NetworkTopology),首先在NetworkTopology中刪除nodeS,然後跟新nodeS的相關資訊,呼叫resolveNetworkLocation,獲得nodeS的位置,並從新加到NetworkTopology裡;
2. 更新心跳資訊(register也是心跳);
l 如果nodeS不存在,表明這是一個新註冊的DataNode,執行
1. 如果註冊資訊的storageID為空,表明這是一個全新的DataNode,分配storageID;
2. 建立DatanodeDescriptor,呼叫resolveNetworkLocation,獲得位置資訊;
3. 呼叫unprotectedAddDatanode(後面分析)新增節點;
4. 新增節點到NetworkTopology中;
5. 新增到心跳陣列中。
上面的過程,我們遺留了兩個方法沒分析,removeDatanode的流程如下:
l 更新系統的狀態,包括capacityTotal,capacityUsed,capacityRemaining和totalLoad;
l 從心跳陣列中刪除節點,並標記節點isAlive屬性為false;
l 從BlocksMap中刪除這個節點上的所有block,用了(三零)分析到的removeStoredBlock方法;
l 呼叫unprotectedAddDatanode;
l 從NetworkTopology中刪除節點資訊。
unprotectedAddDatanode很簡單,它只是更新了Host2NodesMap的資訊。
Hadoop原始碼分析(三三)
下面來看一個大傢伙:
public DatanodeCommand sendHeartbeat(DatanodeRegistration nodeReg,
long capacity,
long dfsUsed,
longremaining,
int xmitsInProgress,
int xceiverCount) throws IOException
DataNode傳送到NameNode的心跳資訊。細心的人會發現,請求的內容還是DatanodeRegistration,應答換成DatanodeCommand了。DatanodeCommand類圖如下:
前面介紹DataNode時,已經分析過了DatanodeCommand支援的命令:
DNA_TRANSFER:拷貝資料塊到其他DataNode
DNA_INVALIDATE:刪除資料塊
DNA_SHUTDOWN:關閉DataNode
DNA_REGISTER:DataNode重新註冊
DNA_FINALIZE:提交升級
DNA_RECOVERBLOCK:恢復資料塊
有了上面這些基礎,我們來看FSNamesystem.handleHeartbeat的處理過程:
l 呼叫getDatanode方法找對應的DatanodeDescriptor,儲存於變數nodeinfo(可能為null)中,如果現有NameNode上記錄的StorageID和請求的不一樣,返回DatanodeCommand.REGISTER,讓DataNode從新註冊。
l 如果發現當前節點需要關閉(已經isDecommissioned),拋異常DisallowedDatanodeException。
l nodeinfo是空或者現在狀態不是活的,返回DatanodeCommand.REGISTER,讓DataNode從新註冊。
l 更新系統的狀態,包括capacityTotal,capacityUsed,capacityRemaining和totalLoad;
l 接下來按順序看有沒有可能的恢復資料塊/拷貝資料塊到其他DataNode/刪除資料塊/升級命令(不討論)。一次返回只能有一條命令,按上面優先順序。
下面分析應答的命令是如何構造的。
首先是DNA_RECOVERBLOCK(恢復資料塊),那是個非常長的流程,同時需要回去討論DataNode上的一些功能,我們在後面介紹它。
對於DNA_TRANSFER(拷貝資料塊到其他DataNode),從DatanodeDescriptor.replicateBlocks中取出儘可能多的專案,放到BlockCommand中。在DataNode中,命令由transferBlocks執行,前面我們已經分析過啦。
刪除資料塊DNA_INVALIDATE也很簡單,從DatanodeDescriptor.invalidateBlocks中獲取儘可能多的專案,放到BlockCommand中,DataNode中的動作,我們也分析過。
我們來討論DNA_RECOVERBLOCK(恢復資料塊),在討論DataNode的過程中,我們沒有講這個命令是用來幹什麼的,還有它在DataNode上的處理流程,是好好分析分析這個流程的時候了。DNA_RECOVERBLOCK命令通過DatanodeDescriptor.getLeaseRecoveryCommand獲取,獲取過程很簡單,將DatanodeDescriptor物件中佇列recoverBlocks的所有內容取出,放入BlockCommand的Block中,設定BlockCommand為DNA_RECOVERBLOCK,就OK了。
關鍵是,這個佇列裡的資訊是用來幹什麼的。我們先來看那些操作會向這個佇列加東西,呼叫關係圖如下:
租約有兩個超時時間,一個被稱為軟超時(1分鐘),另一個是硬超時(1小時)。如果租約軟超時,那麼就會觸發internalReleaseLease方法,如下:
voidinternalReleaseLease(Lease lease, String src) throws IOException
該方法執行:
l 檢查src對應的INodeFile,如果不存在,不處於構造狀態,返回;
l 檔案處於構造狀態,而檔案目標DataNode為空,而且沒有資料塊,則finalize該檔案(該過程在completeFileInternal中已經討論過,租約在過程中被釋放),並返回;
l 檔案處於構造狀態,而檔案目標DataNode為空,資料塊非空,則將最後一個資料塊存放的DataNode目標取出(在BlocksMap中),然後設定為檔案現在的目標DataNode;
l 呼叫INodeFileUnderConstruction.assignPrimaryDatanode,該過程會挑選一個目前還活著的DataNode,作為租約的主節點,並把<block,block目標DataNode陣列>加到該DataNode的recoverBlocks佇列中;
l 更新租約。
上面分析了租約軟超時的情況下NameNode發生租約恢復的過程。DataNode上收到這個命令後,將會啟動一個新的執行緒,該執行緒為每個Block呼叫recoverBlock方法:recoverBlock(blocks[i], false, targets[i], true)。
private LocatedBlockrecoverBlock(Block block, booleankeepLength,
DatanodeID[] datanodeids, booleancloseFile) throwsIOException
它的流程並不複雜,但是分支很多,如下圖(藍線是上面輸入,沒有異常走的流程):
首先是判斷進來的Block是否在ongoingRecovery中,如果存在,返回,不存在,加到ongoingRecovery中。
接下來是個迴圈(框內部分是迴圈體,奇怪,沒找到表示迴圈的符號),對每一個DataNode,獲取Block的BlockMetaDataInfo(下面還會分析),這需要呼叫到DataNode間通訊的介面上的方法getBlockMetaDataInfo。然後分情況看要不要把資訊儲存下來(圖中間的幾個判斷),其中包括要進行同步的節點。
根據引數,更新資料塊資訊,然後呼叫syncBlock並返回syncBlock生產的LocatedBlock。
上面的這一圈,對於我們這個輸入常數來說,就是把Block的長度,更新成為擁有最新時間戳的最小長度值,並得到要更新的節點列表,然後呼叫syncBlock更新各節點。
getBlockMetaDataInfo用於獲取Block的BlockMetaDataInfo,包括Block的generationStamp,最後校驗時間,同時它還會檢查資料塊檔案的元資訊,如果出錯,會丟擲異常。
syncBlock定義如下:
private LocatedBlock syncBlock(Block block, List<BlockRecord>syncList,
booleancloseFile)
它的流程是:
l 如果syncList為空,通過commitBlockSynchronization向NameNode提交這次恢復;
l syncList不為空,那麼先NameNode申請一個新的Stamp,並根據上面得到的長度,構造一個新的資料塊資訊newblock;
l 對於沒一個syncList中的DataNode,呼叫它們上面的updateBlock,更新資訊;更新資訊如果返回OK,記錄下來;
l 如果更新了資訊的DataNode不為空,呼叫commitBlockSynchronization提交這次恢復;並生成LocatedBlock;
l 如果更新的DataNode為空,拋異常。
通過syncBlock,所有需要恢復的DataNode上的Block資訊都被更新。
DataNode上的updateBlock方法我們前面已經介紹了,就不再分析。
下面我們來看NameNode的commitBlockSynchronization方法,它在上面的過程中用於提交資料塊恢復:
public voidcommitBlockSynchronization(Block block,
longnewgenerationstamp, longnewlength,
booleancloseFile, booleandeleteblock, DatanodeID[] newtargets
)
引數分別是block,資料塊;newgenerationstamp,新的時間戳;newlength,新長度;closeFile,是否關閉檔案,deleteblock,是否刪除檔案;newtargets,新的目標列表。
上面的兩次呼叫,輸入引數分別是:
commitBlockSynchronization(block, 0, 0, closeFile, true,DatanodeID.EMPTY_ARRAY);
commitBlockSynchronization(block,newblock.getGenerationStamp(), newblock.getNumBytes(), closeFile,false, nlist);
處理流程是:
l 引數檢查;
l 獲取對應的檔案,記為pendingFile;
l BlocksMap中刪除老的資訊;
l 如果deleteblock為true,從pendingFile刪除Block記錄;
l 否則,更新Block的資訊;
l 如果不關閉檔案,那麼寫日誌儲存更新,返回;
l 關閉檔案的話,呼叫finalizeINodeFileUnderConstruction。
這塊比較複雜,不僅涉及了NameNode和DataNode間的通訊,而且還存在對於DataNode和DataNode間的通訊(DataNode間的通訊就只支援這兩個方法,如下圖)。後面介紹DFSClient的時候,我們還會再回來分析它的功能,以獲取全面的理解。
Hadoop原始碼分析(三四)
繼續對NameNode實現的介面做分析。
public DatanodeCommand blockReport(DatanodeRegistration nodeReg,
long[] blocks) throws IOException
DataNode向NameNode報告它擁有的所有資料塊,其中,引數blocks包含了陣列化以後資料塊的資訊。FSNamesystem.processReport處理這個請求。一番檢查以後,呼叫DatanodeDescriptor的reportDiff,將上報的資料塊分成三組,分別是:
l 刪除:其它情況;
l 加入:BlocksMap中有資料塊,但目前的DatanodeDescriptor上沒有對應資訊;
l 使無效:BlocksMap中沒有找到資料塊。
對於刪除的資料塊,呼叫removeStoredBlock,這個方法我們前面已經分析過啦。
對應需要加入的資料塊,呼叫addStoredBlock方法,處理流程如下:
l 從BlocksMap獲取現在的資訊,記為storedBlock;如果為空,返回;
l 記錄block和DatanodeDescriptor的關係;
l 新舊資料塊記錄不是同一個(我們這個流程是肯定不是啦):
1. 如果現有資料塊長度為0,更新為上報的block的值;
2. 如果現有資料塊長度比新上報的長,invalidateBlock(前面分析過,很簡單的一個方法)當前資料塊;
3. 如果現有資料塊長度比新上報的小,那麼會刪除所有老的資料塊(還是通過invalidateBlock),並更新BlocksMap中資料塊的大小資訊;
4. 跟新可用儲存空間等資訊;
l 根據情況確定資料塊需要複製的數目和目前副本數;
l 如果檔案處於構建狀態或系統現在是安全模式,返回;
l 處理當前副本數和檔案的目標副本數不一致的情況;
l 如果當前副本數大於系統設定門限,開始刪除標記為無效的資料塊。
還是給個流程圖吧:
對於標記為使無效的資料塊,呼叫addToInvalidates方法,很簡單的方法,直接加到FSNamesystem的成員變數recentInvalidateSets中。
public voidblockReceived(DatanodeRegistration registration,
Blockblocks[],
String[] delHints)
DataNode可以通過blockReceived,向NameNode報告它最近接受到的資料塊,同時給出如果資料塊副本數太多時,可以刪除資料塊的節點(引數delHints)。在DataNode中,這個資訊是通過方法notifyNamenodeReceivedBlock,記錄到對應的列表中。
NameNode上的處理不算複雜,對輸入引數進行檢查以後,呼叫上面分析的addStoredBlock方法。然後在PendingReplicationBlocks物件中刪除相應的block。
public voiderrorReport(DatanodeRegistration registration,
int errorCode,
String msg)
向NameNode報告DataNode上的一個錯誤,如果錯誤是硬碟錯,會刪除該DataNode,其它情況只是簡單地記錄收到一條出錯資訊。
publicNamespaceInfo versionRequest() throws IOException;
從NameNode上獲取NamespaceInfo,該資訊用於構造DataNode上的DataStorage。
UpgradeCommand processUpgradeCommand(UpgradeCommand comm) throws IOException;
我們不討論。
public voidreportBadBlocks(LocatedBlock[] blocks) throws IOException
報告錯誤的資料塊。NameNode會迴圈呼叫FSNamesystem的markBlockAsCorrupt方法。處理流程不是很複雜,找對應的INodeFile,如果副本數夠,那麼呼叫invalidateBlock,使該DataNode上的Block無效;如果副本數不夠,加Block到CorruptReplicasMap中,然後準備對好資料塊進行復制。
目前為止,我們已經完成了NameNode上的ClientProtocol和DatanodeProtocol的分析了,NamenodeProtocol我們在理解從NameNode的時候,才會進行分析。
Hadoop原始碼分析(三五)
除了對外提供的介面,NameNode上還有一系列的執行緒,不斷檢查系統的狀態,下面是這些執行緒的功能分析。
在NameNode中,定義瞭如下執行緒:
Daemon hbthread= null; // HeartbeatMonitor thread
publicDaemon lmthread =null; // LeaseMonitor thread
Daemon smmthread= null; // SafeModeMonitor thread
publicDaemon replthread =null; // Replication thread
privateDaemon dnthread =null;
PendingReplicationBlocks中也有一個執行緒:
Daemon timerThread= null;
NameNode內嵌的HTTP伺服器中自然也有執行緒,這塊我們就不分析啦。
HttpServer infoServer;
心跳執行緒用於對DataNode的心態進行檢查,以間隔heartbeatRecheckInterval執行heartbeatCheck方法。如果在一定時間內沒收到DataNode的心跳資訊,我們就認為該節點已經死掉,呼叫removeDatanode(前面分析過)將DataNode標記為無效。
租約lmthread用於檢查租約的硬超時,如果租約硬超時,呼叫前面分析過的internalReleaseLease,釋放租約。
smmthread執行的SafeModeMonitor我們前面已經分析過了。
replthread執行ReplicationMonitor,這個執行緒會定期呼叫computeDatanodeWork和processPendingReplications。
computeDatanodeWork會執行computeDatanodeWork或computeInvalidateWork。computeDatanodeWork從neededReplications中掃描,取出需要複製的項,然後:
l 檢查檔案不存在或者處於構造狀態;如果是,從佇列中刪除複製項,退出對複製項的處理(接著處理下一個);
l 得到當前資料塊副本數並選擇複製的源DataNode,如果空,退出對複製項的處理;
l 再次檢查副本數(很可能有DataNode從故障中恢復),如果發現不需要複製,從佇列中刪除複製項,退出對複製項的處理;
l 選擇複製的目標,如果目標空,退出對複製項的處理;
l 將複製的資訊(資料塊和目標DataNode)加入到源目標DataNode中;在目標DataNode中記錄複製請求;
l 從佇列中將複製項移動到pendingReplications。
可見,這個方法執行後,複製項從neededReplications挪到pendingReplications中。DataNode在某次心跳的應答中,可以拿到相應的資訊,執行復制操作。
computeInvalidateWork當然是用於刪除無效的資料塊。它的主要工作在invalidateWorkForOneNode中完成。和上面computeDatanodeWork類似,不過它的處理更簡單,將recentInvalidateSets的資料通過DatanodeDescriptor.addBlocksToBeInvalidated挪到DataNode中。
dnthread執行的是DecommissionedMonitor,它的run方法週期呼叫decommissionedDatanodeCheck,再到checkDecommissionStateInternal,定期將完成Decommission任務的DataNode狀態從DECOMMISSION_INPROGRESS改為DECOMMISSIONED。
PendingReplicationMonitor中的執行緒用於對處在等待複製狀態的資料塊進行檢查。如果發現長時間該資料塊沒被複制,那麼會將它挪到timedOutItems中。請參考PendingReplicationBlocks的討論。
infoServer的相關執行緒我們就不分析了,它們都用於處理HTTP請求。
上面已經總結了NameNode上的一些為特殊任務啟動的執行緒,除了這些執行緒,NameNode上還執行著RPC伺服器的相關執行緒,具體可以看前面章節。
在我們開始分析Secondary NameNode前,我們給出了以NameNode上一些狀態轉移圖,大家可以通過這個圖,更好理解NameNode。
NameNode:
DataNode:
檔案:
Block,比較複雜:
上面的圖不是很嚴格,只是用於幫助大家理解NameNode對Block複雜的處理過程。
稍微說明一下,“Block in inited DataNode”表明這個資料塊在一個剛初始化的DataNode上。“Block in INodeFile”是資料塊屬於某個檔案,“Block inINodeFileUnderConstruction”表明這資料塊屬於一個正在構建的檔案,當然,處於這個狀態的Block可能因為租約恢復而轉移到“Block in Recover”。右上方描述了需要複製的資料塊的狀態,UnderReplicatedBlocks和PendingReplicationBlocks的區別在於Block是否被插入到某一個DatanodeDescriptor中。Corrupt和Invalidate的就好理解啦。
Hadoop原始碼分析(三六)
轉戰進入Secondary NameNode,前面的分析我們有事也把它稱為從NameNode,從NameNode在HDFS裡是個小配角。
跟Secondary NameNode有關係的類不是很多,如下圖:
首先要討論的是NameNode和Secondary NameNode間的通訊。NameNode上實現了介面NamenodeProtocol(如下圖),就是用於NameNode和Secondary NameNode間的命令通訊。
NameNode和Secondary NameNode間資料的通訊,使用的是HTTP協議,HTTP的容器用的是jetty,TransferFsImage是檔案傳輸的輔助類。
GetImageServlet的doGet方法目前支援取FSImage(getimage),取日誌(getedit)和存FSImage(putimage)。例如:
http://localhost:50070/getimage?getimage
可以獲取FSImage。
http://localhost:50070/getimage?getedit
可以獲取日誌檔案。
儲存FSImage需要更多的引數,它的流程很好玩,SecondaryNameNode傳送一個HTTP請求到NameNode,啟動NameNode上一個HTTP客戶端到SecondaryNameNode上去下載FSImage,下載需要的一些資訊,都放在從NameNode的HTTP請求中。
我們先來考察Secondary NameNode持久化儲存的資訊:
[hadoop@localhostnamesecondary]$ ls –R
.:
current image in_use.lock previous.checkpoint
./current:
edits fsimage fstime VERSION
./image:
fsimage
./previous.checkpoint:
edits fsimage fstime VERSION
in_use.lock的用法和前面NameNode,DataNode的是一樣的。對比NameNode儲存的資訊,我們可以發現Secondary NameNode上儲存多了一個previous.checkpoint。CheckpointStorage就是應用於Secondary NameNode的儲存類,它繼承自FSImage,只新增了很少的方法。
previous.checkpoint目錄儲存了上一個checkpoint的資訊(current裡的永遠是最新的),臨時目錄用於建立新checkpoint,成功後,老的checkpoint儲存在previous.checkpoint目錄中。狀態圖如下(基類FSImage用的是黑色):
至於上面目錄下檔案的內容,和FSImage是一樣的。
CheckpointStorage除了上面圖中的startCheckpoint和endCheckpoint方法(上圖給出了正常流程),還有:
voidrecoverCreate(Collection<File> dataDirs,
Collection<File> editsDirs) throwsIOException
和FSImage.coverTransitionRead類似,用於分析現有目錄,建立目錄(如果不存在)並從可能的錯誤中恢復。
privatevoid doMerge(CheckpointSignature sig)throws IOException
doMerge被類SecondaryNameNode的同名方法呼叫,我們後面再分析。
Hadoop原始碼分析(三七)
Secondary NameNode的成員變數很少,主要的有:
privateCheckpointStorage checkpointImage;
Secondary NameNode使用的Storage
privateNamenodeProtocol namenode;
和NameNode通訊的介面
privateHttpServer infoServer;
傳輸檔案用的HTTP伺服器
main方法是Secondary NameNode的入口,它最終啟動執行緒,執行SecondaryNameNode的run。啟動前的對SecondaryNameNode的構造過程也很簡單,主要是建立和NameNode通訊的介面和啟動HTTP伺服器。
SecondaryNameNode的run方法每隔一段時間執行doCheckpoint(),從NameNode的主要工作都在這一個方法裡。這個方法,總的來說,會從NameNode上取下FSImage和日誌,然後再本地合併,再上傳回NameNode。這個過程結束後,從NameNode上保持了NameNode上持久化資訊的一個備份,同時,NameNode上已經完成合併到FSImage的日誌可以拋棄,一箭雙鵰。
具體的的流程是:
1:呼叫startCheckpoint,為接下來的工作準備空間。startCheckpoint會在內部做一系列的檢查,然後呼叫CheckpointStorage的startCheckpoint方法,建立目錄。
2:呼叫namenode的rollEditLog方法,開始一次新的檢查點過程。呼叫會返回一個CheckpointSignature(檢查點簽名),在上傳合併完的FSImage時,會使用這個簽名。
Namenode的rollEditLog方法最終呼叫的是FSImage的同名方法,前面提到過這個方法,作用是關閉往edits上寫的日誌,開啟日誌到edits.new。明顯,在Secondary NameNode下載fsimage和日誌的時候,對名稱空間的修改,將保持在edits.new的日誌中。
注意,如果FSImage這個時候的狀態(看下面的狀態機,前面出現過一次)不是出於CheckpointStates.ROLLED_EDITS,將拋異常結束這個過程。
3:通過downloadCheckpointFiles下載fsimage和日誌,並設定本地檢查點狀態為CheckpointStates.UPLOAD_DONE。
4:合併日誌的內容到fsimage中。過程很簡單,CheckpointStorage利用繼承自FSImage的loadFSImage載入fsimage,loadFSEdits應用日誌,然後通過saveFSImage儲存。很明顯,現在儲存在硬碟上的fsimage是合併日誌的內容以後的檔案。
5:使用putFSImage上傳合併日誌後的fsimage(讓NameNode通過HTTP到從NameNode取檔案)。這個過程中,NameNode會:
呼叫NameNode的FSImage.validateCheckpointUpload,檢查現在的狀態;
利用HTTP,從Secondary NameNode獲取新的fsimage;
更新結束後設定新狀態。
6:呼叫NameNode的rollFsImage,最終呼叫FSImage的rollFsImage方法,前面我們已經分析過了。
7:呼叫本地endCheckpoint方法,結束一次doCheckpoint流程。
其實前面在分析FSImage的時候,我們在不瞭解SecondaryNameNode的情況下,分析了很多和Checkpoint相關的方法,現在我們終於可以有一個比較統一的瞭解了,下面給出NameNode和Secondary NameNode的儲存系統在這個流程中的狀態轉移圖,方便大家理解。
圖中右側的狀態轉移圖:
檔案系統上的目錄的變化(三六中出現):
Hadoop原始碼分析(三八)
我們可以開始從系統的外部來了解HDFS了,DFSClient提供了連線到HDFS系統並執行檔案操作的基本功能。DFSClient也是個大傢伙,我們先分析它的一些內部類。我們先看LeaseChecker。租約是客戶端對檔案寫操作時需要獲取的一個憑證,前面分析NameNode時,已經瞭解了租約,INodeFileUnderConstruction的關係,INodeFileUnderConstruction只有在檔案寫的時候存在。客戶端的租約管理很簡單,包括了增加的put和刪除的remove方法,run方法會定期執行,並通過ClientProtocl的renewLease,自動延長租約。
接下來我們來分析內部為檔案讀引入的類。
InputStream是系統的虛類,提供了3個read方法,一個skip(跳過資料)方法,一個available方法(目前流中可讀的位元組數),一個close方法和幾個在輸入流中做標記的方法(mark:標記,reset:回到標記點和markSupported:能力查詢)。
FSInputStream也是一個虛類,它將介面Seekable和PositionedReadable混插到類中。Seekable提供了可以在流中定位的能力(seek,getPos和seekToNewSource),而PositionedReadable提高了從某個位置開始讀的方法(一個read方法和兩個readFully方法)。
FSInputChecker在FSInputStream的基礎上,加入了HDFS中需要的校驗功能。校驗在readChecksumChunk中實現,並在內部的read1方法中呼叫。所有的read呼叫,最終都是使用read1讀資料並做校驗。如果校驗出錯,丟擲異常ChecksumException。
有了支援校驗功能的輸入流,就可以開始構建基於Block的輸入流了。我們先回顧前面提到的讀資料塊的請求協議:
然後我們來分析一下建立BlockReader需要的引數,newBlockReader最複雜的請求如下:
public staticBlockReader newBlockReader( Socket sock, String file,
longblockId,
longgenStamp,
longstartOffset,long len,
intbufferSize,boolean verifyChecksum,
StringclientName)
throws IOException
其中,sock為到DataNode的socket連線,file是檔名(只是用於日誌輸出),其它的引數含義都很清楚,和協議基本是一一對應的。該方法會和DataNode進行對話,傳送上面的讀資料塊的請求,處理應答並構造BlockReader物件(BlockReader的建構函式基本上只有賦值操作)。
BlockReader的readChunk用於處理DataNode送過來的資料,格式前面我們已經討論過了,如下圖。
讀資料用的read,會呼叫父類FSInputChecker的read,最後呼叫readChunk,如下:
read如果發現讀到正確的校驗碼,則用過checksumOk方法,向DataNode傳送成功應達。
BlockReader的主要流程就介紹完了,接下來分析DFSInputStream,它封裝了DFSClient讀檔案內容的功能。在它的內部,不但要處理和NameNode的通訊,同時通過BlockReader,處理和DataNode的互動。
DFSInputStream記錄Block的成員變數是:
privateLocatedBlocks locatedBlocks = null;
它不但保持了檔案對應的Block序列,還保持了管理Block的DataNode的資訊,是DFSInputStream中最重要的成員變數。DFSInputStream的建構函式,通過類內部的openInfo方法,獲取這個變數的值。openInfo間接呼叫了NameNode的getBlockLocations,獲取LocatedBlocks。
DFSInputStream中處理資料塊位置的還有下面一些函式:
synchronizedList<LocatedBlock> getAllBlocks()throwsIOException
privateLocatedBlock getBlockAt(long offset)throwsIOException
private synchronizedList<LocatedBlock> getBlockRange(longoffset,
long length)
private synchronizedDatanodeInfo blockSeekTo(long target)throwsIOException
它們的功能都很清楚,需要注意的是他們處理過程中可能會呼叫再次呼叫NameNode的getBlockLocations,使得流程比較複雜。blockSeekTo還會建立對應的BlockReader物件,它被幾個重要的方法呼叫(如下圖)。在開啟到DataNode之前,blockSeekTo會呼叫chooseDataNode,選擇一個現在活著的DataNode。
通過上面的分析,我們已經知道了在什麼時候會連線NameNode,什麼時候會開啟到DataNode的連線。下面我們來看讀資料。read方法定義如下:
public intread(long position,byte[]buffer, int offset,intlength)
該方法會從流的position位置開始,讀取最多length個byte到buffer中offset開始的空間中。引數檢測完以後,通過getBlockRange獲取要讀取的資料塊對應的block範圍,然後,利用fetchBlockByteRange方法,讀取需要的資料。
fetchBlockByteRange從某一個資料塊中讀取一段資料,定義如下:
private voidfetchBlockByteRange(LocatedBlock block, longstart,
long end,byte[] buf,intoffset)
由於讀取的內容都在一個資料塊內部,這個方法會建立BlockReader,然後利用BlockReader的readAll方法,讀取資料。讀的過程中如果發生校驗錯,那麼,還會通過reportBadBlocks,向NameNode報告校驗錯。
另一個讀方法是:
public synchronized intread(byte buf[],int off,int len)throwsIOException
它在流的當前位置(可以通過seek方法調整)讀取資料。首先它會判斷當前流的位置,如果已經越過了物件現在的blockReader能讀取的範圍(當上次read讀到資料塊的尾部時,會發生這中情況),那麼通過blockSeekTo開啟到下一個資料塊的blockReader。然後,read在當前的這個資料塊中通過readBuffer讀資料。主要,這個read方法只在一塊資料塊中讀取資料,就是說,如果還有空間可以存放資料但已經到了資料塊的尾部,它不會開啟到下一個資料塊的BlockReader繼續讀,而是返回,返回值包含了以讀取資料的長度。
DFSDataInputStream是一個Wrapper(DFSInputStream),我們就不討論了。
Hadoop原始碼分析(三九)
接下來當然是分析輸出流了。
處於繼承體系的最上方是OutputStream,它實現了Closeable(方法close)和Flushable(方法flush)介面,提供了3個不同形式的write方法,這些方法的含義都很清楚。接下來的是FSOutputSummer,它引入了HDFS寫資料時需要的計算校驗和的功能。FSOutputSummer的write方法會呼叫write1,write1中計算校驗和並將使用者輸入的資料拷貝到物件的緩衝區中,緩衝區滿了以後會呼叫flushBuffer,flushBuffer最終呼叫還是虛方法的writeChunk,這個時候,緩衝區對應的校驗和緩衝區對的內容都已經準備好了。通過這個類,HDFS可以把一個流轉換成為DataNode資料介面上的包格式(前面我們討論過這個包的格式,如下)。
DFSOutputStream繼承自FSOutputSummer,是一個非常複雜的類,它包含了幾個內部類。我們先分析Packet,其實它對應了上面的資料包,有了上面的圖,這個類就很好理解了,它的成員變數和上面資料塊包含的資訊基本一一對應。建構函式需要的引數有pktSize,包的大小,chunksPerPkt,chunk的數目(chunk是一個校驗單元)和該包在Block中的偏移量offsetInBlock。writeData和writeChecksum用於往緩衝區裡寫資料/校驗和。getBuffer使用者獲得整個包,包括包頭和資料。
DataStreamer和ResponseProcessor用於寫包/讀應答,和我們前面討論DataNode的Pipe寫時類似,客戶端寫資料也需要兩個執行緒,下圖擴充套件了我們在討論DataNode處理寫時的示意圖,包含了客戶端:
DataStreamer啟動後進入一個迴圈,在沒有錯誤和關閉標記為false的情況下,該迴圈首先呼叫processDatanodeError,處理可能的IO錯誤,這個過程比較複雜,我們在後面再討論。
接著DataStreamer會在dataQueue(資料佇列)上等待,直到有資料出現在佇列上。DataStreamer獲取一個資料包,然後判斷到DataNode的連線是否是開啟的,如果不是,通過DFSOutputStream.nextBlockOutputStream開啟到DataNode的連線,並啟動ResponseProcessor執行緒。
DataNode的連線準備好以後,DataStreamer獲取資料包緩衝區,然後將資料包從dataQueue佇列挪到ackQueue佇列,最後通過blockStream,寫資料。如果資料包是最後一個,那麼,DataStreamer將會寫一個長度域為0的包,指示DataNode資料傳輸結束。
DataStreamer的迴圈在最後一個資料包寫出去以後,會等待直到ackQueue佇列為空(表明所有的應答已經被接收),然後做清理動作(包括關閉socket連線,ResponseProcessor執行緒等),退出執行緒。
ResponseProcessor相對來說比較簡單,就是等待來自DataNode的應答。如果是成功的應答,則刪除在ackQueue的包,如果有錯誤,那麼,記錄出錯的DataNode,並設定標誌位。
Hadoop原始碼分析(四零)
有了上面的基礎,我們可以來解剖DFSOutputStream了。先看建構函式:
privateDFSOutputStream(String src,longblockSize, Progressable progress,
intbytesPerChecksum)throws IOException
DFSOutputStream(String src, FsPermissionmasked, boolean overwrite,
shortreplication, long blockSize,Progressable progress,
intbuffersize, intbytesPerChecksum) throwsIOException
DFSOutputStream(String src, intbuffersize, Progressable progress,
LocatedBlock lastBlock, FileStatusstat,
intbytesPerChecksum)throwsIOException {
這些建構函式的引數主要有:檔名src;進度回撥函式progress(預留介面,目前未使用);資料塊大小blockSize;Block副本數replication;每個校驗chunk的大小bytesPerChecksum;檔案許可權masked;是否覆蓋原檔案標記overwrite;檔案狀態資訊stat;檔案的最後一個Block資訊lastBlock;buffersize(?未見引用)。
後面兩個建構函式會呼叫第一個建構函式,這個函式會呼叫父類的建構函式,並設定物件的src,blockSize,progress和checksum屬性。
第二個建構函式會呼叫namenode.create方法,在檔案空間中建立檔案,並啟動DataStreamer,它被DFSClient的create方法呼叫。第三個建構函式被DFSClient的append方法呼叫,顯然,這種情況比價複雜,檔案擁有一些資料塊,新增資料往往新增在最後的資料塊上。同時,append方法呼叫時,Client已經知道了最後一個Block的資訊和檔案的一些資訊,如FileStatus中包含的Block大小,檔案許可權位等等。結合這些資訊,建構函式需要計算並設定一些物件成員變數的值,並試圖從可能的錯誤中恢復(呼叫processDatanodeError),最後啟動DataStreamer。
我們先看正常流程,前面已經分析過,通過FSOutputSummer,HDFS客戶端能將流轉換成package,這個包是通過writeChunk,傳送出去的,下面是它們的呼叫關係。
在檢查完一系列的狀態以後,writeChunk先等待,直到dataQueue中未傳送的包小於門限值。如果現在沒有可用的Packet物件,則建立一個Packet物件,往Packet中寫資料,包括校驗值和資料。如果資料包被寫滿,那麼,將它放入傳送佇列dataQueue中。writeChunk的過程比較簡單,這裡的寫入,也只是把資料寫到本地佇列,等待DataStreamer傳送,沒有實際寫到DataNode上。
createBlockOutputStream用於建立到第一個DataNode的連線,它的宣告如下:
private booleancreateBlockOutputStream(DatanodeInfo[] nodes, String client,
booleanrecoveryFlag)
nodes是所有接收資料的DataNode列表,client就是客戶端名稱,recoveryFlag指示是否是為錯誤恢復建立的連線。createBlockOutputStream很簡單,開啟到第一個DataNode的連線,然後傳送下面格式的資料包,並等待來自DataNode的Ack。如果出錯,記錄出錯的DataNode在nodes中的位置,設定errorIndex並返回false。
當recoveryFlag指示為真時,意味著這次寫是一次恢復操作,對於DataNode來說,這意味著為寫準備的臨時檔案(在tmp目錄中)可能已經存在,需要進行一些特殊處理,具體請看FSDataset的實現。
當Client寫資料需要一個新的Block的時候,可以呼叫nextBlockOutputStream方法。
privateDatanodeInfo[] nextBlockOutputStream(String client)throwsIOException
這個方法的實現很簡單,首先呼叫locateFollowingBlock(包含了重試和出錯處理),通過namenode.addBlock獲取一個新的資料塊,返回的是DatanodeInfo列表,有了這個列表,就可以建立寫資料的pipe了。下一個大動作就是呼叫上面的createBlockOutputStream,建立到DataNode的連線了。
有了上面的準備,我們來分析processDatanodeError,它的主要流程是:
l 引數檢查;
l 關閉可能還開啟著的blockStream和blockReplyStream;
l 將未收到應答的資料塊(在ackQueue中)挪到dataQueue中;
l 迴圈執行:
1. 計算目前還活著的DataNode列表;
2. 選擇一個主DataNode,通過DataNode RPC的recoverBlock方法啟動它上面的恢復過程;
3. 處理可能的出錯;
4. 處理恢復後Block可能的變化(如Stamp變化);
5. 呼叫createBlockOutputStream到DataNode的連線。
l 啟動ResponseProcessor。
這個過程涉及了DataNode上的recoverBlock方法和createBlockOutputStream中可能的Block恢復,是一個相當耗資源的方法,當系統出錯的概率比較小,而且資料塊上能恢復的資料很多(平均32M),還是值得這樣做的。
寫的流程就分析到著,接下來我們來看流的關閉,這個過程也涉及了一系列的方法,它們的呼叫關係如下:
flushInternal會一直等待到傳送佇列(包括可能的currentPacket)和應答佇列都為空,這意味著資料都被DataNode順利接收。
sync作用和UNIX的sync類似,將寫入資料持久化。它首先呼叫父類的flushBuffer方法,將可能還沒拷貝到DFSOutputStream的資料拷貝回來,然後呼叫flushInternal,等待所有的資料都寫完。然後呼叫namenode.fsync,持久化名稱空間上的資料。
closeInternal比較複雜一點,它首先呼叫父類的flushBuffer方法,將可能還沒拷貝到DFSOutputStream的資料拷貝回來,然後呼叫flushInternal,等待所有的資料都寫完。接著結束兩個工作執行緒,關閉socket,最後呼叫amenode.complete,通知NameNode結束一次寫操作。close方法先呼叫closeInternal,然後再本地的leasechecker中移除對應的資訊。
Hadoop原始碼分析(四一)
前面分析的DFSClient內部類,佔據了這個類的實現部分的2/3,我們來看剩下部分。
DFSClient的成員變數不多,而且大部分是系統的預設配置引數,其中比較重要的是到NameNode的RPC客戶端:
public final ClientProtocol namenode;
final private ClientProtocol rpcNamenode;
它們的差別是namenode在rpcNamenode的基礎上,增加了失敗重試功能。DFSClient中提供可各種構造它們的static函式,createClientDatanodeProtocolProxy用於生成到DataNode的RPC客戶端。
DFSClient的建構函式也比價簡單,就是初始化成員變數,close用於關閉DFSClient。
下面的功能,DFSClient只是簡單地呼叫NameNode的對應方法(加一些簡單的檢查),就不羅嗦了:
setReplication/rename/delete/exists(通過getFileInfo的返回值是否為空判斷)/listPaths/getFileInfo/setPermission/setOwner/getDiskStatus/totalRawCapacity/totalRawUsed/datanodeReport/setSafeMode/refreshNodes/metaSave/finalizeUpgrade/mkdirs/getContentSummary/setQuota/setTimes
DFSClient提供了各種create方法,它們最後都是構造一個OutputStream,並將檔名和生成的OutputStream加到leasechecker,完成建立動作。
append操作是通過namenode.append,獲取最後的Block資訊,然後構造一個OutputStream,並將檔名和生成的OutputStream加到leasechecker,完成append動作。
getFileChecksum用於獲取檔案的校驗資訊,它在得到資料塊的位置資訊後利用DataNode提供的OP_BLOCK_CHECKSUM操作,獲取需要的資料,並綜合起來。過程簡單,方法主要是在處理OP_BLOCK_CHECKSUM需要交換的資料包。
DFSClient內部還有一些其它的輔助方法,都比較簡單,就不再分析了。
Hadoop原始碼分析(MapReduce概論)
大家都熟悉檔案系統,在對HDFS進行分析前,我們並沒有花很多的時間去介紹HDFS的背景,畢竟大家對檔案系統的還是有一定的理解的,而且也有很好的文件。在分析Hadoop的MapReduce部分前,我們還是先了解系統是如何工作的,然後再進入我們的分析部分。下面的圖來自http://horicky.blogspot.com/2008/11/hadoop-mapreduce-implementation.html,是我看到的講MapReduce最好的圖。
以Hadoop帶的wordcount為例子(下面是啟動行):
hadoop jar hadoop-0.19.0-examples.jar wordcount /usr/input/usr/output
使用者提交一個任務以後,該任務由JobTracker協調,先執行Map階段(圖中M1,M2和M3),然後執行Reduce階段(圖中R1和R2)。Map階段和Reduce階段動作都受TaskTracker監控,並執行在獨立於TaskTracker的Java虛擬機器中。
我們的輸入和輸出都是HDFS上的目錄(如上圖所示)。輸入由InputFormat介面描述,它的實現如ASCII檔案,JDBC資料庫等,分別處理對於的資料來源,並提供了資料的一些特徵。通過InputFormat實現,可以獲取InputSplit介面的實現,這個實現用於對資料進行劃分(圖中的splite1到splite5,就是劃分以後的結果),同時從InputFormat也可以獲取RecordReader介面的實現,並從輸入中生成<k,v>對。有了<k,v>,就可以開始做map操作了。
map操作通過context.collect(最終通過OutputCollector.
collect)將結果寫到context中。當Mapper的輸出被收集後,它們會被Partitioner類以指定的方式區分地寫出到輸出檔案裡。我們可以為Mapper提供Combiner,在Mapper輸出它的<k,v>時,鍵值對不會被馬上寫到輸出裡,他們會被收集在list裡(一個key值一個list),當寫入一定數量的鍵值對時,這部分緩衝會被Combiner中進行合併,然後再輸出到Partitioner中(圖中M1的黃顏色部分對應著Combiner和Partitioner)。
Map的動作做完以後,進入Reduce階段。這個階段分3個步驟:混洗(Shuffle),排序(sort)和reduce。
混洗階段,Hadoop的MapReduce框架會根據Map結果中的key,將相關的結果傳輸到某一個Reducer上(多個Mapper產生的同一個key的中間結果分佈在不同的機器上,這一步結束後,他們傳輸都到了處理這個key的Reducer的機器上)。這個步驟中的檔案傳輸使用了HTTP協議。
排序和混洗是一塊進行的,這個階段將來自不同Mapper具有相同key值的<key,value>對合併到一起。
Reduce階段,上面通過Shuffle和sort後得到的<key, (list of values)>會送到Reducer. reduce方法中處理,輸出的結果通過OutputFormat,輸出到DFS中。
Hadoop原始碼分析(MapTask)
接下來我們來分析Task的兩個子類,MapTask和ReduceTask。MapTask的相關類圖如下:
MapTask其實不是很複雜,複雜的是支援MapTask工作的一些輔助類。MapTask的成員變數少,只有split和splitClass。我們知道,Map的輸入是split,是原始資料的一個切分,這個切分由org.apache.hadoop.mapred.InputSplit的子類具體描述(前面我們是通過org.apache.hadoop.mapreduce.InputSplit介紹了InputSplit,它們對外的API是一樣的)。splitClass是InputSplit子類的類名,通過它,我們可以利用Java的反射機制,建立出InputSplit子類。而split是一個BytesWritable,它是InputSplit子類序列化以後的結果,再通過InputSplit子類的readFields方法,我們可以回覆出對應的InputSplit物件。
MapTask最重要的方法是run。run方法相當簡單,配置完系統的TaskReporter後,就根據情況執行runJobCleanupTask,runJobSetupTask,runTaskCleanupTask或執行Mapper。由於MapReduce現在有兩套API,MapTask需要支援這兩套API,使得MapTask執行Mapper分為runNewMapper和runOldMapper,run*Mapper後,MapTask會呼叫父類的done方法。
接下來我們來分析runOldMapper,最開始部分是構造Mapper處理的InputSplit,更新Task的配置,然後就開始建立Mapper的RecordReader,rawIn是原始輸入,然後分正常(使用TrackedRecordReader,後面討論)和跳過部分記錄(使用SkippingRecordReader,後面討論)兩種情況,構造對應的真正輸入in。
跳過部分記錄是Map的一種出錯恢復策略,我們知道,MapReduce處理的資料集合非常大,而有些任務對一部分出錯的資料不進行處理,對結果的影響很小(如大資料集合的一些統計量),那麼,一小部分的資料出錯導致已處理的大量結果無效,是得不償失的,跳過這部分記錄,成了Mapper的一種選擇。
Mapper的輸出,是通過MapOutputCollector進行的,也分兩種情況,如果沒有Reducer,那麼,用DirectMapOutputCollector(後面討論),否則,用MapOutputBuffer(後面討論)。
構造完Mapper的輸入輸出,通過構造配置檔案中配置的MapRunnable,就可以執行Mapper了。目前系統有兩個MapRunnable:MapRunner和MultithreadedMapRunner,如下圖。
原有API在這塊的處理上和新API有很大的不一樣。介面MapRunnable是原有API中Mapper的執行器,run方法就是用於執行使用者的Mapper。MapRunner是單執行緒執行器,相當簡單,首先,當MapTask呼叫:
MapRunnable<INKEY,INVALUE,OUTKEY,OUTVALUE>runner =
ReflectionUtils.newInstance(job.getMapRunnerClass(),job);
MapRunner的configure會在newInstance的最後被呼叫,configure執行的過程中,對應的Mapper會通過反射機制構造出來。
MapRunner的run方法,會先建立對應的key,value物件,然後,對InputSplit的每一對<key,value>,呼叫Mapper的map方法,迴圈結束後,Mapper對應的清理方法會被呼叫。我們需要注意,key,value物件在run方法中是被重複使用的,就是說,每次傳入Mapper的map方法的key,value都是同一個物件,只不過是裡面的內容變了,物件並沒有變。如果你需要保留key,value的內容,需要實現clone機制,克隆出物件的一個新備份。
相對於新API的多執行緒執行器,老API的MultithreadedMapRunner就比較複雜了,總體來說,就是通過阻塞佇列配合Java的多執行緒執行器,將<key,value>分發到多個執行緒中去處理。需要注意的是,在這個過程中,這些執行緒共享一個Mapper例項,如果Mapper有共享的資源,需要有一定的保護機制。
runNewMapper用於執行新版本的Mapper,比runOldMapper稍微複雜,我們就不再討論了。
Hadoop原始碼分析(MapTask輔助類 I)
MapTask的輔助類主要針對Mapper的輸入和輸出。首先我們來看MapTask中用的的Mapper輸入,在類圖中,這部分位於右上角。
MapTask.TrackedRecordReader是一個Wrapper,在原有輸入RecordReader的基礎上,新增了收集上報統計資料的功能。
MapTask.SkippingRecordReader也是一個Wrapper,它在MapTask.TrackedRecordReader的基礎上,新增了忽略部分輸入的功能。在分析MapTask.SkippingRecordReader之前,我們先看一下類SortedRanges和它相關的類。
類SortedRanges.Ranges表示了一個範圍,以開始位置和範圍長度(這樣的話就可以表示長度為0的範圍)來表示一個範圍,並提供了一系列的範圍操作方法。注意,方法getEndIndex得到的右端點並不包含在範圍內(應理解為開區間)。SortedRanges包含了一系列不重疊的範圍,為了保證包含的範圍不重疊,在add方法和remove方法上需要做一些處理,保證不重疊的約束。SkipRangeIterator是訪問SortedRanges包含的Ranges的迭代器。
MapTask.SkippingRecordReader的實現很簡單,因為要忽略的輸入都保持在SortedRanges.Ranges,只需要在next方法中,判斷目前範圍時候落在SortedRanges.Ranges中,如果是,忽略,並將忽略的記錄寫檔案(可配置)
NewTrackingRecordReader和NewOutputCollector被新API使用,我們不分析。
MapTask的輸出輔助類都繼承自MapOutputCollector,它只是在OutputCollector的基礎上新增了close和flush方法。
DirectMapOutputCollector用在Reducer的數目為0,就是不需要Reduce階段的時候。它是直接通過
out = job.getOutputFormat().getRecordWriter(fs,job, finalName, reporter);
得到對應的RecordWriter,collect直接到RecordWriter上。
如果Mapper後續有reduce任務,系統會使用MapOutputBuffer做為輸出,這是個比較複雜的類,有1k行左右的程式碼。
我們知道,Mapper是通過OutputCollector將Map的結果輸出,輸出的量很大,Hadoop的機制是通過一個circle buffer 收集Mapper的輸出, 到了io.sort.mb * percent量的時候,就spill到disk,如下圖。圖中出現了兩個陣列和一個緩衝區,kvindices保持了記錄所屬的(Reduce)分割槽,key在緩衝區開始的位置和value在緩衝區開始的位置,通過kvindices,我們可以在緩衝區中找到對應的記錄。kvoffets用於在緩衝區滿的時候對kvindices的partition進行排序,排完序的結果將輸出到輸出到本地磁碟上,其中索引(kvindices)保持在spill{spill號}.out.index中,資料儲存在spill{spill號}.out中。
當Mapper任務結束後,有可能會出現多個spill檔案,這些檔案會做一個歸併排序,形成Mapper的一個輸出(spill.out和spill.out.index),如下圖:
這個輸出是按partition排序的,這樣的話,Mapper的輸出被分段,Reducer要獲取的就是spill.out中的一段。(注意,記憶體和硬碟上的索引結構不一樣)
(感謝彭帥的Hadoop Map Stage流程分析http://www.cnblogs.com/OnlyXP/archive/2009/05/25/1488811.html)
Hadoop原始碼分析(MapTask輔助類,II)
有了上面Mapper輸出的記憶體儲存結構和硬碟儲存結構討論,我們來仔細分析MapOutputBuffer的流程。
首先是成員變數。最先初始化的是作業配置job和統計功能reporter。通過配置,MapOutputBuffer可以獲取本地檔案系統(localFs和rfs),Reducer的數目和Partitioner。
SpillRecord是檔案spill.out{spill號}.index在記憶體中的對應抽象(記憶體資料和檔案資料就差最後的校驗和),該檔案保持了一系列的IndexRecord,如下圖:
IndexRecord有3個欄位,分別是startOffset:記錄偏移量,rawLength:初始長度,partLength:實際長度(可能有壓縮)。SpillRecord保持了一系列的IndexRecord,並提供方法用於新增記錄(沒有刪除記錄的操作,因為不需要),獲取記錄,寫檔案,讀檔案(通過建構函式)。
接下來是一些和輸出快取區kvbuffer,快取區記錄索引kvindices和快取區記錄索引排序工作陣列kvoffsets相關的處理,下面的圖有助於說明這段程式碼。
這部分依賴於3個配置引數,io.sort.spill.percent是kvbuffer,kvindices和kvoffsets的總大小(以M為單位,預設是100,就是100M,這一部分是MapOutputBuffer中佔用儲存最多的)。io.sort.record.percent是kvindices和kvoffsets佔用的空間比例(預設是0.05)。前面的分析我們已經知道kvindices和kvoffsets,如果記錄數是N的話,它佔用的空間是4N*4bytes,根據這個關係和io.sort.record.percent的值,我們可以計算出kvindices和kvoffsets最多能有多少個記錄,並分配相應的空間。引數io.sort.spill.percent指示當輸出緩衝區或kvindices和kvoffsets記錄數量到達對應的佔用率的時候,會啟動spill,將記憶體緩衝區的記錄存放到硬碟上,softBufferLimit和softRecordLimit為對應的位元組數。
值對<key, value>輸出到緩衝區是通過Serializer序列化的,這部分的初始化跟在上面輸出快取後面。接下來是一些計數器和可能的資料壓縮處理器的初始化,可能的Combiner和combiner工作的一些配置。
最後是啟動spillThread,該Thread會檢查記憶體中的輸出快取區,在滿足一定條件的時候將緩衝區中的內容spill到硬碟上。這是一個標準的生產者-消費者模型,MapTask的collect方法是生產者,spillThread是消費者,它們之間同步是通過spillLock(ReentrantLock)和spillLock上的兩個條件變數(spillDone和spillReady)完成的。
先看生產者,MapOutputBuffer.collect的主要流程是:
l 報告進度和引數檢測(<K,V>符合Mapper的輸出約定);
l spillLock.lock(),進入臨界區;
l 如果達到spill條件,設定變數並通過spillReady.signal(),通知spillThread;並等待spill結束(通過spillDone.await()等待);
l spillLock.unlock();
l 輸出key,value並更新kvindices和kvoffsets(注意,方法collect是synchronized,key和value各自輸出,它們也會佔用連續的輸出緩衝區);
kvstart,kvend和kvindex三個變數在判斷是否需要spill和spill是否結束的過程中很重要,kvstart是有效記錄開始的下標,kvindex是下一個可做記錄的位置,kvend的作用比較特殊,它在一般情況下kvstart==kvend,但開始spill的時候它會被賦值為kvindex的值,spill結束時,它的值會被賦給kvstart,這時候kvstart==kvend。這就是說,如果kvstart不等於kvend,系統正在spill,否則,kvstart==kvend,系統處於普通工作狀態。其實在程式碼中,我們可以看到很多kvstart==kvend的判斷。
下面我們分情況,討論kvstart,kvend和kvindex的配合。初始化的時候,它們都被賦值0。
下圖給出了一個沒有spill的記錄新增過程:
注意kvindex和kvnext的關係,取模實現了迴圈緩衝區
如果在新增記錄的過程中,出現spill(多種條件),那麼,主要的過程如下:
首先還是計算kvnext,主要,這個時候kvend==kvstart(圖中沒有畫出來)。如果spill條件滿足,那麼,kvindex的值會賦給kvend(這是kvend不等於kvstart),從kvstart和kvend的大小關係,我們可以知道記錄位於陣列的那一部分(左邊是kvstart<kvend的情況,右邊是另外的情況)。Spill結束的時候,kvend值會被賦給kvstart,kvend==kvstart又重新滿足,同時,我們可以發現kvindex在這個過程中沒有變化,新的記錄還是寫在kvindex指向的位置,然後,kvindex=kvnect,kvindex移到下一個可用位置。
大家體會一下上面的過程,特別是kvstart,kvend和kvindex的配合,其實,<key,value>對輸出使用的緩衝區,也有類似的過程。
Collect在處理<key,value>輸出時,會處理一個MapBufferTooSmallException,這是value的序列化結果太大,不能一次放入緩衝區的指示,這種情況下我們需要呼叫spillSingleRecord,特殊處理。
Hadoop原始碼分析(MapTask輔助類,III)
接下來討論的是key,value的輸出,這部分比較複雜,不過有了前面kvstart,kvend和kvindex配合的分析,有利於我們理解這部分的程式碼。
輸出緩衝區中,和kvstart,kvend和kvindex對應的是bufstart,bufend和bufmark。這部分還涉及到變數bufvoid,用於表明實際使用的緩衝區結尾(見後面BlockingBuffer.reset分析),和變數bufmark,用於標記記錄的結尾。這部分程式碼需要bufmark,是因為key或value的輸出是變長的,(前面元資訊記錄大小是常量,就不需要這樣的變數)。
最好的情況是緩衝區沒有翻轉和value序列化結果很小,如下圖:
先對key序列化,然後對value做序列化,臨時變數keystart,valstart和valend分別記錄了key結果的開始位置,value結果的開始位置和value結果的結束位置。
序列化過程中,往緩衝區寫是最終呼叫了Buffer.write方法,我們後面再分析。
如果key序列化後出現bufindex < keystart,那麼會呼叫BlockingBuffer的reset方法。原因是在spill的過程中需要對<key,value>排序,這種情況下,傳遞給RawComparator的必須是連續的二進位制緩衝區,通過BlockingBuffer.reset方法,解決這個問題。下圖解釋瞭如何解決這個問題:
當發現key的序列化結果出現不連續的情況時,我們會把bufvoid設定為bufmark,見緩衝區開始部分往後挪,然後將原來位於bufmark到bufvoid出的結果,拷到緩衝區開始處,這樣的話,key序列化的結果就連續存放在緩衝區的最開始處。
上面的調整有一個條件,就是bufstart前面的緩衝區能夠放下整個key序列化的結果,如果不能,處理的方式是將bufindex置0,然後呼叫BlockingBuffer內部的out的write方法直接輸出,這實際呼叫了Buffer.write方法,會啟動spill過程,最終我們會成功寫入key序列化的結果。
下面我們看write方法。key,value序列化過程中,往緩衝區寫資料是最終呼叫了Buffer.write方法,又是一個複雜的方法。
l do-while迴圈,直到我們有足夠的空間可以寫資料(包括緩衝區和kvindices和kvoffsets)
u 首先我們計算緩衝區連續寫是否寫滿標誌buffull和緩衝區非連續情況下有足夠寫空間標誌wrap(這個實在拗口),見下面的討論;條件(buffull && !wrap)用於判斷目前有沒有足夠的寫空間;
u 在spill沒啟動的情況下(kvstart == kvend),分兩種情況,如果陣列中有記錄(kvend !=kvindex),那麼,根據需要(目前輸出空間不足或記錄數達到spill條件)啟動spill過程;否則,如果空間還是不夠(buffull && !wrap),表明這個記錄非常大,以至於我們的記憶體緩衝區不能容下這麼大的資料量,拋MapBufferTooSmallException異常;
u 如果空間不足同時spill在執行,等待spillDone;
l 寫資料,注意,如果buffull,則寫資料會不連續,則寫滿剩餘緩衝區,然後設定bufindex=0,並從bufindex處接著寫。否則,就是從bufindex處開始寫。
下圖給出了緩衝區連續寫是否寫滿標誌buffull和緩衝區非連續情況下有足夠寫空間標誌wrap計算的幾種可能:
情況1和情況2中,buffull判斷為從bufindex到bufvoid是否有足夠的空間容納寫的內容,wrap是圖中白顏色部分的空間是否比輸入大,如果是,wrap為true;情況3和情況4中,buffull判斷bufindex到bufstart的空間是否滿足條件,而wrap肯定是false。明顯,條件(buffull&& !wrap)滿足時,目前的空間不夠一次寫。
接下來我們來看spillSingleRecord,只是用於寫放不進記憶體緩衝區的<key,value>對。過程很流水,首先是建立SpillRecord記錄,輸出檔案和IndexRecord記錄,然後迴圈,構造SpillRecord並在恰當的時候輸出記錄(如下圖),最後輸出spill{n}.index檔案。
前面我們提過spillThread,在這個系統中它是消費者,這個消費者相當簡單,需要spill時呼叫函式sortAndSpill,進行spill。sortAndSpill和spillSingleRecord類似,函式的開始也是建立SpillRecord記錄,輸出檔案和IndexRecord記錄,然後,需要在kvoffsets上做排序,排完序後順序訪問kvoffsets,也就是按partition順序訪問記錄。
按partition迴圈處理排完序的陣列,如果沒有combiner,則直接輸出記錄,否則,呼叫combineAndSpill,先做combin然後輸出。迴圈的最後記錄IndexRecord到SpillRecord。
sortAndSpill最後是輸出spill{n}.index檔案。
combineAndSpill比價簡單,我們就不分析了。
BlockingBuffer中最後要分析的方法是flush方法。呼叫flush方法,意味著Mapper的結果都已經collect了,需要對緩衝區做一些最後的清理,併合並spill{n}檔案產生最後的輸出。
緩衝區處理部分很簡單,先等待可能的spill過程完成,然後判斷緩衝區是否為空,如果不是,則呼叫sortAndSpill,做最後的spill,然後結束spill執行緒。
flush合併spill{n}檔案是通過mergeParts方法。如果Mapper最後只有一個spill{n}檔案,簡單修改該檔案的檔名就可以。如果Mapper沒有任何輸出,那麼我們需要建立啞輸出(dummy files)。如果spill{n}檔案多於1個,那麼按partition迴圈處理所有檔案,將處於處理partition的記錄輸出。處理partition的過程中可能還會再次呼叫combineAndSpill,最記錄再做一次combination,其中還涉及到工具類Merger,我們就不再深入研究了。
Hadoop原始碼分析(Task的內部類和輔助類)
從前面的圖中,我們可以發現Task有很多內部類,並擁有大量類成員變數,這些類配合Task完成相關的工作,如下圖。
MapOutputFile管理著Mapper的輸出檔案,它提供了一系列get方法,用於獲取Mapper需要的各種檔案,這些檔案都存放在一個目錄下面。
我們假設傳入MapOutputFile的JobID為job_200707121733_0003,TaskID為task_200707121733_0003_m_000005。MapOutputFile的根為
{mapred.local.dir}/taskTracker/jobcache/{jobid}/{taskid}/output
在下面的討論中,我們把上面的路徑記為{MapOutputFileRoot}
以上面JogID和TaskID為例,我們有:
{mapred.local.dir}/taskTracker/jobcache/job_200707121733_0003/task_200707121733_0003_m_000005/output
需要注意的是,{mapred.local.dir}可以包含一系列的路徑,那麼,Hadoop會在這些根路徑下找一個滿足要求的目錄,建立所需的檔案。MapOutputFile的方法有兩種,結尾帶ForWrite和不帶ForWrite,帶ForWrite用於建立檔案,它需要一個檔案大小作為引數,用於檢查磁碟空間。不帶ForWrite用於獲取以建立的檔案。
getOutputFile:檔名為{MapOutputFileRoot}/file.out;
getOutputIndexFile:檔名為{MapOutputFileRoot}/file.out.index
getSpillFile:檔名為{MapOutputFileRoot}/spill{spillNumber}.out
getSpillIndexFile:檔名為{MapOutputFileRoot}/spill{spillNumber}.out.index
以上四個方法用於Task子類MapTask中;
getInputFile:檔名為{MapOutputFileRoot}/map_{mapId}.out
用於ReduceTask中。我們到使用到他們的地方再介紹相應的應用場景。
介紹完臨時檔案管理以後,我們來看Task.CombineOutputCollector,它繼承自org.apache.hadoop.mapred.OutputCollector,很簡單,只是一個OutputCollector到IFile.Writer的Adapter,活都讓IFile.Writer幹了。
ValuesIterator用於從RawKeyValueIterator(Key,Value都是DataInputBuffer,ValuesIterator要求該輸入已經排序)中獲取符合RawComparator<KEY>comparator的值的迭代器。它在Task中有一個簡單子類,CombineValuesIterator。
Task.TaskReporter用於向JobTracker提交計數器報告和狀態報告,它實現了計數器報告Reporter和狀態報告StatusReporter。為了不影響主執行緒的工作,TaskReporter有一個獨立的執行緒,該執行緒通過TaskUmbilicalProtocol介面,利用Hadoop的RPC機制,向JobTracker報告Task執行情況。
FileSystemStatisticUpdater用於記錄對檔案系統的對/寫操作位元組數,是個簡單的工具類。
Hadoop原始碼分析(mapreduce.lib.partition/reduce/output)
Map的結果,會通過partition分發到Reducer上,Reducer做完Reduce操作後,通過OutputFormat,進行輸出,下面我們就來分析參與這個過程的類。
Mapper的結果,可能送到可能的Combiner做合併,Combiner在系統中並沒有自己的基類,而是用Reducer作為Combiner的基類,他們對外的功能是一樣的,只是使用的位置和使用時的上下文不太一樣而已。
Mapper最終處理的結果對<key, value>,是需要送到Reducer去合併的,合併的時候,有相同key的鍵/值對會送到同一個Reducer那,哪個key到哪個Reducer的分配過程,是由Partitioner規定的,它只有一個方法,輸入是Map的結果對<key, value>和Reducer的數目,輸出則是分配的Reducer(整數編號)。系統預設的Partitioner是HashPartitioner,它以key的Hash值對Reducer的數目取模,得到對應的Reducer。
Reducer是所有使用者定製Reducer類的基類,和Mapper類似,它也有setup,reduce,cleanup和run方法,其中setup和cleanup含義和Mapper相同,reduce是真正合並Mapper結果的地方,它的輸入是key和這個key對應的所有value的一個迭代器,同時還包括Reducer的上下文。系統中定義了兩個非常簡單的Reducer,IntSumReducer和LongSumReducer,分別用於對整形/長整型的value求和。
Reduce的結果,通過Reducer.Context的方法collect輸出到檔案中,和輸入類似,Hadoop引入了OutputFormat。OutputFormat依賴兩個輔助介面:RecordWriter和OutputCommitter,來處理輸出。RecordWriter提供了write方法,用於輸出<key, value>和close方法,用於關閉對應的輸出。OutputCommitter提供了一系列方法,使用者通過實現這些方法,可以定製OutputFormat生存期某些階段需要的特殊操作。我們在TaskInputOutputContext中討論過這些方法(明顯,TaskInputOutputContext是OutputFormat和Reducer間的橋樑)。
OutputFormat和RecordWriter分別對應著InputFormat和RecordReader,系統提供了空輸出NullOutputFormat(什麼結果都不輸出,NullOutputFormat.RecordWriter只是示例,系統中沒有定義),LazyOutputFormat(沒在類圖中出現,不分析),FilterOutputFormat(不分析)和基於檔案FileOutputFormat的SequenceFileOutputFormat和TextOutputFormat輸出。
基於檔案的輸出FileOutputFormat利用了一些配置項配合工作,包括mapred.output.compress:是否壓縮;mapred.output.compression.codec:壓縮方法;mapred.output.dir:輸出路徑;mapred.work.output.dir:輸出工作路徑。FileOutputFormat還依賴於FileOutputCommitter,通過FileOutputCommitter提供一些和Job,Task相關的臨時檔案管理功能。如FileOutputCommitter的setupJob,會在輸出路徑下建立一個名為_temporary的臨時目錄,cleanupJob則會刪除這個目錄。
SequenceFileOutputFormat輸出和TextOutputFormat輸出分別對應輸入的SequenceFileInputFormat和TextInputFormat,我們就不再詳細分析啦。
Hadoop原始碼分析(IFile)
Mapper的輸出,在傳送到Reducer前是存放在本地檔案系統的,IFile提供了對Mapper輸出的管理。我們已經知道,Mapper的輸出是<Key,Value>對,IFile以記錄<key-len, value-len, key,value>的形式存放了這些資料。為了儲存鍵值對的邊界,很自然IFile需要儲存key-len和value-len。
和IFile相關的類圖如下:
其中,檔案流形式的輸入和輸出是由IFIleInputStream和IFIleOutputStream抽象。以記錄形式的讀/寫操作由IFile.Reader/IFile.Writer提供,IFile.InMemoryReader用於讀取存在於記憶體中的IFile檔案格式資料。
我們以輸出為例,來分析這部分的實現。首先是下圖的和序列化反序列化相關的Serialization/Deserializer,這部分的code是在包org.apache.hadoop.io.serializer。序列化由Serializer抽象,通過Serializer的實現,使用者可以利用serialize方法把物件序列化到通過open方法開啟的輸出流裡。Deserializer提供的是相反的過程,對應的方法是deserialize。hadoop.io.serializer中還實現了配合工作的Serialization和對應的工廠SerializationFactory。兩個具體的實現是WritableSerialization和JavaSerialization,分別對應了Writeble的序列化反序列化和Java本身帶的序列化反序列化。
有了Serializer/Deserializer,我們來分析IFile.Writer。Writer的建構函式是:
public Writer(Configuration conf,FSDataOutputStream out,
Class<K> keyClass, Class<V>valueClass,
CompressionCodec codec, Counters.CounterwritesCounter)
conf,配置引數,out是Writer的輸出,keyClass 和valueClass是輸出的Kay,Value的class屬性,codec是對輸出進行壓縮的方法,引數writesCounter用於對輸出位元組數進行統計的Counters.Counter。通過這些引數,我們可以構造我們使用的支援壓縮功能的輸出流(類成員out,類成員rawOut儲存了建構函式傳入的out),相關的計數器,還有就是Kay,Value的Serializer方法。
Writer最主要的方法是append方法(居然不是write方法,呵呵),有兩種形式:
public void append(K key, V value) throws IOException {
public void append(DataInputBuffer key,DataInputBuffer value)
append(K key, V value)的主要過程是檢查引數,然後將key和value序列化到DataOutputBuffer中,並獲取序列化後的長度,最後把長度(2個)和DataOutputBuffer中的結果寫到輸出,並復位DataOutputBuffer和計數。append(DataInputBufferkey, DataInputBuffer value)處理過程也比較類似,就不再分析了。
close方法中需要注意的是,我們需要標記檔案尾,或者是流結束。目前是通過寫2個值為EOF_MARKER的長度來做標記。
IFileOutputStream是用於配合Writer的輸出流,它會在IFiles的最後新增校驗資料。當Writer呼叫IFileOutputStream的write操作時,IFileOutputStream計算並保持校驗和,流被close的時候,校驗結果會寫到對應檔案的檔案尾。實際上存放在磁碟上的檔案是一系列的<key-len, value-len, key, value>記錄和校驗結果。
Reader的相關過程,我們就不再分析了。
Hadoop原始碼分析(*IDs類和*Context類)
我們開始來分析Hadoop MapReduce的內部的執行機制。使用者向Hadoop提交Job(作業),作業在JobTracker物件的控制下執行。Job被分解成為Task(任務),分發到叢集中,在TaskTracker的控制下執行。Task包括MapTask和ReduceTask,是MapReduce的Map操作和Reduce操作執行的地方。這中任務分佈的方法比較類似於HDFS中NameNode和DataNode的分工,NameNode對應的是JobTracker,DataNode對應的是TaskTracker。JobTracker,TaskTracker和MapReduce的客戶端通過RPC通訊,具體可以參考HDFS部分的分析。
我們先來分析一些輔助類,首先是和ID有關的類,ID的繼承樹如下:
這張圖可以看出現在Hadoop的org.apache.hadoop.mapred向org.apache.hadoop.mapreduce遷移帶來的一些問題,其中灰色是標註為@Deprecated的。ID攜帶一個整型,實現了WritableComparable介面,這表明它可以比較,而且可以被Hadoop的io機制序列化/解序列化(必須實現compareTo/readFields/write方法)。JobID是系統分配給作業的唯一識別符號,它的toString結果是job_<jobtrackerID>_<jobNumber>。例子:job_200707121733_0003表明這是jobtracker200707121733(利用jobtracker的開始時間作為ID)的第3號作業。
作業分成任務執行,任務號TaskID包含了它所屬的作業ID,同時也有任務ID,同時還保持了這是否是一個Map任務(成員變數isMap)。任務號的字串表示為task_<jobtrackerID>_<jobNumber>_[m|r]_<taskNumber>,如task_200707121733_0003_m_000005表示作業200707121733_0003的000005號任務,改任務是一個Map任務。
一個任務有可能有多個執行(錯誤恢復/消除Stragglers等),所以必須區分任務的多個執行,這是通過類TaskAttemptID來完成,它在任務號的基礎上新增了嘗試號。一個任務嘗試號的例子是attempt_200707121733_0003_m_000005_0,它是任務task_200707121733_0003_m_000005的第0號嘗試。
JVMId用於管理任務執行過程中的Java虛擬機器,我們後面再討論。
為了使Job和Task工作,Hadoop提供了一系列的上下文,這些上下文儲存了Job和Task工作的資訊。
處於繼承樹的最上方是org.apache.hadoop.mapreduce.JobContext,前面我們已經介紹過了,它提供了Job的一些只讀屬性,兩個成員變數,一個儲存了JobID,另一個型別為JobConf,JobContext中除了JobID外,其它的資訊都保持在JobConf中。它定義瞭如下配置項:
l mapreduce.inputformat.class:InputFormat的實現
l mapreduce.map.class:Mapper的實現
l mapreduce.combine.class:Reducer的實現
l mapreduce.reduce.class:Reducer的實現
l mapreduce.outputformat.class:OutputFormat的實現
l mapreduce.partitioner.class:Partitioner的實現
同時,它提供方法,使得通過類名,利用Java反射提供的Class.forName方法,獲得類對應的Class。org.apache.hadoop.mapred的JobContext物件比org.apache.hadoop.mapreduce.JobContext多了成員變數progress,用於獲取進度資訊,它型別為JobConf成員job指向mapreduce.JobContext對應的成員,沒有新增任何新功能。
JobConf繼承自Configuration,保持了MapReduce執行需要的一些配置資訊,它管理著46個配置引數,包括上面mapreduce配置項對應的老版本形式,如mapreduce.map.class 對應mapred.mapper.class。這些配置項我們在使用到它們的時候再介紹。
org.apache.hadoop.mapreduce.JobContext的子類Job前面也已經介紹了,後面在討論系統的動態行為時,再回來看它。
TaskAttemptContext用於任務的執行,它引入了標識任務執行的TaskAttemptID和任務狀態status,並提供新的訪問介面。org.apache.hadoop.mapred的TaskAttemptContext繼承自mapreduce的對應版本,只是增加了記錄進度的progress。
TaskInputOutputContext和它的子類都在包org.apache.hadoop.mapreduce中,前面已經分析過了,我們就不再羅嗦。
Hadoop原始碼分析(包hadoop.mapred中的MapReduce介面)
前面已經完成了對org.apache.hadoop.mapreduce的分析,這個包提供了Hadoop MapReduce部分的應用API,用於使用者實現自己的MapReduce應用。但這些介面是給未來的MapReduce應用的,目前MapReduce框架還是使用老系統(參考補丁HADOOP-1230)。下面我們來分析org.apache.hadoop.mapred,首先還是從mapred的MapReduce框架開始分析,下面的類圖(灰色部分為標記為@Deprecated的類/介面):
我們把包mapreduce的類圖附在下面,對比一下,我們就會發現,org.apache.hadoop.mapred中的MapReduce API相對來說很簡單,主要是少了和Context相關的類,那麼,好多在mapreduce中通過context來完成的工作,就需要通過引數來傳遞,如Map中的輸出,老版本是:
output.collect(key,result); // output’s type is:OutputCollector
新版本是:
context.write(key, result); // output’s type is: Context
它們分別使用OutputCollector和Mapper.Context來輸出map的結果,顯然,原有OutputCollector的新API中就不再需要。總體來說,老版本的API比較簡單,MapReduce過程中關鍵的物件都有,但可擴充套件性不是很強。同時,老版中提供的輔助類也很多,我們前面分析的FileOutputFormat,也有對應的實現,我們就不再討論了。
Hadoop原始碼分析(包mapreduce.lib.input)
接下來我們按照MapReduce過程中資料流動的順序,來分解org.apache.hadoop.mapreduce.lib.*的相關內容,並介紹對應的基類的功能。首先是input部分,它實現了MapReduce的資料輸入部分。類圖如下:
類圖的右上角是InputFormat,它描述了一個MapReduceJob的輸入,通過InputFormat,Hadoop可以:
l 檢查MapReduce輸入資料的正確性;
l 將輸入資料切分為邏輯塊InputSplit,這些塊會分配給Mapper;
l 提供一個RecordReader實現,Mapper用該實現從InputSplit中讀取輸入的<K,V>對。
在org.apache.hadoop.mapreduce.lib.input中,Hadoop為所有基於檔案的InputFormat提供了一個虛基類FileInputFormat。下面幾個引數可以用於配置FileInputFormat:
l mapred.input.pathFilter.class:輸入檔案過濾器,通過過濾器的檔案才會加入InputFormat;
l mapred.min.split.size:最小的劃分大小;
l mapred.max.split.size:最大的劃分大小;
l mapred.input.dir:輸入路徑,用逗號做分割。
類中比較重要的方法有:
protectedList<FileStatus> listStatus(Configuration job)
遞迴獲取輸入資料目錄中的所有檔案(包括檔案資訊),輸入的job是系統執行的配置Configuration,包含了上面我們提到的引數。
publicList<InputSplit> getSplits(JobContext context)
將輸入劃分為InputSplit,包含兩個迴圈,第一個迴圈處理所有的檔案,對於每一個檔案,根據輸入的劃分最大/最小值,迴圈得到檔案上的劃分。注意,劃分不會跨越檔案。
FileInputFormat沒有實現InputFormat的createRecordReader方法。
FileInputFormat有兩個子類,SequenceFileInputFormat是Hadoop定義的一種二進位制形式存放的鍵/值檔案(參考http://hadoop.apache.org/core/docs/current/api/org/apache/hadoop/io/SequenceFile.html),它有自己定義的檔案佈局。由於它有特殊的副檔名,所以SequenceFileInputFormat過載了listStatus,同時,它實現了createRecordReader,返回一個SequenceFileRecordReader物件。TextInputFormat處理的是文字檔案,createRecordReader返回的是LineRecordReader的例項。這兩個類都沒有過載FileInputFormat的getSplits方法,那麼,在他們對於的RecordReader中,必須考慮FileInputFormat對輸入的劃分方式。
FileInputFormat的getSplits,返回的是FileSplit。這是一個很簡單的類,包含的屬性(檔名,起始偏移量,劃分的長度和可能的目標機器)已經足以說明這個類的功能。
RecordReader用於在劃分中讀取<Key,Value>對。RecordReader有五個虛方法,分別是:
l initialize:初始化,輸入引數包括該Reader工作的資料劃分InputSplit和Job的上下文context;
l nextKey:得到輸入的下一個Key,如果資料劃分已經沒有新的記錄,返回空;
l nextValue:得到Key對應的Value,必須在呼叫nextKey後呼叫;
l getProgress:得到現在的進度;
l close,來自java.io的Closeable介面,用於清理RecordReader。
我們以LineRecordReader為例,來分析RecordReader的構成。前面我們已經分析過FileInputFormat對檔案的劃分了,劃分完的Split包括了檔名,起始偏移量,劃分的長度。由於檔案是文字檔案,LineRecordReader的初始化方法initialize會建立一個基於行的讀取物件LineReader(定義在org.apache.hadoop.util中,我們就不分析啦),然後跳過輸入的最開始的部分(只在Split的起始偏移量不為0的情況下進行,這時最開始的部分可能是上一個Split的最後一行的一部分)。nextKey的處理很簡單,它使用當前的偏移量作為Key,nextValue當然就是偏移量開始的那一行了(如果行很長,可能出現截斷)。進度getProgress和close都很簡單。
Hadoop原始碼分析(包mapreduce.lib.map)
Hadoop的MapReduce框架中,Map動作通過Mapper類來抽象。一般來說,我們會實現自己特殊的Mapper,並註冊到系統中,執行時,我們的Mapper會被MapReduce框架呼叫。Mapper類很簡單,包括一個內部類和四個方法,靜態結構圖如下:
內部類Context繼承自MapContext,並沒有引入任何新的方法。
Mapper的四個方法是setup,map,cleanup和run。其中,setup和cleanup用於管理Mapper生命週期中的資源,setup在完成Mapper構造,即將開始執行map動作前呼叫,cleanup則在所有的map動作完成後被呼叫。方法map用於對一次輸入的key/value對進行map動作。run方法執行了上面描述的過程,它呼叫setup,讓後迭代所有的key/value對,進行map,最後呼叫cleanup。
org.apache.hadoop.mapreduce.lib.map中實現了Mapper的三個子類,分別是InverseMapper(將輸入<key, value> map為輸出<value, key>),MultithreadedMapper(多執行緒執行map方法)和TokenCounterMapper(對輸入的value分解為token並計數)。其中最複雜的是MultithreadedMapper,我們就以它為例,來分析Mapper的實現。
MultithreadedMapper會啟動多個執行緒執行另一個Mapper的map方法,它會啟動mapred.map.multithreadedrunner.threads(配置項)個執行緒執行Mapper:mapred.map.multithreadedrunner.class(配置項)。MultithreadedMapper重寫了基類Mapper的run方法,啟動N個執行緒(對應的類為MapRunner)執行mapred.map.multithreadedrunner.class(我們稱為目標Mapper)的run方法(就是說,目標Mapper的setup和cleanup會被執行多次)。目標Mapper共享同一份InputSplit,這就意味著,對InputSplit的資料讀必須執行緒安全。為此,MultithreadedMapper引入了內部類SubMapRecordReader,SubMapRecordWriter,SubMapStatusReporter,分別繼承自RecordReader,RecordWriter和StatusReporter,它們通過互斥訪問MultithreadedMapper的Mapper.Context,實現了對同一份InputSplit的執行緒安全訪問,為Mapper提供所需的Context。這些類的實現方法都很簡單。
Hadoop原始碼分析(包org.apache.hadoop.mapreduce)
有了前一節的分析,我們來看一下具體的介面,它們都處於包org.apache.hadoop.mapreduce中。
上面的圖中,類可以分為4種。右上角的是從Writeable繼承的,和Counter(還有CounterGroup和Counters,也在這個包中,並沒有出現在上面的圖裡)和ID相關的類,它們保持MapReduce過程中需要的一些計數器和標識;中間大部分是和Context相關的*Context類,它為Mapper和Reducer提供了相關的上下文;關於Map和Reduce,對應的類是Mapper,Reducer和描述他們的Job(在Hadoop中一次計算任務稱之為一個job,下面的分析中,中文為“作業”,相應的task我們稱為“任務”);圖中其他類是配合Mapper和Reduce工作的一些輔助類。
如果你熟悉HTTPServlet, 那就能很輕鬆地理解Hadoop採用的結構,把整個Hadoop看作是容器,那麼Mapper和Reduce就是容器裡的元件,*Context儲存了元件的一些配置資訊,同時也是和容器通訊的機制。
和ID相關的類我們就不再討論了。我們先看JobContext,它位於*Context繼承樹的最上方,為Job提供一些只讀的資訊,如Job的ID,名稱等。下面的資訊是MapReduce過程中一些較關鍵的定製資訊:
(來自http://www.ibm.com/developerworks/cn/opensource/os-cn-hadoop2/index.html):
引數 |
作用 |
預設值 |
其它實現 |
InputFormat |
將輸入的資料集切割成小資料集 InputSplits, 每一個 InputSplit 將由一個 Mapper 負責處理。此外 InputFormat 中還提供一個 RecordReader 的實現, 將一個 InputSplit 解析成 <key,value> 對提供給 map 函式。 |
TextInputFormat |
SequenceFileInputFormat |
OutputFormat |
提供一個 RecordWriter 的實現,負責輸出最終結果 |
TextOutputFormat |
SequenceFileOutputFormat |
OutputKeyClass |
輸出的最終結果中 key 的型別 |
LongWritable |
|
OutputValueClass |
輸出的最終結果中 value 的型別 |
Text |
|
MapperClass |
Mapper 類,實現 map 函式,完成輸入的 <key,value> 到中間結果的對映 |
IdentityMapper |
LongSumReducer, |
CombinerClass |
實現 combine 函式,將中間結果中的重複 key 做合併 |
null |
|
ReducerClass |
Reducer 類,實現 reduce 函式,對中間結果做合併,形成最終結果 |
IdentityReducer |
AccumulatingReducer, LongSumReducer |
InputPath |
設定 job 的輸入目錄, job 執行時會處理輸入目錄下的所有檔案 |
null |
|
OutputPath |
設定 job 的輸出目錄,job 的最終結果會寫入輸出目錄下 |
null |
|
MapOutputKeyClass |
設定 map 函式輸出的中間結果中 key 的型別 |
如果使用者沒有設定的話,使用 OutputKeyClass |
|
MapOutputValueClass |
設定 map 函式輸出的中間結果中 value 的型別 |
如果使用者沒有設定的話,使用 OutputValuesClass |
|
OutputKeyComparator |
對結果中的 key 進行排序時的使用的比較器 |
WritableComparable |
|
PartitionerClass |
對中間結果的 key 排序後,用此 Partition 函式將其劃分為R份,每份由一個 Reducer 負責處理。 |
HashPartitioner |
KeyFieldBasedPartitioner PipesPartitioner |
Job繼承自JobContext,提供了一系列的set方法,用於設定Job的一些屬性(Job更新屬性,JobContext讀屬性),同時,Job還提供了一些對Job進行控制的方法,如下:
l mapProgress:map的進度(0—1.0);
l reduceProgress:reduce的進度(0—1.0);
l isComplete:作業是否已經完成;
l isSuccessful:作業是否成功;
l killJob:結束一個在執行中的作業;
l getTaskCompletionEvents:得到任務完成的應答(成功/失敗);
l killTask:結束某一個任務;
相關文章
- hadoop原始碼分析Hadoop原始碼
- Hadoop學習——Client原始碼分析Hadoopclient原始碼
- Hadoop2原始碼分析-HDFS核心模組分析Hadoop原始碼
- hadoop中的TextInputFormat類原始碼分析HadoopORM原始碼
- Hadoop2原始碼分析-MapReduce篇Hadoop原始碼
- Hadoop2原始碼分析-Hadoop V2初識Hadoop原始碼
- hadoop 原始碼分析HDFS架構演進Hadoop原始碼架構
- Hadoop2原始碼分析-準備篇Hadoop原始碼
- Hadoop3.2.1 【 HDFS 】原始碼分析 : Standby Namenode解析Hadoop原始碼
- Hadoop2原始碼分析-序列化篇Hadoop原始碼
- Hadoop原始碼篇--Client原始碼Hadoop原始碼client
- Hadoop3.2.1 【 HDFS 】原始碼分析 : Secondary Namenode解析Hadoop原始碼
- Hadoop2原始碼分析-RPC探索實戰Hadoop原始碼RPC
- Hadoop3.2.1 【 YARN 】原始碼分析 :RPC通訊解析HadoopYarn原始碼RPC
- Hadoop3.2.1 【 YARN 】原始碼分析 :AdminService 淺析HadoopYarn原始碼
- Hadoop2原始碼分析-RPC機制初識Hadoop原始碼RPC
- Hadoop2原始碼分析-YARN RPC 示例介紹Hadoop原始碼YarnRPC
- Hadoop原理與原始碼Hadoop原始碼
- Retrofit原始碼分析三 原始碼分析原始碼
- Hadoop的GroupComparator是如何起做用的(原始碼分析)Hadoop原始碼
- 集合原始碼分析[2]-AbstractList 原始碼分析原始碼
- 集合原始碼分析[1]-Collection 原始碼分析原始碼
- 集合原始碼分析[3]-ArrayList 原始碼分析原始碼
- Guava 原始碼分析之 EventBus 原始碼分析Guava原始碼
- Hadoop原始碼篇--Reduce篇Hadoop原始碼
- Hadoop2原始碼分析-YARN 的服務庫和事件庫Hadoop原始碼Yarn事件
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- 以太坊原始碼分析(36)ethdb原始碼分析原始碼
- 以太坊原始碼分析(38)event原始碼分析原始碼
- 以太坊原始碼分析(41)hashimoto原始碼分析原始碼
- 以太坊原始碼分析(43)node原始碼分析原始碼
- 以太坊原始碼分析(52)trie原始碼分析原始碼
- Hadoop原始碼分類概要整理Hadoop原始碼
- Meltdown的分析——完整版;-)
- Hadoop3.2.1 【 HDFS 】原始碼分析 : DataXceiver: 讀取資料塊 解析 [二]Hadoop原始碼
- Hadoop3.2.1 【 HDFS 】原始碼分析 : 檔案系統資料集 [一]Hadoop原始碼
- 深度 Mybatis 3 原始碼分析(一)SqlSessionFactoryBuilder原始碼分析MyBatis原始碼SQLSessionUI