HDFS小文件问题解决方案与启发实践

前言


继上文聊聊HDFS BlockManager的服务化改造之后,本文我们继续来讨论HDFS扩展性相关的问题。在本文的阐述过程中,我们将通过一个平时遇到的典型问题-HDFS小文件过多问题作为贯穿全文的一个核心要点。在下文中,笔者将会介绍小文件的缘由,现有解决办法,新的解决方案等等内容。

小文件的产生以及影响


这里“小文件”的一个标准定义不应该说是绝对大小非常小的文件,这样说不够准确,而是应该值不满足一个块大小并且文件本身非常小的文件(比如大量不大1MB的文件)。小文件产生过多的原因很大一部分归结于用户的应用程度在执行的时候没有很好的预估写出数据量的规模,导致写出过多的小文件

如果小文件产生过多了,它会有什么严重的影响呢?主要为下面2点:

  • 加重HDFS的namespace命名空间,因为过多的小文件意味着更多文件元数据信息需要NameNode来保存了。
  • 读取小文件数据的任务执行时,消耗过多的集群资源。因为map task在执行的时候一般只处理1个文件,如果这个时候要读取的文件过多,就会造成大量的map task启动。

以上两点影响中第二点可能会被大家所忽视。下面我们来看现有条件下的
几种解决方案。

现有小文件问题解决方案


小文件问题往往在集群数据规模迅速扩张的时候被暴露出来,它的影响不是说本身占据了多少存储空间,而是说它会大大加大NameNode的负载,从而会影响到NameNode的性能问题。在现有的Hadoop系统中,有什么办法可以解决小文件问题呢?在cloudera官方的一篇博客(博客链接可见本文末尾链接处)中,给了我们一个很好的答案。下面是笔者对其的简要概括。

第一个解决方案,通过文件归档的方式进行处理。文件归档在这里的意思是将文件再次进行整理和保存,使之更易管理和保存。而Hadoop中的归档是在HDFS之上又构建了一个新的抽象层,叫HAR(Hadoop Archives ),访问的格式变为了har:// URL。它的实现原理如下图1-1所示。




图 1-1 Hadoop 归档原理

从上图我们可以看出,Hadoop在归档文件时通过二层索引文件的查找,进行最终文件的读取。所以在效率上会比普通HDFS读取文件慢一些。Hadoop归档文件可以通过hadoop archive归档命令产生,归档文件产生后并不会删除原文件,此时可以由用户决定是否保留原始文件。归档文件是不可修改的,当执行删除,重命名操作时会抛出异常。Hadoop归档文件相关内容可以详细阅读官方地址:http://hadoop.apache.org/docs/current/hadoop-archives/HadoopArchives.html

第二个解决方案,通过改变文件的写出方式,写入到SequenceFile格式的文件中。这主要是因为SequenceFile独有的存储格式决定了它可以很好的满足小文件存储的需求。SequenceFile文件内部存储数据的方式是以下面key-value的形式拼接而成。




图 1-2 SequenceFile内部存储形式

因为考虑到小文件中的内容少,在这里我们可以以文件名作为key,文件内容作为value,直接写到SequenceFile中

第三种解决方案,依赖外部系统的数据访问模式进行数据的管理,比如HBase。我们通过写HBase的方式进行数据的写入而不是直接写HDFS,HBase数据的存储格式也是类似于SequenceFile中的K-V的格式。

但是说了这么多,以上3种解决方案并没有从本质上解决小文件问题,它们都或多或少把这个问题绕开了或者说是一种变相的调和了。如果要把它从根本上解决,还是得从HDFS内部进行改造。

HDFS小文件问题启发实践


小文件问题是HDFS使用者经常会遇到的问题,这直接关系到HDFS的扩展性问题。对此,在Hadoop社区内部有过很多相应的讨论和方案设想。下面笔者举出个人觉得比较完整的两套方案。

HDFS小文件存储


第一套方案源自HDFS-8998(Small files storage supported inside HDFS),此issue解决的问题是在HDFS内部支持小文件的存储管理。

它的核心思想如下:

  • 1.在HDFS内部创建一个小文件存储空间,在其下指定最大可存储的块列表为N个。换句话说,这N个block块是专门用来存储接下来的小文件的。
  • 2.当第1个客户端发起了一次请求,NameNode随之分配N个块中的第1个块给此客户端,并锁住此块。客户端请求操作完成才释放此块。
  • 3.当第2个客户端来的时候,NameNode会分配一个没有锁住的块给客户端,如果没有找到,则分配一个新块给第2个客户端。后面的客户端请求处理同样如此。
  • 4.如果某个块被写满,也会分配新的一个块给客户端,然后写满的块将从N个候选块列表中移除,表示此块将不再接受写处理。
  • 5.当N个块中没有未锁住的块并且NameNode无法再申请新块的时候,则当前客户端必须等待其它客户端操作完毕,并释放块。

可能上面5点说完,大家还不是太理解,下面是此过程的简单展示,见图1-3。




图 1-3 HDFS小文件存储

