原文出自云台博客:http://yuntai.1kapp.com/?p=952
HDFS被设计成写一次,读多次的应用场景,这应该跟它的MapReduce机制是紧密关联的,通过对线上的读写比例监控,大概读写比是10:1,也验证了它设计的目标。
GFS论文提到的文件读取简单流程:
在HDFS中,具体流程如下图:
从上图,可以看出读取文件需要如下几个流程:
在此,仅就NameNode接收客户端的RPC请求获取文件,文件相应的块,块位置进行分析,其调用代码流程为:NameNodeRpcServer.getBlockLocations()->FSNamesystem. getBlockLocations()->FSNamesystem. getBlockLocations()->DatanodeManager.sortLocatedBlocks()。具体流程图如下:
从上图可看出,接收Client的RPC请求方法getBlockLocations,检查该路径是否有读取权限,然后进入尝试获取块位置的第一次循环,如果是第一次,首先检查该NameNode节点状态是否允许读操作;然后从NameSpace获取该文件对于的inode对象;如果支持访问时间,且循环是第一次,则contine进入下一个循环,如果为第二次,修改该文件的访问时间,并且开始调用BlockManager类的方法createLocatedBlocks
for (int attempt = 0; attempt < 2; attempt++) {
if (attempt == 0) { // first attempt is with readlock
readLock();//第一次读取,获得读锁
} else { // second attempt is with write lock
//第二次需要写锁,因为需要修改访问时间
writeLock(); // writelock is needed to set accesstime
}
try {
checkOperation(OperationCategory.READ);
// if the namenode is in safemode, then do not update access time
if (isInSafeMode()) {
doAccessTime = false;
}
long now = now();
//从namespace获取该文件的inode对象
INodeFile inode = dir.getFileINode(src);
if (inode == null) {
throw new FileNotFoundException("File does not exist: " + src);
}
assert !inode.isLink();
if (doAccessTime && isAccessTimeSupported()) {
if (now <= inode.getAccessTime() + getAccessTimePrecision()) {
// if we have to set access time but we only have the readlock, then
// restart this entire operation with the writeLock.
if (attempt == 0) {
continue;
}
}
dir.setTimes(src, inode, -1, now, false);
}
//获取文件块位置
return blockManager.createLocatedBlocks(inode.getBlocks(),
inode.computeFileSize(false), inode.isUnderConstruction(),
offset, length, needBlockToken);
} finally {
if (attempt == 0) {
//第一次循环完毕,关闭读锁
readUnlock();
} else {
//第二次循环完毕,关闭写锁,此处锁的粒度是否可以变得更细呢,因为仅在设置访问时间处需要加锁
writeUnlock();
}
}
}
如果文件存在块,需要获取文件块信息及其位置信息
assert namesystem.hasReadOrWriteLock();
if (blocks == null) {
return null;
} else if (blocks.length == 0) {
//如果不存在块
return new LocatedBlocks(0, isFileUnderConstruction,
Collections.emptyList(), null, false);
} else {
if (LOG.isDebugEnabled()) {
LOG.debug("blocks = " + java.util.Arrays.asList(blocks));
}
final AccessMode mode = needBlockToken? AccessMode.READ: null;
//通过文件块列表,获取到块信息,及其副本所在的DN节点列表
final List locatedblocks = createLocatedBlockList(
blocks, offset, length, Integer.MAX_VALUE, mode);
final BlockInfo last = blocks[blocks.length - 1];
//如果最后一个块状态为complete状态,最后位置=文件大小-最后一个块大小
//否则最后位置是文件大小
final long lastPos = last.isComplete()?
fileSizeExcludeBlocksUnderConstruction - last.getNumBytes()
: fileSizeExcludeBlocksUnderConstruction;
//获取文件最后一个块副本所在位置
final LocatedBlock lastlb = createLocatedBlock(last, lastPos, mode);
return new LocatedBlocks(
fileSizeExcludeBlocksUnderConstruction, isFileUnderConstruction,
locatedblocks, lastlb, last.isComplete());
对于方法createLocatedBlockList
,通过名字及其参数就知道,通过该方法,可以获取文件所有的块,块信息(包含块在文件内的起始位置),块所在的DN机器。方法createLocatedBlock用于处理每一个块,只改方法中,如果三个副本中有几个副本损坏了,则只将没有损坏的块加入machines集合,如果三个副本均被损坏,则将三个副本所在的DN均加入machines集合?这是为什么呢?难道在客户端通过标示corrupt再判断该块是否损坏?需要再确认下
。。。。。。。。。。。。。。。。。。。。。。。。
//获取包含该块的DN节点数目
final int numNodes = blocksMap.numNodes(blk);
//是否整个块均被损坏:损坏块副本数等于节点数目
final boolean isCorrupt = numCorruptNodes == numNodes;
//如果某块所有的副本均损坏,则机器的数量为所有副本对应的机器数
final int numMachines = isCorrupt ? numNodes: numNodes - numCorruptNodes;
final DatanodeDescriptor[] machines = new DatanodeDescriptor[numMachines];
int j = 0;
if (numMachines > 0) {
for(Iterator it = blocksMap.nodeIterator(blk);
it.hasNext();) {
final DatanodeDescriptor d = it.next();
final boolean replicaCorrupt = corruptReplicas.isReplicaCorrupt(blk, d);
//如果三个副本中有几个副本损坏了,则只将没有损坏的块加入machines集合
//如果三个副本均被损坏,则将三个副本所在的DN均加入machines集合??这是为什么呢??
//难道在客户端通过标示corrupt再判断该块是否损坏
if (isCorrupt || (!isCorrupt && !replicaCorrupt))
machines[j++] = d;
}
}
assert j == machines.length :
"isCorrupt: " + isCorrupt +
" numMachines: " + numMachines +
" numNodes: " + numNodes +
" numCorrupt: " + numCorruptNodes +
" numCorruptRepls: " + numCorruptReplicas;
final ExtendedBlock eb = new ExtendedBlock(namesystem.getBlockPoolId(), blk);
//eb:通过poolid跟块唯一标示一个块
//machines:副本所在的机器
return new LocatedBlock(eb, machines, pos, isCorrupt);
在获取到文件所有的LocatedBlock对象后,还需要对每个块所对应的DN节点按照与client的距离进行排序,如果离客户端越近,那么就越会排在数组的前面,如果该DN节点已经退役,则排行的数组的最后面,引入排序规则,有利用提高下载文件的速度,具体实现在DatanodeManager.
sortLocatedBlocks()
/** Sort the located blocks by the distance to the target host.
* 通过DN与client的距离,对每个副本的的DN节点进行排序 ,以便于更快地获取到数据*/
public void sortLocatedBlocks(final String targethost,
final List locatedblocks) {
//sort the blocks
final DatanodeDescriptor client = getDatanodeByHost(targethost);
for (LocatedBlock b : locatedblocks) {
//对于单个块对于的副本,按照DN节点与客户端的距离进行排序
networktopology.pseudoSortByDistance(client, b.getLocations());
// Move decommissioned datanodes to the bottom
//如果块的DN节点列表内,有DN为退役状态,则将该DN节点移动DN列表尾部
Arrays.sort(b.getLocations(), DFSUtil.DECOM_COMPARATOR);
}
}