HDFS源代碼結構
HDFS的源代碼都在org.apche.hadoop.hdfs包下。HDFS的源代碼分佈16個目錄下,它們可以分爲如下四類。
1、基礎包
包括工具和安全包。其中,hdfs.util包含了一些HDFS實現需要的輔助數據結構;hdfs.security.token.block和hdfs.sercurity.token.delegation結合Hadoop的安全框架,提供了安全訪問HDFS的機制,該安全特性最先是有Yahoo開發的,集成了企業廣泛應用的Kerberos標準,使得用戶可以在一個集羣管理各類商業敏感數據。
2、HDFS實體實現包
這是代碼分析的重點,包括7個包:
hdfs.server.commom包含了一些名字節點和數據節點共享的功能,如系統升級、存儲空間等。
hdfs.protocol提供了HDFS各個實體間通過IPC交互的接口。
hdfs.server.namenode、hdfs.server.datanode和hdfs分別包含了名字節點、數據節點和客戶端的實現。上述代碼是HDFS代碼分析的重點。
hdfs.server.namenode.metrics和hdfs.server.datanode.metrics實現了名字節點和數據節點上度量數據的收集功能。度量數據包括名字節點進程和數據節點進程上事件的計數,例如數據節點上就可以收集到寫入字節數、被複制的塊的數據等信息。
3、應用包
包括hdfs.tools和hdfs.server.balancer,這兩個包提供查詢HDFS狀態信息工具dfsadmin、文件系統檢查工具fsck和HDFS均衡器balancer(通過start-balancer.sh啓動)的實現。
4、WebHDFS相關包
包括hdfs.web.resources、hdfs.web.namenode.web.resources、hdfs.server.datanode.web.resources和hdfs.web共4個包。
WebHDFS是HDFS1.0中引入的新功能,它提供了一個完整的、通過HTTP訪問HDFS的機制。對比只讀的hftp文件系統,WebHDFS提供了HTTP上讀寫HDFS的能力,並在此基礎上實現了訪問HDFS的c客戶端和用戶空間文件系統(FUSE)。
基於遠程過程調用的接口
接口是軟件系統不同組成部分銜接的約定,一個良好的接口設計可以降低系統各部分的相互依賴,提高組成單元的內聚性,降低組成單元間的耦合程度,從而提高系統的維護性和擴展性。對於HDFS這樣的一個複雜系統,接口也是觀察系統工作的一個出發點。
HDFS的體系結構包括了名字節點、數據節點和客戶端3個主要角色,它們間有兩種主要的通信接口:
1、 Hadoop遠程過程調用接口。
2、基於TCP或HTTP的流式接口。
本節先分析HDFS各節點間的IPC接口。
這些接口可以分爲三大類。
1、客戶端相關的接口
定義在org.apache.hadoop.hdfs.protocol包中。具體接口包括:
ClientProtocol:客戶端與名字節點間的接口,這是一個非常重要的接口,是HDFS客戶訪問文件系統的入口。客戶端通過這個接口訪問名字節點,操作文件或目錄的元數據信息,讀寫文件也必須先訪問名字節點,接下來再和數據節點進行交互,操作文件數據。另外,從名字節點能獲取分佈式系統的一些整體運行狀態信息,也是通過這個接口進行的。
ClientDatanodeProtocol:客戶端與數據節點間的接口。用於客戶端和數據節點進行交互,這個接口用的比較少,客戶端和數據節點間的主要交互是通過流接口進行讀/寫文件數據的操作。錯誤發生時,客戶端需要數據節點配合進行恢復,或者當客戶端進行本地文件讀優化設時,需要通過IPC接口獲取一些信息。
2、服務器間的接口
包括名字節點、第二名字節點、數據節點間存在的IPC調用關係,其接口定義在org.apache.hadoop.hdfs.server.protocol包中。具體接口包括:
DatanodeProtocol:數據節點與名字節點間的接口,這是另外一個重要的接口。在HDFS的主從體系結構中,數據節點作爲從節點不斷地通過這個接口向主節點名字節點報告一些信息,同步信息到名字節點;同時,該節口的一些訪問,方法的返回值會帶回名字節點指令,根據這些指令,數據節點或移動、或刪除、或恢復本地磁盤上的數據塊,或者執行其他操作。
InterDatanodeProtocol:數據節點與數據節點間的接口。數據節點通過這個接口,和其他數據節點進行通信,恢復數據塊,保證數據的一致性。
NamenodeProtocol:第二名字節點、HDFS均衡器與名字節間的接口。第二名字節點會不停地獲取名字節點上某一個時間點的命名空間鏡像和鏡像的變化日誌,然後合併得到一個新的鏡像,並將該結果發送回名字節點,在這個過程,名字節點會通過這個接口,配合第二名字節點完成元數據的合併。該接口也爲HDFS均衡器balancer的正常工作提供一些信息。
3、和安全相關的接口
分別在org.apche.hadoop.security.authorize包和org.apache.hadoop.sercurity包中,包括RefreshAuthorizationPolicyProtocol和RefreshUserMappingsProtocol。名字節點實現了這兩個接口。
客戶端相關的接口
客戶端相關接口上的類不但爲ClientProtocol和ClientDatanodeProtocol服務,同時也應用服務器間接口的交互,包含了很多HDFS中基本概念的抽象。
其中這些抽象中,其中的類可以分爲3種:
1、數據塊相關
通過前面的分析已經瞭解了,HDFS將文件內容分爲數據塊,多副本保存在數據節點上。
數據塊在HDFS中的抽象是org.apache.hadoop.hdfs.protocol.Block,它包含了3個成員變量,都是長整形:
BlockId:數據塊的唯一標識,即數據塊的ID號
numBytes:數據塊包含的文件數據大小。
generationStamp:數據塊的版本號,或數據塊時間戳
需要注意的是,Block類還維護了一個“關聯”影子成員變量:數據塊名。如果某個數據塊的ID號是123456,那麼其數據塊名爲blk_123456.數據塊名應用與數據節點中,作爲數據塊保存在Linux文件系統上的文件名的一部分,Block.getBlockName()、Block.isBlockFilename()、Block.filename2id()等方法都與數據塊名相關。
Block是大量和數據塊相關的類的基礎,在客戶端接口上,這樣的類有LocatedBlock、LocatedBlocks和BlockLocalPathInfo。顧名思義,LocatedBlock就是已經去認了存儲位置的數據塊。其成員變量除了對應的數據塊之外,還包括數據塊在對應文件中的偏移量offset、數據塊所在的數據節點信息locs和數據塊是否損壞標誌corrupt等。locs是一個類型爲DatanodeInfo的數組,包含了所有可用的數據塊位置,損壞的數據塊對應的數據節點信息,則不會出現在數組裏。或得數據塊的位置信息,是讀寫數據塊的前提條件。如下所示:
//Block
public class Block implements Writable, Comparable<Block> {
static { // register a ctor
WritableFactories.setFactory
(Block.class,
new WritableFactory() {
public Writable newInstance() { return new Block(); }
});
}
// generation stamp of blocks that pre-date the introduction of
// a generation stamp.
public static final long GRANDFATHER_GENERATION_STAMP = 0;
/**
*/
public static boolean isBlockFilename(File f) {
String name = f.getName();
if ( name.startsWith( "blk_" ) &&
name.indexOf( '.' ) < 0 ) {
return true;
} else {
return false;
}
}
static long filename2id(String name) {
return Long.parseLong(name.substring("blk_".length()));
}
private long blockId;
private long numBytes;
private long generationStamp;
public void write(DataOutput out) throws IOException {
out.writeLong(blockId);
out.writeLong(numBytes);
out.writeLong(generationStamp);
}
public void readFields(DataInput in) throws IOException {
this.blockId = in.readLong();
this.numBytes = in.readLong();
this.generationStamp = in.readLong();
if (numBytes < 0) {
throw new IOException("Unexpected block size: " + numBytes);
}
}
......
}
//LocatedBlock
public class LocatedBlock implements Writable {
static { // register a ctor
WritableFactories.setFactory
(LocatedBlock.class,
new WritableFactory() {
public Writable newInstance() { return new LocatedBlock(); }
});
}
private Block b;
private long offset; // offset of the first byte of the block in the file
private DatanodeInfo[] locs;
// corrupt flag is true if all of the replicas of a block are corrupt.
// else false. If block has few corrupt replicas, they are filtered and
// their locations are not part of this object
private boolean corrupt;
private Token<BlockTokenIdentifier> blockToken = new Token<BlockTokenIdentifier>();
///////////////////////////////////////////
// Writable
///////////////////////////////////////////
public void write(DataOutput out) throws IOException {
blockToken.write(out);
out.writeBoolean(corrupt);
out.writeLong(offset);
b.write(out);
out.writeInt(locs.length);
for (int i = 0; i < locs.length; i++) {
locs[i].write(out);
}
}
public void readFields(DataInput in) throws IOException {
blockToken.readFields(in);
this.corrupt = in.readBoolean();
offset = in.readLong();
this.b = new Block();
b.readFields(in);
int count = in.readInt();
this.locs = new DatanodeInfo[count];
for (int i = 0; i < locs.length; i++) {
locs[i] = new DatanodeInfo();
locs[i].readFields(in);
} }
......
}
//locatedBlocks
public class LocatedBlocks implements Writable {
private long fileLength;
private List<LocatedBlock> blocks; // array of blocks with prioritized locations
private boolean underConstruction;
/**
* Find block containing specified offset.
*
* @return block if found, or null otherwise.
*/
public int findBlock(long offset) {
// create fake block of size 1 as a key
LocatedBlock key = new LocatedBlock();
key.setStartOffset(offset);
key.getBlock().setNumBytes(1);
Comparator<LocatedBlock> comp =
new Comparator<LocatedBlock>() {
// Returns 0 iff a is inside b or b is inside a
public int compare(LocatedBlock a, LocatedBlock b) {
long aBeg = a.getStartOffset();
long bBeg = b.getStartOffset();
long aEnd = aBeg + a.getBlockSize();
long bEnd = bBeg + b.getBlockSize();
if(aBeg <= bBeg && bEnd <= aEnd
|| bBeg <= aBeg && aEnd <= bEnd)
return 0; // one of the blocks is inside the other
if(aBeg < bBeg)
return -1; // a's left bound is to the left of the b's
return 1;
}
};
return Collections.binarySearch(blocks, key, comp);
}
public void insertRange(int blockIdx, List<LocatedBlock> newBlocks) {
int oldIdx = blockIdx;
int insStart = 0, insEnd = 0;
for(int newIdx = 0; newIdx < newBlocks.size() && oldIdx < blocks.size();
newIdx++) {
long newOff = newBlocks.get(newIdx).getStartOffset();
long oldOff = blocks.get(oldIdx).getStartOffset();
if(newOff < oldOff) {
insEnd++;
} else if(newOff == oldOff) {
// replace old cached block by the new one
blocks.set(oldIdx, newBlocks.get(newIdx));
if(insStart < insEnd) { // insert new blocks
blocks.addAll(oldIdx, newBlocks.subList(insStart, insEnd));
oldIdx += insEnd - insStart;
}
insStart = insEnd = newIdx+1;
oldIdx++;
} else { // newOff > oldOff
assert false : "List of LocatedBlock must be sorted by startOffset";
}
}
insEnd = newBlocks.size();
if(insStart < insEnd) { // insert new blocks
blocks.addAll(oldIdx, newBlocks.subList(insStart, insEnd));
}
}
public static int getInsertIndex(int binSearchResult) {
return binSearchResult >= 0 ? binSearchResult : -(binSearchResult+1);
}
//////////////////////////////////////////////////
// Writable
//////////////////////////////////////////////////
static { // register a ctor
WritableFactories.setFactory
(LocatedBlocks.class,
new WritableFactory() {
public Writable newInstance() { return new LocatedBlocks(); }
});
}
public void write(DataOutput out) throws IOException {
out.writeLong(this.fileLength);
out.writeBoolean(underConstruction);
// write located blocks
int nrBlocks = locatedBlockCount();
out.writeInt(nrBlocks);
if (nrBlocks == 0) {
return;
}
for (LocatedBlock blk : this.blocks) {
blk.write(out);
}
}
public void readFields(DataInput in) throws IOException {
this.fileLength = in.readLong();
underConstruction = in.readBoolean();
// read located blocks
int nrBlocks = in.readInt();
this.blocks = new ArrayList<LocatedBlock>(nrBlocks);
for (int idx = 0; idx < nrBlocks; idx++) {
LocatedBlock blk = new LocatedBlock();
blk.readFields(in);
this.blocks.add(blk);
}
}
......}
//BlockLocalPathInfo
public class BlockLocalPathInfo implements Writable {
static final WritableFactory FACTORY = new WritableFactory() {
public Writable newInstance() { return new BlockLocalPathInfo(); }
};
static { // register a ctor
WritableFactories.setFactory(BlockLocalPathInfo.class, FACTORY);
}
private Block block;
private String localBlockPath = ""; // local file storing the data
private String localMetaPath = ""; // local file storing the checksum
@Override
public void write(DataOutput out) throws IOException {
block.write(out);
Text.writeString(out, localBlockPath);
Text.writeString(out, localMetaPath);
}
@Override
public void readFields(DataInput in) throws IOException {
block = new Block();
block.readFields(in);
localBlockPath = Text.readString(in);
localMetaPath = Text.readString(in);
}
/**
* Get number of bytes in the block.
* @return Number of bytes in the block.
*/
public long getNumBytes() {
return block.getNumBytes();
}
}
另一組類和數據節點相關,它們是DatanodeId和Datanode Info。DatanodeInfo繼承自DatanodeID,在DatanodeID的基礎上,提供了數據節點上的一些度量信息。
數據節點標識DatanodeId用於在HDFS集羣中確定一個數據節點,它的成員變量包括:name:數據節點使用IP套接字地址作爲它的名字,name是一個字符串,可以是IP地址、端口號對,也可以是主機名、端口號對,其中,端口是數據節點的流接口地址,後續我們會繼續討論數據節點的流接口。storageID:數據節點的存儲標識,當數據節點用不同的存儲標識在名字節點上註冊時,名字節點通過這個標識,瞭解到着是一個唄重新使用的數據節點。inofoPort:數據節點WWW服務器的監聽端口,通過何故端口可以使用HTTP/HTTPS協議訪問數據節點。ipcPort:數據節點IPC服務器監聽端口,對應客戶端,該端口提供了ClientDatanodeProtocol接口中定義的服務。相對與DatanodeInfo提供了附加狀態信息包括:容量、已經使用容量、剩餘容量、狀態更新時間、流接口服務線程數、數據節點在集羣中的位置、數據節點狀態燈信息,通過這些信息可以分析數據節點的負載狀態,應用與資源調度、數據節點服務選擇等。
//DatanodeId
public class DatanodeID implements WritableComparable<DatanodeID> {
public static final DatanodeID[] EMPTY_ARRAY = {};
public String name; /// hostname:portNumber
public String storageID; /// unique per cluster storageID protected int infoPort; /// the port where the infoserver is running
public int ipcPort; /// the port where the ipc server is running
}
//DatanodeInfo相關結構public class DatanodeInfo extends DatanodeID implements Node {
protected long capacity;
protected long dfsUsed;
protected long remaining;
protected long lastUpdate;
protected int xceiverCount;
protected String location = NetworkTopology.DEFAULT_RACK;
......
}
出現在ClientProtocol接口上的最後一組類是HdfsFileStatus和DirectoryListing。HdfsFileStatus保存了HDFS文件/目錄的屬性,DirectoryListing用於一次返回一個目錄下的多個文件/子目錄的熟悉,它們都用於實現FileSystem.getFileStatus()方法族。詳情如下:
//HdfsFileStatus
public class HdfsFileStatus implements Writable {
static { // register a ctor
WritableFactories.setFactory
(HdfsFileStatus.class,
new WritableFactory() {
public Writable newInstance() { return new HdfsFileStatus(); }
});
}
private byte[] path; // local name of the inode that's encoded in java UTF8
private long length;
private boolean isdir;
private short block_replication;
private long blocksize;
private long modification_time;
private long access_time;
private FsPermission permission;
private String owner;
private String group;
......
}
//DirectoryListing
public class DirectoryListing implements Writable {
static { // register a ctor
WritableFactories.setFactory
(DirectoryListing.class,
new WritableFactory() {
public Writable newInstance() { return new DirectoryListing(); }
});
}
private HdfsFileStatus[] partialListing;
private int remainingEntries;
......
}
ClientProtocol
客戶端和名字節點間的通信協議定義在ClientProtocol接口,這個接口是個冗長的接口。它主要提供的能力可以分兩大類:用於實現Hadoop文件系統相關功能的能力;用於對HDFS狀態進行查詢、設置的能力。
ClientDataProtocol
和ClientProtocol相比,ClientDatanodeProtocol接口相當簡單,它只有三個方法,recoverBlock()、geBlickInfo()、getBlockLocakPathInfo()。recoverBlock()方法應用於HDFS的客戶端DFSClient的輸出流DFSOutputStram中,客戶端往數據節點輸出數據的過程中,如果某個副本所在的數據節點出現錯誤,客戶端就會嘗試進行數據塊恢復,這時候需要調用recoverBlock(),從正常工作的數據節點中找到恢復點,然後才能繼續輸出數據。getBlockInfo()和HDFS的文件一致性相關。文件一致性模型描述了對文件進行讀寫時數據的可見性。這個方法的輸入是Block對象,輸出是更新的,反映數據變化的新Block對象。而getBlockLocakPathInfo()方法類應用與本地讀優化,執行本地讀的客戶端通過getBlockLocalPathInfo()成員函數,獲得某個數據塊對應的數據塊文件及數據塊校驗信息文件的本地路徑,然後就可以進一步的本地讀操作。
在一般情況下,客戶端和數據節點主要通過基於流的接口進行交互,較少使用ClientDatanodeProtocol中提供的方法。
DatanodeProtocol
DatanodeRegistration類和NamespaceInfo類。由類的名字可知,DatanodeRegistration用於數據節點註冊。它繼承自DatanodeId,在介紹ClientProtocol時,我們已經知道DatanodeID可用於在HDFS中確定一個數據節點,由此可見,在數據節點註冊的時間還必須包含其他的信息,即DatanodeRegistration在DatanodeID基礎上新增加的成員變量exportedKeys和storageInfo,其中exportedKeys用於HDFS的安全特性,我們不再分析,類型爲StrorageInfo的另一個變量保存了數據節點的存儲系統信息。它包括了三個成員變量:layoutVersion:是一個負整數,保存了HDFS存儲系統信息結構的版本號。namespaceID:存儲系統的唯一標識符。cTime:該存儲系統信息的創建時間。當數據節點註冊時,名字節點和數據節點需要對這些信息,即StorageInfo包含的信息進行檢查,以保證當前註冊的節點是HDFS的一個合法數據節點,而不是一個屬於其他集羣的節點,或者曾經屬於集羣,但未進行必要升級的節點,以保證存儲系統的一致性。NamespaceInfo繼承自StorageInfo,除了StorageInfo中的layoutVersion、namespanceID和cTime屬性,它引入了buildVersion和distributedUpgradeVersion。成員變量buildVersion保存了系統構建的(subversion)版本號,distributedUpgradeVersion則用於數據節點升級前進行的版本檢查。注意,NamespaceInfo中包含的信息是整個HDFS集羣的信息,和具體的數據節點沒有關係。
數據節點通過DatanodeProtocol.register()方法向名字節點註冊,註冊成功後,數據節點通過DatanodeProtocol.blockReport()方法上報它所管理的全部數據塊信息,DatanodeProtocol。sendHeartbeat()是數據節點與名字節點的心跳節點,當然這個接口還包括reportBadBlocks()、blockReceived()、errorReport()、processUpgradeCommand()等方法。blockReport()和sendHeartbeat()的返回值都和名字節點指令DatanodeCommand,它們的定義如下:
public interface DatanodeProtocol extends VersionedProtocol {
/**
* 25: Serialized format of BlockTokenIdentifier changed to contain
* multiple blocks within a single BlockTokenIdentifier
*
* (bumped to 25 to bring in line with trunk)
*/
public static final long versionID = 25L;
// error code
final static int NOTIFY = 0;
final static int DISK_ERROR = 1; // there are still valid volumes on DN
final static int INVALID_BLOCK = 2;
final static int FATAL_DISK_ERROR = 3; // no valid volumes left on DN
/**
* Determines actions that data node should perform
* when receiving a datanode command.
*/
final static int DNA_UNKNOWN = 0; // unknown action
final static int DNA_TRANSFER = 1; // transfer blocks to another datanode
final static int DNA_INVALIDATE = 2; // invalidate blocks
final static int DNA_SHUTDOWN = 3; // shutdown node
final static int DNA_REGISTER = 4; // re-register
final static int DNA_FINALIZE = 5; // finalize previous upgrade
final static int DNA_RECOVERBLOCK = 6; // request a block recovery
final static int DNA_ACCESSKEYUPDATE = 7; // update access key
final static int DNA_BALANCERBANDWIDTHUPDATE = 8; // update balancer bandwidth
/**
* Register Datanode.
*
* @see org.apache.hadoop.hdfs.server.datanode.DataNode#dnRegistration
* @see org.apache.hadoop.hdfs.server.namenode.FSNamesystem#registerDatanode(DatanodeRegistration)
*
* @return updated {@link org.apache.hadoop.hdfs.server.protocol.DatanodeRegistration}, which contains
* new storageID if the datanode did not have one and
* registration ID for further communication.
*/
public DatanodeRegistration register(DatanodeRegistration registration
) throws IOException;
/**
* sendHeartbeat() tells the NameNode that the DataNode is still
* alive and well. Includes some status info, too.
* It also gives the NameNode a chance to return
* an array of "DatanodeCommand" objects.
* A DatanodeCommand tells the DataNode to invalidate local block(s),
* or to copy them to other DataNodes, etc.
*/
public DatanodeCommand[] sendHeartbeat(DatanodeRegistration registration,
long capacity,
long dfsUsed, long remaining,
int xmitsInProgress,
int xceiverCount) throws IOException;
/**
* blockReport() tells the NameNode about all the locally-stored blocks.
* The NameNode returns an array of Blocks that have become obsolete
* and should be deleted. This function is meant to upload *all*
* the locally-stored blocks. It's invoked upon startup and then
* infrequently afterwards.
* @param registration
* @param blocks - the block list as an array of longs.
* Each block is represented as 2 longs.
* This is done instead of Block[] to reduce memory used by block reports.
*
* @return - the next command for DN to process.
* @throws IOException
*/
public DatanodeCommand blockReport(DatanodeRegistration registration,
long[] blocks) throws IOException;
......
}
InterDatanodeProtocol
InterDatanodeProtocol有三個方法,getBlockMetaDataInfo()、startBlockRevovery()、updateBlock()方法,其中getBlockMetaDataInfo並沒有被其他HDFS代碼使用,該方法可用於實現HDFS的維護工具。startBlockRevover和updateBlock用於數據塊恢復。前面在ClientDatanodeProtocol接口的時候,就知道數據節點想客戶端提供了錯誤恢復的遠程方法:ClientDatanodeProtocol.recoverBlock方法。它用於對客戶端寫數據過程中出現的故障進行錯誤恢復;名字節點到數據節點的指令DNA_RECOVERBLOCK也可可發起一個數據塊恢復。
爲IPC建立連接是一個開銷比較大的操作,在正常的數據節點寫操作的過程中,節點間不需要通過遠程方法調用進行交互。但故障發生後,參與到這個寫操作的數據節點彙總,有一個數據節點會被選中出來,協調其他數據節點,進行故障恢復,該過程需要使用到InterDatanodeProtocol中的這兩個遠程方法。
NamenodeProtocol
名字節點實現了該遠程接口,調用者有兩個,一個是第二名字節點,另一個而是HDFS工具:均衡器balancer。均衡器使用了NamenodeProtocol的getBlocks()和getBlockKeys()方法,getBlocks()可以獲取某一個數據節點上的一系列數據塊及位置,根據這些返回值,均衡器可以把數據塊從該數據節點移動到其他數據節點,達到平衡各個數據節點數據塊數量的目的;getBlockKes()方法用於支持這個過程中需要的安全特性。
NamenodeProtocol中的另外三個方法(getEditLogSize、rollEditLog、rollFsImage)相互配合,完成第二名字節點的功能:獲取HDFS的命名空間鏡像和鏡像編輯日誌,合併得到一個新的鏡像,上傳新命名空間鏡像到名字節點,替換原有鏡像並清空鏡像編輯日誌。
版權申明:本文部分摘自【蔡斌、陳湘萍】所著【Hadoop技術內幕 深入解析Hadoop Common和HDFS架構設計與實現原理】一書,僅作爲學習筆記,用於技術交流,其商業版權由原作者保留,推薦大家購買圖書研究,轉載請保留原作者,謝謝!