相当于一个每个块只分配一个客户端处理,但是它与普通HDFS写数据的一个最大的不同点在于它还可以被其他客户端复写。也就是说,小文件存储空间中的块可以包含多个文件的数据片段,从而达到对于block块的合理利用,如下图所示。




图 1-4 HDFS小文件存储块结构图

图1-4中块内的空白部分的意思是指此块还未被写入部分。这样的块数据存储模式改变了传统方式的一个块属于一个文件的模式,造成一个块可能对应多个文件的局面。因此这里需要一个额外的服务,来记住各个文件在块内的偏移量,从而准确的读取到相应的文件数据

还有一点比较有意思,在这种存储模式下,删除文件的操作就不是简单的块删除操作了,因为块内还包含了其它文件的内容。所以删除的操作应该是这样的:文件对应的INodeFile对象可以从NameNode中删除,对应的块对象应该被重写,在重写的过程中过滤掉待删除文件的内容。在原始文档中,做了一个改进,是通过一个阈值(阈值控制的是无效数据占block块总存储数据的比例)来做重写动作,而不是每次删除文件操作都做一次重写。

HDFS-8998与之前Ozone有点类似之处,所以目前此issue还是处于Open状态。不过这个想法我们可以好好学习一下。下面来看第二个方案,也比较有意思。

HDFS命名空间的键值存储

第二套方案同样来自社区,issue编号HDFS-8286(Scaling out the namespace using KV store)。它解决的问题就不仅仅针对的是HDFS小文件问题了,更准确地来说,它要解决的是HDFS的扩展性问题,比如说在几千、几万个节点的情况下,NameNode是否还能正常工作。方案的提出者来自于horhortonworks的一位Hadoop Commiter。

正如HDFS-8286中标题所显示的那样,此方案的一个核心改造点在于将HDFS命名空间以K-V的形式进行存储,从而利于其扩展性。

在现有HDFS当中,INode对象的组织是通过树型的形式,也就是说,如果给定一个路径的话,它需要通过它的父亲节点进行进一步的查找,具体细节大家可以学习相关类INodesInPath。

而设计者在此使用K-V形式来做元数据的存储是有原因的,一旦我们将NameNode的元数据成功地变为了K-V的存储形式,那么在下一阶段,我们可以将其移入外部的更加完整K-V存储服务中,比如Redis、LevelDB等等。

下面我们来学习一下HDFS-8286所提出的K-V存储的过程。首先它并不是我们想象中的全路径–>INodeFile对象这样简单的映射关系。如果是这样的话,当目录树足够深的情况下,那么全路径可能会非常长。而且,如果采用全路径的话,同一个目录下的全路径会存在重复的前缀。这会比较浪费存储空间。笔者直接想到的是这2点的不足,可能还有其它的弊端。

而设计者在此提出了一个新的K-V关系映射:

  • inode id对于INode对象的映射,表示为[inode id, INodeFile]
  • Key [parent inode id, child filename] 到 inode id的映射。表示为[[parent inode id, child filename], inode id]。

上述2者映射关系分别有不同的作用,第一个关系很好理解,就是通过inode id匹配INode对象。第二个关系比较巧妙,是通过构造新的key [parent inode id, child filename]值来找某INodeFile下的某子文件id。然后再通过找到的inode id,再去找对应的INode对象

举个例子,如下图1-5所示:




图 1-5 HDFS K-V键值查找INode对象过程

图1-5所示的过程为解析查找/foo/bar文件的过程,

  • 1.因为根目录的inode id 为1,首先组合出一个key[1, foo],此key查找的是对象/foo。
  • 2.查得/foo的INode对象id为2,再次构造下一个key,[2, bar],此时key查找的对象是/foo/bar。
  • 3.查得/foo/bar的INode对象id为3,然后再次查询K-V库,得到inode id为3的INodeFile对象,此对象即为bar文件信息。

我们可以看出,这样的键值存储还是比较高效的,至少说每次保存的值都是有效值,不存在冗余的数据。可能唯一美中不足之处在于它会需要做很多次的查询解析。

HDFS命名空间键值对存储的好处在于其未来的扩展性,而不是说短期内的好不好用的问题。如果NameNode中的元数据成功迁移到了K-V存储的形式,那么在下一阶段的时候,我们可以进一步的优化,将其存储在更优的K-V存储系统中,比如LevelDB。LevelDB在内存中会存储近期内被更改的数据集,而对于HDFS而言,活跃的数据一般不会太多,绝大多数更应该是冷数据。无疑这将会大大提高NameNode的扩展性。 这也是原作者提出的很重要的一点。

参考资料


[1].https://issues.apache.org/jira/browse/HDFS-8286
[2].https://issues.apache.org/jira/browse/HDFS-8998
[3].https://issues.apache.org/jira/secure/attachment/12729255/hdfs-kv-design.pdf
[4].https://issues.apache.org/jira/secure/attachment/12754049/HDFS-8998.design.001.pdf
[5].http://blog.cloudera.com/blog/2009/02/the-small-files-problem

posted @ 2020-01-12 19:08  回眸,境界  阅读(308)  评论(0编辑  收藏  举报