[hadoop源码阅读][8]-datanode-FSDataset

与块相关的操作由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

时间: 2024-08-29 13:32:34

[hadoop源码阅读][8]-datanode-FSDataset的相关文章

Mac搭建Hadoop源码阅读环境

1.本次Hadoop源码阅读环境使用的阅读工具是idea,Hadoop版本是2.7.3.需要安装的工具包括idea.jdk.maven.protobuf等 2.jdk,使用的版本是1.8版,在jdk官网下载jdk-8u111-macosx-x64.dmg,点击安装,一路next. 3.idea安装,略 4.maven,使用的版本是3.3.9,下载apache-maven-3.3.9-bin.tar,解压: tar -zxvf  apache-maven-3.3.9-bin.tar 进入 Mave

Hadoop源码阅读环境搭建

Hadoop源码阅读环境搭建 一.说明 作为一个学习hadoop的同学,必须在本机上搭建hadoop源码阅读环境,这样,在方便阅读源码的同时也方便进行调试和源码修改.好了,下面开始搭建环境. 1.环境说明:hadoop 版本:1.2.1. IDE:eclipse.操作系统:centos 2.网上有人是通过eclipse的新建项目指定目录的方式将hadoop目录转换成Eclipse工程同时导入eclipse,具体做法如下: File-->new-->Java Project-->勾掉Use

IntelliJ IDEA 配置 Hadoop 源码阅读环境

1.下载安装IDEA https://www.jetbrains.com/idea/download/#section=windows 2.下载hadoop源码 https://archive.apache.org/dist/hadoop/core/ 3.使用IDEA打开hadoop源码 4.配置自定义Maven配置文件 file -> setting -> 如下图设置: 附(需要使用aliyun的源,默认国外源基本用不了): 1 <?xml version="1.0"

基于Eclipse构建Hadoop源码阅读环境

一.工具 1.hadoop-2.6.0-src.tar 2.eclipse 3.maven 4.protoc二.下载源码地址:http://mirrors.hust.edu.cn/apache/hadoop/common/三.准备maven包:eclipse-maven3-plugin M2_HOME E:\apache-maven-3.3.3 path ;%M2_HOME%\bin 测试:cmd-->mvn -v四.protoc安装 1.准备:protoc-2.5.0-win32.zip.pr

Apache Hadoop 源码阅读

总之一句话,这些都是hadoop-2.2.0的源代码里有的.也就是不光只是懂理论,编程最重要,还是基本功要扎实啊.... 在hadoop-2.2.0的源码里,按Ctrl + Shift + T . 跳进某个方法里,按F5.F6.   跳出某个方法里,按F7.

hadoop源码阅读

1.Hadoop的包的功能分析 2.由于Hadoop的MapReduce和HDFS都有通信的需求,需要对通信的对象进行序列化.Hadoop并没有采用java的序列化,而是引入它自己的系统.org.apache.hadoop.io中定义了大量的可序列化对象,他们都实现了Writable接口. 3.介绍完org.apache.hadoop.io以后,我们开始来分析org.apache.hadoop.ipc.RPC采用客户机/服务器模式. 4.既然是RPC,自然就用客户端和服务端,当然,org.apa

Hadoop源码阅读-HDFS-day2

昨天看到了AbstractFileSystem,也知道应用访问文件是通过FileContext这个类,今天来看这个类的源代码,先看下这个类老长的注释说明 1 /** 2 * The FileContext class provides an interface to the application writer for 3 * using the Hadoop file system. 4 * It provides a set of methods for the usual operatio

Hadoop学习系列笔记一:搭建hadoop源码阅读环境

本文来源于<Hadoop技术内幕深入解析Hadoop common和HDFS架构设计与实现原理> 一.Hadoop基本概念 Hadoop是Apache基金会下的一个开源分布式计算平台,以Hadoop分布式文件系统(HDFS)和MapReduce分布式计算框架为核心,为用户提供了底层细节透明的分布式基础设施. HDFS的高容错性.高伸缩性等优点,允许用户将Hadoop部署在廉价的硬件上,构建分布式系统. MapReduce分布式计算计算框架则允许用户在不了解分布式系统底层细节的情况下开发并行.分

Eclipse Hadoop源码阅读环境

一.解压hadoop src包到workspace目录 二.File->Import->Existing Maven Projects,在Root Directory处选择解压好的目录,Finish 三.等待maven把依赖包下载好后,会出现一些问题,下面是重头戏. 四.maven问题解决 (1)若发现maven依赖包找不见,找到位置后删除jar包目录,然后右击工程,Alt+F5,确认后会自动下载.不要手动下载第三方包,可能不识别 (2)报 maven-resources-plugin pri