Hadoop源码剖析--HDFS的数据存储

一、HDFS内存存储原理

HDFS的数据存储包括两块:(1)HDFS内存存储;(2)HDFS异构存储。

HDFS内存存储是一种十分特殊的存储方式,将会对集群数据的读写带来不小的性能提升,而HDFS异构存储则能帮助我们更加合理地把数据存到应该存的地方。

HDFS的LAZY_PERSIST内存存储策略用的是下面的这种方法,

Hadoop源码剖析--HDFS的数据存储

其中第4步写数据到内存中,第6步异步地将数据写到磁盘,前面几步是如何设置StorageType的操作,在下文中会具体提到。所以异步存储的大体步骤可以归纳如下:

  1. 对目标文件目录设置StoragePolicy为LAZY_PERSIST的内存存储策略。
  2. 客户端进程向NameNode发起创建/写文件的请求。
  3. 客户端请求到具体的DataNode后DataNode会把这些数据块写入RAM内存中,同时启动异步线程服务将内存数据持久化写到磁盘上。

LAZY_PERSIST名称的源由就是数据不是马上落盘,而是懒惰的、延时地进行处理。

那么什么叫做虚拟内存盘呢?在Linux中,的确有将内存模拟为一块盘的技术,叫虚拟内存盘。这是一种模拟的盘,实际数据都是存放在内存中的。因此我们将机器内存利用起来,作为一块独立的虚拟盘供DataNode使用了。

二、HDFS内存存储流程分析

    要想让文件数据存储到内存中,我们就需要设置一个存储策略,就是上面提到的LAZY_PERSIST,而不是默认的存储策略:StoragePolicy.DEFAULT,默认的存储策略是DISK类型。设置存储策略有以下3种方法:

  1. 通过命令行的方式,调用如下命令:

 

hdfs storagepolicies –setStoragePolicy –path <path> -policy LAZY_PERSIST

 

  1. 第二种方法是调用对应的程序方法,比如调用暴露在外部的create文件方法,但是得带上参数CreateFlag.LAZY_PERSIST。
  2. 还有一种方法是通过FileSystem的setStoragePolicy方法,不过这个方法在2.8版本之后会提供,如下所示:

 

fs.setStoragePolicy(path,”LAZY_PERSIST”);

 

    设置好了LAZY_PERSIST存储策略之后,我们可以想一个问题,当DataNode内存不足的时候怎么办呢?这里采用的是LRU(Least Recently Used)机制,意为最近最少使用算法。其中里面有一个getNextCandidateForEviction方法会采用LRU算法把最近很久没有访问过的块给移除掉,并且在内存中移除与候选块属于同一副本信息的块并释放内存空间。

 

三、LAZY_PERSIST内存存储的使用

    上面说到内存存储使用虚拟内存盘来存储,首先需要将机器中已经完成好的虚拟内存盘配置到dfs.datanode.data.dir中,其次还要带上RAM_DISK标签,以此表明此目录对应的存储介质为RAM_DISK,配置样例如下:

 

<property>

    <name>dfs.datanode.data.dir</name>

    <value>/grid/0,/grid/1,/grid/2,[RAM_DISK]/mnt/dn-tmpfs</value>

</property>

 

    注意,这个标签是必须是打上的,否则HDFS默认的都是DISK。

    其实,在HDFS异构存储方式中,除了内存存储之外,就是HDFS的Archival Storage。它一般用于冷数据的存储。

 

四、HDFS异构存储

    异构存储类型在HDFS中声明了一下几种Storage Type:

  1. RAM_DISK
  2. SSD
  3. DISK
  4. ARCHIVE

那么我们如何让HDFS知道集群中的数据存储目录分别是哪种类型的存储介质呢?这就需要在配置属性时主动声明,HDFS并没有自动检测识别的功能。配置属性dfs.datanode.data.dir可以对本地对应存储目录进行设置,同时带上一个存储类型标签,声明此目录用的是哪种类型的存储介质,例子如下,

 

[SSD]file:///grid/dn/ssd0

 

了解完异构存储类型后,我们来了解一下HDFS异构存储的实现原理。HDFS异构存储可总结为以下三点:

  1. DataNode通过心跳汇报自身数据存储目录的StorageType给NameNode。
  2. 随后NameNode进行汇总并更新集群内各个节点的存储类型情况。
  3. 待复制文件根据自身设定的存储策略信息向NameNode请求拥有此类型存储介质的DataNode作为候选节点。

那么第一步我们是如何知道他的数据存储目录呢?这里使用了一个方法来获取数据存储目录,

 

conf.getTrimmedStringCollection(DFS_DATANODE_DATRA_DIR_KEY);

当然,我们也很关心如何解析配置并最终得到对应存储类型的过程,即下面执行的内容(这里解析的是上面红色配置的部分,使用代码对它进行解析即可):

 

location = StorageLocation.parse(locationString);

 

    StorageLocation的解析方法如下:

 

public static StorageLocation parse(String rawLocation)

    throws IOException,SecurityException{

  Matcher matcher = regex.matcher(rawLocation);

  StorageType storageType = StorageType.DEFAULT;

  String location = rawLocation;

  

  if(matcher.matches()){

    String classString = matcher.group(1);

    location = matcher.group(2);

    if(!classString.isEmpty()){

        storageType = 

           StorageType.valueOf(StringUtils.toUpperCase(classString));

         }

}

return new StorageLocation(storageType,new Path(location),toUrl());

}

 

    后续这些解析好的存储目录以及对应的存储介质类型会加入到storageMap中。storageMap存储了目录到类型的映射关系,可以说是非常细粒度的。更重要的是,这些信息会被DataNode组织成StorageReport通过心跳的形式上报给NameNode。StorageReport里面包括StorageMap和磁盘存储信息。它最终被BPServiceActor的sendHeartBeat调用发送给NameNode。因此NameNode会收到StorageMap、坏磁盘数据信息、DataNode自身存储的容量信息。

    接下来就是第二阶段的心跳处理过程。心跳处理在DataNodeManager的handlerHeartbeat中进行。最终在heartbeatManager中会调用到DatanodeDescription对象的updateHeartbeatState方法,该方法会更新Storage的信息,这里面还会统计心跳的更新次数,如下所示:

 

//进行统计计数的更新统计

totalCapacity += report.getCapacity();

totalRemaining += report.getRemaining;

totalBlockPoolUsed += report.getBlockPoolUsed();

totalDfsUsed += report.getDfsUsed();

 

    接下来就进入的第三阶段,会存储一些副本节点的存储类型。有目标存储介质需求的待复制文件块就会向NameNode请求DataNode,这部分处理在FSNamesystem的getAdditionDatanode中进行。它的实现步骤是:

  1. 获取策略id—》存储策略(RAM,DISK,….)
  2. 寻找对应的存储目录
  3. 通过StoragePolicyID选择满足需求的节点

最后,对大数据以及ios感兴趣的朋友可以关注“大数据及周边技术”公众号或者加小编微信进群交流。

Hadoop源码剖析--HDFS的数据存储

END:

推一本小编近期要上的一本书《Hive数据仓库企业级应用》

Hadoop源码剖析--HDFS的数据存储