继续聊聊HDFS BlockManager扩展性问题

前言


最近一段时间,笔者在业余时间继续对HDFS的扩展性内容进行学习。一句话来概括,越往里深入,越发现里面可以讲的东西越多,涉及到的点也越多。社区在这个方面确实做了很多的讨论。本文所讲述的主题源自于社区JIRA HDFS-7844(Create an off-heap hash table implementation),大意是利用堆外内存(off-heap)来进行元数据的存储。之前我们总是提到的NameNode内存空间使用过大,指的是JVM中的堆内存。而堆外内存是不受JVM管理的,它由操作系统来管理。如果将Block、INode中的数据从堆内移到堆外存储,无疑将会巨大减轻NameNode压力。在下文中,笔者会花一定篇幅介绍此内容,并同时介绍HDFS块优化相关的内容。

堆外内存概述


堆外内存,通俗地理解,就是不受JVM控制的内存。堆外内存在使用上能支持更大空间的内存存储。还有一点好处是堆外内存可以在JVM间共享,减少对象的复制。

在现有JDK中,有2种可以对堆外内存进行使用的API。

第一种,nio包中的ByteBuffer相关的API。如下代码示例:

ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);

第二种,调用Unsafe方法进行堆外内存的使用,示例代码如下:

Unsafe.allocateMemory(1024 * 1024 * 20);

以上2种方法的使用类似于C语言中的malloc方法。

关于堆外内存更多的资料大家可以自行查找资料进行学习。

HDFS堆外内存哈希存储的设计


在了解完堆外存储的基本概念之后,我们重新回到本文的主题。在HDFS-7844中,以off-heap的方式实现了一个哈希存储表。作者在设计此存储表的目的是为了将来存Block块数据、INode文件数据做使用的。

在原设计中,作者并不是直接在哈希表上对内存做直接管理,而是先定义内存管理器类,此内存管理器类是可配的,在这里它有可能就是off-heap的。在内存管理器内部,才是对数据进行直接存入、取出的操作

相比于off-heap的内存管理器,on-heap的内存管理器显得更复杂一些,它的内部定义了这么几个变量:

1.当前分配空间的起始地址(此地址会随着空间不断分配出去而后移)。
2.最大分配地址。此处我们基本可以认定为无上限,除非我们想要额外指定。
3.buffer map数据。此map存储了起始存储地址到缓存数据的映射。

通过如上3个变量的定义,我们大致可以勾画出下面的一张结构图。



图1-1 内存管理结构图

上图可理解为在一大片的存储空间内,被分割为了各个小的buffer缓冲,这些buffer缓冲数据由他们的起始存储地址作为key进行定位查找。

而off-heap的内存管理则显得简单许多,下面列举部分利用堆外内存进行内存分配管理的代码:

下面首先是定义以及初始化方法,

/**
  * NativeMemoryManager is a memory manager which uses sun.misc.Unsafe to
  * allocate memory off-heap.  This memory will be allocated using the current
  * platform's equivalent of malloc().
  */
 @Private
 @Unstable
 public class NativeMemoryManager implements MemoryManager {
  static final Logger LOG =
      LoggerFactory.getLogger(NativeMemoryManager.class);

   private final static Unsafe unsafe;

   private final static String loadingFailureReason;

   static {
     Unsafe myUnsafe = null;
     String myLoadingFailureReason = null;
     try {
       Field f = Unsafe.class.getDeclaredField("theUnsafe");
       f.setAccessible(true);
       myUnsafe = (Unsafe)f.get(null);
     } catch (Throwable e) {
       myLoadingFailureReason = e.getMessage();
     } finally {
       unsafe = myUnsafe;
       loadingFailureReason = myLoadingFailureReason;
     }
   }

用Unsafe方法分配地址空间时,会在方法结束后返回一个起始地址,于是这边就不需要人工地进行地址的累加。

   public long allocate(long size) {
     return unsafe.allocateMemory(size);
   }

然后是数据存取方法,

   @Override
   public byte getByte(long addr) {
     return unsafe.getByte(null, addr);
   }

   @Override
   public void putByte(long addr, byte val) {
     unsafe.putByte(null, addr, val);
   }

如果不使用此空间,则可以进行空间的释放。

    public void free(long addr) {
     unsafe.freeMemory(addr);
   }

假设目前我们已经构建完这块哈希存储表了,我们如何将它有效的运用到目前的Block块数据或者INode文件中呢?首先它可以很好的适配当前DataNode的块组织结构。之前笔者写过一个篇文章介绍过HDFS内部块的组织结构,Block块在DataNode内部被组织为若干个超级大的“链表”,此“链表”并不是由于JDK自带的LinkedList相关类实现的,而是HDFS内部自身实现了一套。如果本文提到的哈希存储表未来能够成功应用到HDFS上,这些链表上的数据将会很好的迁移到哈希表上。

而对于哈希表的数目,我们当然不能仅限只有一个,因为这会带来一定的锁竞争。我们可以维护多个哈希存储表。每次有相应存储表的块数据报告上来的时候,我们就单独锁住需要更新的哈希表即可。

其它优化点


在HDFS-7836的设计文档中,还提到了2点关于块汇报相关的优化点。

第一点,块汇报的拆分。在比较早些时候的块报告的实现中,DataNode每次心跳发送块报告的时候会将所有块信息放在一个message中,一次性发送给NameNode。这会带来很大弊端,如果数据多了,这个信息将会非常大,不仅会导致信息发送慢,还会导致NameNode处理慢。一种更好的做法是将这些storage report进行拆分,每次发送一小部分,分多次发送。在目前HDFS中,已经有类似的配置功能,配置项dfs.blockreport.split.threshold。此配置的意思是给定一个阈值,如果DataNode上总的块数小于此阈值,则块数据分一次性发送,否则按照每个存储目录划分,分多次汇报。

第二点,块汇报信息的压缩。块信息的压缩处理看起来是一个不错的主意。而且目前已经有许多性能较好的压缩算法。如果我们对这些块数据进行了压缩发送,首先它能帮助我们减少很大一部分的网络IO。唯一不足之处可能就是它需要在NameNode端进行一次解压动作。但是以目前计算机处理的速度,这点处理对NameNode本身性能的影响应该是可以忽略的。但比较可惜的一点,据笔者对于此方面的了解,目前HDFS还没实现对于汇报块的压缩功能,大家也慢慢期待吧。

参考资料


[1].https://issues.apache.org/jira/browse/HDFS-7844
[2].https://issues.apache.org/jira/browse/HDFS-7836
[3].https://issues.apache.org/jira/secure/attachment/12700628/BlockManagerScalabilityImprovementsDesign.pdf

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