与块相关的操作由Dataset相关的类处理,存储结构由大到小是卷(FSVolume)、目录(FSDir)和文件(Block和元数据等)
block相关
block类有三个属性
private long blockId;//blockid
private long numBytes;//block大小
private long generationStamp;//block版本号
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操作。
FSDataset,FSVolumeSet,FSVolume,FSDir之间的关系
由于DataNode上可以指定多个Storage来存储数据块,HDFS规定了一个目录能存放Block的数目,所以一个Storage上存在多个目录。
对应的,FSDataset中用FSVolume来对应一个Storage,FSDir对应一个目录,所有的FSVolume由FSVolumeSet管理,FSDataset中通过一个FSVolumeSet对象,就可以管理它的所有存储空间。
FSDir相关
FSDir对应着HDFS中的一个目录,目录里存放着数据块文件和它的元文件。默认情况下,每个目录下最多有64个子目录,最多能存储64个块。在初始化一个目录时,会递归扫描该目录下的目录和文件,从而形成一个树状结构。
addBlock方法用来添加块到当前目录,如果当前目录不能容纳更多的块,那么将块添加到一个子目录中,如果没有子目录,则创建子目录。getBlockInfo和getVolumeMap方法用于递归扫描当前目录下所有块的信息。clearPath方法用于删除文件时,更新文件路径中所有目录的信息。
FSDir主要有四个属性
File dir;//存储路径的子目录current/
int numBlocks = 0;//存储目录当前已经存储的数据块的数量
FSDir children[];//目录current/的子目录
int lastChildIdx = 0;//存储上一个数据块的子目录序号
public File addBlock(Block b, File src) throws IOException
private File addBlock(Block b, File src, boolean createOk, boolean resetIdx) throws IOException
当有数据块到达DataNode节点时,DataNode并不是马上在current/中为这个数据块选择合适的存储目录,而是先把它存放到存储路径的tmp/子目录下,当这个数据块被DataNode节点成功接受之后,才把它移动到current/下的合适目录中
DataNode节点会首先把文件的数据块存储到存储路径的子目录current/下;当子目录current/中已经存储了maxBlocksPerDir个数据块之后,就会在目录current/下创建maxBlocksPerDir个子目录,然后从中选择一个子目录,把数据块存储到这个子目录中;如果选择的子目录也已经存储了maxBlocksPerDir个数据块,则又在这个子目录下创建maxBlocksPerDir个子目录,从这些子目录中选一个来存储数据块,就这样一次递归下去,直到存储路径的剩余存储空间不够存储一个数据块为止。maxBlocksPerDir的默认值是64,但也可以通过DataNode的配置文件来设置,它对应的配置选项是dsf.datanode.numblocks。
FSVolume相关
FSVolume类的主要属性为
private FSDir dataDir; //存储有效的数据块的最终位置(current/)
private File tmpDir; //存储数据块的中间位置(tmp/)
private File detachDir; //存储数据块的copy on write(detach/)
private DF usage; //获取当前存储目录的空间使用信息
private DU dfsUsage; //获取当前存储目录所在的磁盘分区空间信息
private long reserved; //预留存储空间大小
每一个FSVolume在初始化的时候都进行了恢复操作,它会尽量恢复可能由于DataNode所在节点宕机而造成影响
1).对于detach/下的所有数据块文件(detach/下不存在目录,只有文件),如果该文件在current/下不存在,则把它移动到current/下,最后清空detach/目录
2).如果DataNode节点被设置为支持append操作(对应的配置项为dfs.support.apend),那么对于blocksBeingWritten/下的所有数据块文件(blocksBeingWritten/下不存在目录,只有文件),如果该文件在current/下不存在,则把它移动到current/下,最后清空blocksBeingWritten/目录;否则清空blocksBeingWritten/目录。
FSVolume(File currentDir, Configuration conf) throws IOException { this.reserved = conf.getLong("dfs.datanode.du.reserved", 0); this.dataDir = new FSDir(currentDir); this.currentDir = currentDir; boolean supportAppends = conf.getBoolean("dfs.support.append", false); File parent = currentDir.getParentFile(); this.detachDir = new File(parent, "detach"); if (detachDir.exists()) { recoverDetachedBlocks(currentDir, detachDir);//从detach目录恢复 } this.tmpDir = new File(parent, "tmp"); if (tmpDir.exists()) { FileUtil.fullyDelete(tmpDir);//删除tmp目录 } blocksBeingWritten = new File(parent, "blocksBeingWritten"); if (blocksBeingWritten.exists()) { if (supportAppends) { recoverBlocksBeingWritten(blocksBeingWritten);//从blocksBeingWritten恢复 } else { FileUtil.fullyDelete(blocksBeingWritten); } } ... this.usage = new DF(parent, conf); this.dfsUsage = new DU(parent, conf); this.dfsUsage.start(); }
另外几个重要的函数是
File addBlock(Block b, File f) throws IOException//这里的f可能是tmp目录下的文件 { File blockFile = dataDir.addBlock(b, f); File metaFile = getMetaFile(blockFile, b); dfsUsage.incDfsUsed(b.getNumBytes() + metaFile.length()); return blockFile; } long getCapacity() throws IOException { if (reserved > usage.getCapacity()) { return 0; } return usage.getCapacity() - reserved;//getCapacity()函数在这里调用了2次是 没有必要的 } long getAvailable() throws IOException { long remaining = getCapacity() - getDfsUsed(); long available = usage.getAvailable();//这里是一个bug 应该是loage.getAvailable() - reserved; 因为上面的减去了reserved if (remaining > available) { remaining = available; } return (remaining > 0) ? remaining : 0; }
使用DF、DU类来定期的更新这个“分区”的空间使用信息,使得统计的准确性.这个在下面在介绍.
DU,DF的使用
为了能够比较准确地获取一个DataNode节点的存储空间的总容量、使用量和可用量,HDFS通过程序实现了unix系统的df、du命令,它们被分别用来获取系统本地磁盘的使用情况和目录或文件的大小信息。
HDFS通过org.apache.hadoop.fs.DF类来实现unix的df命令,org.apache.hadoop.fs.DU类来实现unix的du命令。DF类和DU类都是通过使用java程序执行Shell脚本命令来是想各自的功能的。
简单类图如下,
FSVolumeSet相关
FSVolumeSet对所有的FSVolume对象进行管理,实际上就是对所有的存储路径进行管理。FSVolumeSet主要为上层(DataNode进程)提供存储数据块选择一个的存储路径(分区),就是为该数据块创建一个对应的本地磁盘文件,同时也负载统计它的存储空间的状态信息和收集所有的数据块信息。在FSVolumeSet中使用getNextVolume()方法来实现负载均衡,其实就是循环队列.
synchronized FSVolume getNextVolume(long blockSize) throws IOException { if (curVolume >= volumes.length) { curVolume = 0; } int startVolume = curVolume; while (true) { FSVolume volume = volumes[curVolume]; curVolume = (curVolume + 1) % volumes.length; if (volume.getAvailable() > blockSize) { return volume; } if (curVolume == startVolume) { throw new DiskOutOfSpaceException("Insufficient space for an additional block"); } } }
综合总结下上面的关系如下图所示
FSDateaSet相关
这个类和函数比较多,也比较复杂,具体的还是看代码吧,重点有如下几个函数
public BlockWriteStreams writeToBlock(Block b, boolean isRecovery) throws IOException;
得到一个block的输出流。BlockWriteStreams既包含了数据输出流,也包含了元数据(校验文件)输出流,这是一个相当复杂的方法。参数isRecovery说明这次写是不是对以前失败的写的一次恢复操作。我们先看正常的写操作流程:首先,如果输入的block是个正常的数据块,或当前的block已经有线程在写,writeToBlock会抛出一个异常。否则,将创建相应的临时数据文件和临时元数据文件,并把相关信息,创建一个ActiveFile对象,记录到ongoingCreates中,并创建返回的BlockWriteStreams。前面我们已经提过,建立新的ActiveFile时,当前线程会自动保存在ActiveFile的threads中。
我们以blk_3148782637964391313为例,当DataNode需要为Block ID为3148782637964391313创建写流时,DataNode创建文件tmp/blk_3148782637964391313做为临时数据文件,对应的meta文件是tmp/blk_3148782637964391313_XXXXXX.meta。其中XXXXXX是版本号。
isRecovery为true时,表明我们需要从某一次不成功的写中恢复,流程相对于正常流程复杂。如果不成功的写是由于提交(参考finalizeBlock方法)后的确认信息没有收到,先创建一个detached文件(备份)。接着,writeToBlock检查是否有还有对文件写的线程,如果有,则通过线程的interrupt方法,强制结束线程。这就是说,如果有线程还在写对应的文件块,该线程将被终止。同时,从ongoingCreates中移除对应的信息。接下来将根据临时文件是否存在,创建/复用临时数据文件和临时数据元文件。后续操作就和正常流程一样,根据相关信息,创建一个ActiveFile对象,记录到ongoingCreates中……
由于这块涉及了一些HDFS写文件时的策略,以后我们还会继续讨论这个话题。
public void updateBlock(Block oldblock, Block newblock) throws IOException;
更新一个block。这也是一个相当复杂的方法。updateBlock的最外层是一个死循环,循环的结束条件,是没有任何和这个数据块相关的写线程。每次循环,updateBlock都会去调用一个叫tryUpdateBlock的内部方法。tryUpdateBlock发现已经没有线程在写这个块,就会跟新和这个数据块相关的信息,包括元文件和内存中的映射表volumeMap。如果tryUpdateBlock发现还有活跃的线程和该块关联,那么,updateBlock会试图结束该线程,并等在join上等待。
public void finalizeBlock(Block b) throws IOException;
提交(或叫:结束finalize)通过writeToBlock打开的block,这意味着写过程没有出错,可以正式把Block从tmp文件夹放到current文件夹。在FSDataset中,finalizeBlock将从ongoingCreates中删除对应的block,同时将block对应的DatanodeBlockInfo,放入volumeMap中。我们还是以blk_3148782637964391313为例,当DataNode提交Block ID为3148782637964391313数据块文件时,DataNode将把tmp/blk_3148782637964391313移到current下某一个目录,以subdir12为例,这是tmp/blk_3148782637964391313将会挪到current/subdir12/blk_3148782637964391313。对应的meta文件也在目录current/subdir12下。
参考url
http://dongyajun.iteye.com/blog/600841
http://blog.csdn.net/xhh198781/article/details/7172649
http://blog.jeoygin.org/2012/03/hdfs-source-analysis-3-datanode-storage.html