HDFS的EC(Erasure Coding,纠删码)和块管理

前言

基于我们常见的副本复制机制,HDFS 会将每个replica复制到多个rack和host上。 复制(replication)提供了一种简单而强大的冗余形式,可以防止大多数故障情况。这种副本策略简单而直接:即然我们怕数据丢失,那就把相同的数据复制多份,放到不同的地方。

但是这种通过复制来实现数据安全的方式显然会带来额外的存储空间开销。比如,HDFS默认的3副本存储策略,会带来200%的额外存储开销,在写数据的时候也增加了额外的200%的写流量。而这部分额外的开销仅仅是在极少发生的某个副本丢失的情况下才会发生价值。多副本的放置策略带来的数据本地访问的收益我认为也非常有限和牵强,很多情况下,读取数据的客户端和数据根本不运行在一个集群,即使在一个集群(比如NM和DN伴随部署),假如集群规模很大,客户端和数据块在一起的概率也不大。

一个自然的改进是使用纠删码(Easure Coding)代替复制,它使用更少的存储空间,同时仍然提供相同级别的容错能力。 在典型配置下,与 3x 复制相比,EC 可以将存储成本降低约 50%。 纠删码是Cloudera 和英特尔的工程师与更广泛的 Apache Hadoop 社区合作,推动了 HDFS-7285下的 HDFS-EC 项目。 目前,Hadoop 3.0 开始支持纠删吗。

但EC也绝非完胜Replication,因为本质上EC是用计算换存储,即,虽然实现了更小的存储空间,但是需要用更多的计算资源,比如数据写入的时候的额外计算,以及数据恢复时候的额外计算。

在阅读跟块管理相关的代码时,一个典型的感觉是混乱

  1. NameNode代码中关于Block和Replica的定义不够清楚,很多时候相互混用。举个很简单的例子,我们最常说的Data的Block Report(块汇报),其实,DataNode汇报的并不是Block,而是Block中的一个Replica而已,比如基于复制的连续布局,DataNode汇报的仅仅是一个副本(replica),在纠删码中的条带布局中,一个DataNode汇报的仅仅是一个Internal Block(replica)。
  2. 有了EC和条带布局以后,逻辑块和物理块的定义不在清晰,很多类比如BlockInfo, Block在这里用来表示逻辑块,在那里用来表示物理块。
    这些混乱给跟块相关的代码的理解带来了很多困难。在本文中,我力求精确,约定如下:
连续布局条带布局
Block(块)三个相同物理块的副本集合一个逻辑块,包含了多个不同的物理块
Replica(副本)存储在某台机器的单个物理副本一个物理块(也叫Internal Block或者Physical Block),或者是数据物理块data block,或者是校验物理块parity block
Logical Block(逻辑块)逻辑块和物理块含义相同一个逻辑块,包含了多个不同的物理块
Internal/Physical Block(物理块/内部块)逻辑块和物理块含义相同一个物理块,有可能是数据物理块data block,有可能是校验物理块parity block

本文依然按照从上到下的原则,先讲解纠删码的基本原理,再讲解纠删码在Hadoop中的设计和实现。

这篇文章目前还处于草稿状态,没怎么校对,有时间会校对然后不断修改更新。

1. 纠删码的基本介绍以及和基于复制的方案的对比

1.1 纠删码简介以及和传统副本复制方式的对比

在Cloudera的文章Introduction to HDFS Erasure Coding in Apache Hadoop中对纠删码有很详细的上层介绍。本文有一小部分内容来源于该文章,如有侵权,我会立刻删除。
在我们比较不同的数据存储方案的时候,有两个考虑因素,数据持久性(这种存储方案可以容忍怎样的数据副本失效而保证数据依然不丢失),数据的使用效率(去重以后的有效数据占总存储数据的比值)。比如,HDFS的默认3副本存储方案,其数据持久是可以容忍最多两副本的丢失,同时,数据的使用效率是1/3,即虽然存储了三副本,但是有两份副本都作为备份。
基于纠删码的存储策略,就是为了在保证数据持久性不变的情况下,提高了数据的使用效率。但是也如上文介绍,引入了更多的CPU开销。

最简单形式的纠删码基于异或 (XOR)运算,如表-1 所示的亦或关系表

XYZ
011
000
101
110
表-1 亦或运算关系表

亦或运算可交换的,比如X ⊕ Y = Y ⊕ X。亦或运算也是可任意组合的: X ⊕ Y ⊕ Z = (X ⊕ Y) ⊕ Z。 这意味着 XOR 可以生成 1 来自任意数量的数据位的奇偶校验位。 例如,1 ⊕ 0 ⊕ 1 ⊕ 1 = 1,当第三位丢失时,可以通过对剩余数据位{1,0,1}和奇偶校验位1进行异或来恢复。而异或可以取任意数量的数据单元作为输入,它是非常有限的,因为它最多只能产生一个奇偶校验单元。 因此,基于 XOR 的编码最多可以容忍一个数据丢失,数据的使用效率为 n-1/n(一组 n 个总单元有 n-1 个数据单元),但对于像 HDFS 这样需要容忍多个单元的丢失的系统来说是不够的

EC 的另一种形式 Reed-Solomon (RS) 解决了这一限制。 RS 使用复杂的线性代数运算来生成多个奇偶校验单元,因此可以容忍每组多个故障。 这使其成为生产存储系统的常见选择。 RS 可通过两个参数进行配置:k 和 m。 如图 1 所示,RS(k,m) 的工作原理是将 k 个数据单元的向量与生成矩阵 (GT) 相乘,以生成具有 k 个数据单元和 m 个奇偶校验单元的扩展码字向量。 只要 (k + m) 个单元中的 k 个可用,就可以通过将幸存的单元(数据单元和校验单元)乘以 GT 的转置矩阵来恢复存储故障。 (GT 中对应于故障单元的行应在取其倒数之前删除。)这意味着该组可以容忍任何 m 个单元的故障。


图 1:具有四个数据单元和两个奇偶校验单元的 Reed-Solomon 编码

通过Reed-Solomon,用户可以通过选择不同的k和m值来灵活调整数据持久性和存储成本。 奇偶校验单元的数量 (m) 决定了可以容忍的同时存储故障的数量。 数据单元与奇偶校验单元的比率决定了存储效率, 即 k / k + m


图 1:具有四个数据单元和两个奇偶校验单元的 Reed-Solomon 编码

图 2:各种存储方案的数据持久性和存储效率对比

从图-2可以看到,我们和最常用的基于副本复制的3副本存储策略对比,RS(6,3)和RS(10,4)均表现出更好的数据持久性和存储效率,但是这是以更高的CPU消耗为代价的。我们在讲Hadoop的具体实现的时候会详细讲解。


图 3:RAID 5和RAID 6的EC校验示意图

EC 长期以来一直用于本地存储系统,特别是以 RAID-5 和 RAID-6 的形式。 RAID-5 通常使用 XOR 编码,因为它只需要容忍单个磁盘故障,而 RAID-6 使用带有两个奇偶校验单元的 Reed-Solomon 来容忍最多两次故障。 单元大小通常是可配置的,每个磁盘上具有相同偏移量的单元形成的纠删码组。

1.2 纠删码的存储布局–连续还是条带?

和我们的磁盘用扇区去划分存储结构一样,为了管理大小不一的各种文件,分布式存储系统通常将文件划分为固定大小的逻辑字节范围(称为逻辑块),然后这些逻辑块被映射到集群上的存储块,这反映了集群上数据的物理布局。

逻辑块和存储块之间最简单的映射是连续块布局,它将每个逻辑块一对一地映射到存储块。 读取具有连续块布局的文件就像按顺序线性读取每个存储块一样简单。因此,对于连续块布局,我们甚至都不需要区分逻辑块和物理块,他们都代表一个文件的一块连续字节范围。

相比之下,条带块布局将逻辑块分解为更小的存储单元(通常称为Cell),并在一组存储块中循环写入单元条带(Stripe)。 读取具有条带布局的文件需要查询逻辑块的存储块集合,然后从该存储块集合读取单元条带。
块布局方案(连续与条带)和块冗余形式(复制与 纠删码)是两个正交维度,形成了四种排列组合形式。尽管在HDFS的使用场景下,连续布局被用在基于复制的块冗余形式,而条带布局用在了纠删码中,但是,他们其实可以随意正交。
些系统(包括 Ceph 和 QFS)支持在每个目录或每个文件的基础上配置布局和/或冗余。
本节讨论如何在两种块布局上支持 EC。
下图显示了HDFS常用了基于复制的冗余方式下,使用连续布局:

图 4:基于复制的冗余方式下,使用连续布局/div>

下图显示了HDFS常用了基于复制的冗余方式下,使用条带布局:

图 5:基于复制的冗余方式下,使用条带布局/div>

下图显示了纠删码冗余形式下的连续布局和条带布局:

图 6:基于复制的冗余方式下,使用条带布局/div>

对于 HDFS-EC,应该选择哪种布局方式? 连续布局更容易实现,因为读写路径仍然与当前具有复制的系统非常相似。 然而,它仅适用于文件非常大的情况,因为只有在写入完整条带时才能实现全部成本节省。 例如,对于 RS (10,4),只有一个 128MB 数据块的条带最终仍会写入四个 128MB 奇偶校验块,存储开销为 400%(比 3 路复制更糟糕)。 连续布局方式下,客户端不适合进行EC校验码的计算,因为这需要将GB级别的校验块load到内存计算校验码。

另一方面,采用条带布局的纠删码可以实现小文件和大文件的存储节省,因为单元(Cell)大小要小得多(通常为 64KB 或 1MB, HDFS默认为1MB)。 这种整体较小的组大小还支持在线 EC,其中客户端直接写入纠删码数据,因为计算奇偶校验信息只需要几兆字节的缓冲。 但是也有一个缺点,一些对本地化很敏感的工作场景会收到影响,因为一个文件的连续部分总是会分散到不同的机器上取,而不是向连续布局一样,一个大的Block只会写到一台机器上。 为了更好地服务此类工作负载,可以将条带文件转换为连续布局,但这几乎需要重写整个文件。

基于以上考虑,HDFS中基于复制的块冗余策略使用了连续布局,基于纠删码的块冗余策略使用了条带布局,而不允许两种冗余策略和两种布局方式可以随意正交。

连续块布局的假设广泛而深入地嵌入到 HDFS 内部逻辑中,为了在这种情况下让HDFS支持基于纠删码的条带布局的块冗余形式,需要定义一个统一的块的概念,即严格讲逻辑块和物理块分开 。 为了支持条带布局,逻辑块的概念必须与存储块的概念分开。 前者表示文件中的逻辑字节范围,后者是DataNode上存储的数据块的基本单位。 图 -7 演示了逻辑块和存储块的概念。 在示例中,文件 /tmp/foo 在逻辑上分为 13 个条带单元(cell_0 到 cell_12)。 逻辑块0代表单元0~8的逻辑字节范围,逻辑块1代表单元9~12。 Cell 0、3、6 形成一个物理存储块,它将作为单个数据块存储在 DataNode 上。 为了简洁起见,该图不包括奇偶校验块/单元:

图 7:逻辑块和物理块映射关系/div>

支持这种泛化的一个简单机制是 HDFS NameNode 监视其块映射中的每个存储块,该块映射从块 ID 映射到相应的块,然后使用另一个映射从逻辑块到其成员存储块。 然而,这意味着小文件将在 NameNode 上产生大量内存开销,因为条带化会产生比复制更多的存储块。

为了减少这种开销,我们引入了一种新的分层块命名协议。 目前HDFS根据块创建时间顺序分配块ID。 相反,该协议将每个块 ID 分为 2~3 个部分,如图 7 所示。每个块 ID 以指示其布局的标志开头(连续 = 0,条带 = 1)。 对于条带块来说,ID的其余部分由两部分组成:中间部分是逻辑块的ID,尾部分表示逻辑块中存储块的索引。 这允许 NameNode 将逻辑块作为其存储块的摘要进行管理。 存储块ID可以通过屏蔽索引映射到其逻辑块; 当NameNode处理DataNode块报告时,这是必需的。

2. 纠删码在Hadoop中的具体实现

在讲纠删码的时候,基于复制的块冗余都使用3副本场景来举例,而纠删码都用RS(6,2)来举例。
在讲解纠删码的具体实现以前,我们先讲解HDFS中怎么维护Block、Replica、DataNode之间复杂的关联关系的。基于对这些信息的理解,我们再去理解纠删码的相关内容。

Hadoop的Block管理的基本逻辑和数据结构

从基本直觉出发,我们可以想到,hadoop的块管理,需要管理以下的信息和提供以下的功能:

  • Block以及Block -> Replica的对应: 整个集群的所有的Block的信息,以及Block中每一个Replica的信息。这个信息在Hadoop中封装成为BlockInfo对象,根据Block是连续布局还是条带布局,其具体实现类分为了BlockInfoContinuous和BlockInfoStriped。
  • Replica的存储位置: 对于Replica,我们显然需要知道replica存放在哪个DataNode了,这个存放的位置在Hadoop中用DatanodeStorageInfo对象表示,即不仅仅精确到DataNode,还精确到了DataNode的某个Storage的层次,因为有可能一个DataNode支持多种不同的存放,disk,ram或者flash等。
  • 对Block信息的快速查找: 拿到一个Block的ID,我们可以立刻找到这个Block的BlockInfo。显然,这里需要一个类似Hash的存储结构,比如Set或者Map。在Hadoop中,这个Hash的结构被封装在BlocksMap对象中。
  • DataNode到Replica的一对多信息:对于一个DatanodeStorageInfo,我们可以知道上面的所有的replica。显然,这里似乎需要一个链表的结构。这里并不需要一个hash的结构,因为如果我们需要知道一个Replica是否存在于某个DataNode上,其实只需要通过BlocksMap来查找到BlockInfo,进而知道其所有replica的DatanodeStorageInfo的信息
  • Replica的快速增删:对于一个replica的增加和删除(都是由某一个DataNode的快汇报产生),我们需要在它对应的BlockInfo中将replica的信息删除,同时,需要在DatanodeStorageInfo中将挂载的这个Replica删除。

BlockInfo的基本结构

BlockInfo封装的是一个Block的基本信息,比如这个Block属于哪个BlockPool,副本数多少(只有连续布局方式有副本数的概念,条带布局没有这个参数),还有下文将要讲到的BlockUnderConstructionFeature,代表这个Block当前正处于构造(正在被写入)的临时状态,以及最重要的,这个Block的所有Replica信息,这个信息存放在一个叫做triplets的数组中。其基本结构如下图所示。
从triplets的名字可以看出来(triplet在英文中是三胞胎/三联体的意思),triplets是一个三元组的集合,这里的每个三元组代表了一个replica的相关信息,对于第i个replica,triplets[3 * i]存放的是这个replica i 的DatanodeStorageInfo信息,triplets[3*i + 1]存放的是这个replica所在的DatanodeStorageInfo的Replica链表中的前一个Block的BlockInfo,triplets[3*i + 2]存放的是这个replica所在的DatanodeStorageInfo的Replica链表中的后一个Block的BlockInfo。
请注意这里的的含义,这里的,不是指这个Replica所在的Block的前一个Replica或者后一个Replica,也不是指整个HDFS集群中存在一个Replica的全局链表的前一个或者后一个Block,而是指这个Replica所在的DatanodeStorageInfo对应的Replica链表的前一个或者后一个replica。我发现很多HDFS的书籍上都并未对这一点说清楚,导致读者不看代码根本不知道其真实含义,只是人云亦云地知道是
所以这个triplets数组中的每一个triplet有三个元素,整个triplets数组其实是一个一维数组,所以整个triplets的大小就是3 * baseSize,这个baseSize对于连续布局来说,就是副本数量,比如系统配置的文件副本数是5,那么某个BlockInfo.triplets[]数组的长度就是3 * 5 = 15,对于纠删码来说,就是一个BlockGroup中的internal block数量, 比如对于RS(6,2),就是8,tripelets[]数组的长度就是3 * 8 = 24
通过这样的一个3 * baseSize的数组,实际上形成了连接BlockInfo的一个双向链表

public abstract class BlockInfo extends Block
    implements LightWeightGSet.LinkedElement {

  private short replication; // 副本数量
  private volatile long bcId; // 这个block所属于的block collection,比如一个INodeFile就是一个BlockCollection
  private LightWeightGSet.LinkedElement nextLinkedElement;
  protected Object[] triplets; // 存放replica信息的三元组

下图是这个triplets数组的示意图。为了方便起见,我们将一维的triplets数组变成一个baseSize * 3 的二维数组:
在这里插入图片描述

DatanodeStorageInfo的基本结构

基本信息介绍

在NameNode端,使用DatanodeDescriptor来表示一个DataNode的相关信息,包括一些静态信息(静态信息存放在它的父类DatanodeInfo中)和一些动态信息比如不断变化的块信息。
DatanodeStorageInfo是NameNode端用来代表DataNode端的一块数据存储路径(比如某一个Volume),显然,这个存储介质具有特定的唯一ID,存储介质类型,容量,当前的使用率等等信息。在DataNode端与之对等的类是DatanodeStorage。显然,DatanodeStorageInfo和DatanodeDescriptor是多对一的关系。类关系图如下:
在这里插入图片描述

----------------------------------------------DatanodeStorageInfo----------------------------------------------
  // 一个DataNodeStorageInfo指的是某一台DN的某个storage
  private final DatanodeDescriptor dn;
  private final String storageID;
  private StorageType storageType;
  private State state;

  private long capacity;
  private long dfsUsed;
  private long nonDfsUsed;
  private volatile long remaining;
  private long blockPoolUsed;
    // 这个虽然叫blockList,但是由于并不是用Java的Collection集合,而是自己维护的数组,因此blockList只是这个数组的header
  private volatile BlockInfo blockList = null;

为了表达一个DatanodeStorage上挂载的Replica的信息,DatanodeStorage使用blockList保存了这个链表的head节点。链表之间的链接关系通过BlockInfo内部的对象引用来实现。我们下文讲BlockInfo的时候会讲到。
总之, DatanodeStorageInfo保存了这个 Storage上的Replica链表的头结点,用以维护这个Datanode上所有的Replica信息,方便遍历。比如,我们需要将一个DataNode进入maintenance状态,这意味着这个DataNode上的所有DatanodeStorageInfo上的所有的replica都需要进行进入maintenance前的处理,比如通过复制保证副本数等。

遍历这个DatanodeStoragreInfo上的所有Replica
  class BlockIterator implements Iterator<BlockInfo> {
    private BlockInfo current;

    BlockIterator(BlockInfo head) {
      this.current = head;
    }

    public boolean hasNext() {
      return current != null;
    }

    public BlockInfo next() {
      BlockInfo res = current;
      current =
          current.getNext(current.findStorageInfo(DatanodeStorageInfo.this));
      return res;
    }

    public void remove() {
      throw new UnsupportedOperationException("Sorry. can't remove.");
    }
  }

我们从next()方法可以看到,寻找当前的BlockInfo的下一个节点是通过调用当前节点的BlockInfo的findStorageInfo来实现的。
如果我们需要遍历一个Datanode上的所有Replica呢?其实只需要遍历这个 DatanodeDescriptor上的所有DatanodeStorageInfo,然后根据DatanodeStorageInfo.BlockIterator来进行遍历就行了。因此DatanodeDescriptor.BlockIterator是对DatanodeStorageInfo.BlockIterator的封装。这里不再赘述。

BlockInfo和DatanodeStorageInfo中块数据结构的动态维护

这里的块信息其实并不准确,应该是Replica的信息的维护。块的整体的增删是发生在客户端写文件或者删除文件导致的块的增加和删除。
这里的Replica信息的变化发生在DataNode 的blockReport时,NameNode会根据BlockReport的信息,对Replica或者Block进行同步的增删等操作。必须再次强调,这里的BlockReport,其实表达并不准确,DataNode汇报的并不是 Block,而是Replica。
NameNode收到新增Replica的 blockReport,会通过BlockManager.addStoredBlock()来将这个Replica添加到其属于的Block的BlockInfo中去。而这个BlockInfo对应的整个Block,是在客户端写数据过程中在NameNode端创建好的,只不过当时这个Block还没有收到任何的DataNode的块汇报。
这是通过调用DatanodeStorageInfo的insertToList方法来实现的。

--------------------------------------------------DatanodeStorageInfo-------------------------------------------------
public AddBlockResult addBlock(BlockInfo b, Block reportedBlock) {
    .......
    // 把BlockInfo b 添加到this(DataNodeStorageInfo中去)
    b.addStorage(this, reportedBlock); // 先更新这个Replica的Storage信息
    insertToList(b);// 设置这个block在this(当前的这个DataNodeStorageInfo)中的信息
    ......
  }

从上面的代码看到,会首先通过b.addStore()将这个Replica添加到其所属的Block对应的BlockInfo中去,代表这个Block增加了一个Replica。
addStorage()的代码如下所示,显然, addStorage的职责仅仅是将这个Block新汇报的 Replica放到BlockInfo的triplets中去,并设置了这个Replica的DatanodeStorageInfo信息,但是,还没有设置任何的前向(pre)或者后向(next)的任何信息:

----------------------------------------------BlockInfo---------------------------------------------------------------
  // reportedBlock是要进行插入的replica
  boolean addStorage(DatanodeStorageInfo storage, Block reportedBlock) {
    // find the last null node
    int lastNode = ensureCapacity(1); // 可插入位置的三元组的第一个位置,这个位置存放这个block的DatanodeStorageInfo信息
    setStorageInfo(lastNode, storage); // 把lastnode设置为这个Storage,代表我设置了当前这个BlockInfoContinuous的存储信息
    setNext(lastNode, null); // 先暂时不设置previous和next的信息
    setPrevious(lastNode, null);
    return true;
  }

在BlockInfo中设置好了replica的位置信息以后,就要维护DatanodeStorageInfo中的BlockInfo的链表信息,这是通过调用DatanodeStorageInfo.insertToList()实现的:

--------------------------------------------------DatanodeStorageInfo-------------------------------------------------
  /**
   * this指的是当前的DatanodeStorageInfo, b是新的replica所对应的BlockInfo, blockList是this.blockList,即
   * 当前这个DatanodeStorageInfo上的BlockInfo链表的头结点
   * @param b
   */
  public void insertToList(BlockInfo b) {
    blockList = b.listInsert(blockList, this); // 把当前节点b插入到DatanodeStorageInfo(this)的头结点blockList所代表的链表的头部
    numBlocks++;
  }

insertToList()会将当前新汇报的这个Replica所属的BlockBlockInfo插入到DatanodeStorageInfo的链表的头部,因此原来的头结点blockList将会更新为当前新插入的节点(参数b)。这是通过调用BlockInfo.listInsert()来实现的:

----------------------------------------------BlockInfo---------------------------------------------------------------
   /*
   * 根据当前的这个this(block)所在的DataNodeBlockInfo中维护的BlockInfoContinuous链表的头节点head,
   * 把this(block)的pre和next插入进去。显然,this(block)会成为旧的head的pre节点,旧的head是this(block)的next
   */
  BlockInfo listInsert(BlockInfo head, DatanodeStorageInfo storage) {
    // 在前面已经通过setStorage设置了这个BlockInfo的对应的Replica的storage了,因此肯定可以找到这个DatanodeStorageInfo
    int dnIndex = this.findStorageInfo(storage);
    this.setPrevious(dnIndex, null);
    this.setNext(dnIndex, head); // next节点就是head, head的pre节点就是自己
    if (head != null) {
      head.setPrevious(head.findStorageInfo(storage), this);// 把head节点的pre设置为当前的BlockInfoContinous节点
    }
    return this;
  }
 // 将BlockInfo to设置为当前的BlockInfo(this)的pre节点
  BlockInfo setPrevious(int index, BlockInfo to) {
    BlockInfo info = (BlockInfo) triplets[index*3+1];
    triplets[index*3+1] = to;
    return info;
  }
  // 将BlockInfo to设置为当前的BlockInfo(this)的next节点
  BlockInfo setNext(int index, BlockInfo to) {
    BlockInfo info = (BlockInfo) triplets[index*3+2];
    triplets[index*3+2] = to;
    return info;
  }

其基本流程如下图所示:
在这里插入图片描述
至于删除一个Replica,道理相同,本质上也是通过每一个DatanodeStorageInfo挂载的BlockInfo以及更新每一个BlockInfo的triplets[]数组来表达的双向链表关系,来维护前向和后缀指针,这里不做赘述。

BlocksMap的基本结构和动态管理

BlocksMap是一个对象,其成员变量blocks是一个GSet,这个GSet中的元素是一个个的BlockInfo, GSet其一个基于链地址法处理hash冲突的一个hash表,用来基于hash的方式查找BlockInfo。

----------------------------------------------------BlocksMap-----------------------------------------------------
class BlocksMap {
  ......
  /** Constant {@link LightWeightGSet} capacity. */
  private final int capacity;
  
  private GSet<Block, BlockInfo> blocks; // 这是一个Set,一个BlockInfo的HashSet,不是一个map
  1. 获取一个Block(Replica)的BlockInfo
    比如,根据一个Block查找一个BlockInfo(这个典型的情况是DataNode汇报上来一个Replica,需要将这个 Replica存放到对应的 Block的BlockInfo中),这里寻址显然需要依赖BlockInfo.hashCode()方法,这里BlockInfo的hashCode()返回值就是blockId。
----------------------------------------------------BlocksMap-----------------------------------------------------
  /** Returns the block object if it exists in the map. */
  BlockInfo getStoredBlock(Block b) {
    return blocks.get(b);
  }
  1. 获取一个Block的Location信息,即这个Block的所有的Replica的DatanodeStorageInfo信息,其实就是根据这个Block查找到对应的BlockInfo,然后在这个BlockInfo的triplets[]中逐个遍历replica的DatanodeStorageInfo信息
----------------------------------------------------BlocksMap-----------------------------------------------------
  Iterable<DatanodeStorageInfo> getStorages(final BlockInfo storedBlock) {
    return new Iterable<DatanodeStorageInfo>() {
      @Override
      public Iterator<DatanodeStorageInfo> iterator() {
        return new StorageIterator(storedBlock);
      }
    };
  }
  
  public static class StorageIterator implements Iterator<DatanodeStorageInfo> {
    private final BlockInfo blockInfo;
    private int nextIdx = 0;

    StorageIterator(BlockInfo blkInfo) {
      this.blockInfo = blkInfo;
    }

    @Override
    public boolean hasNext() {
      if (blockInfo == null) {
        return false;
      }
      while (nextIdx < blockInfo.getCapacity() &&
          blockInfo.getDatanode(nextIdx) == null) {
        // note that for striped blocks there may be null in the triplets
        nextIdx++;
      }
      return nextIdx < blockInfo.getCapacity();
    }
  1. 添加或者删除Block

这里的添加block显然不是添加Replica,而是添加一个Block的元数据信息,它不是datanode的块汇报导致的,而是客户端在写文件的过程中添加一个block或者客户端删除一个文件导致block被删除导致的。
添加Block是通过addBlockCollection()来进行的,因为一个Block肯定是属于一个BlockCollection的:

----------------------------------------------------BlocksMap-----------------------------------------------------
  BlockInfo addBlockCollection(BlockInfo b, BlockCollection bc) {
    BlockInfo info = blocks.get(b);
    if (info != b) { // 如果找不到(info==null)或者不一致
      info = b;
      blocks.put(info);
      incrementBlockStat(info);
    }
    info.setBlockCollectionId(bc.getId());
    return info;
  }

删除Block是通过removeBlock()来进行的,除了要将BlocksMap中Block的信息删除,还要检查这个Block的所有Replica对应的 DatanodeStorageInfo,将这些replica从对应的DatanodeStorageInfo的块链表中清除:

----------------------------------------------------BlocksMap-----------------------------------------------------
 void removeBlock(BlockInfo block) {
    BlockInfo blockInfo = blocks.remove(block); // 从BlocksMap中删除
    final int size = blockInfo.isStriped() ?
        blockInfo.getCapacity() : blockInfo.numNodes();
    for(int idx = size - 1; idx >= 0; idx--) { //将这个block的每一个replica从其DatanodeStorageInfo中清除
      DatanodeDescriptor dn = blockInfo.getDatanode(idx);
      if (dn != null) {
        removeBlock(dn, blockInfo); // remove from the list and wipe the location
      }
    }
  }

2.1 Hadoop对纠删码在用户层的支持

在Hadoop中,纠删码的Policy定义为ErasureCodingPolicy类:

public final class ErasureCodingPolicy implements Serializable {
  private final String name; // policy的名字
  private final ECSchema schema; // 纠删码的模式定义,包含了纠删码的名字,
  private final int cellSize; // cell的大小
  private final byte id;
public final class ECSchema implements Serializable 

  /**
   * The erasure codec name associated.
   */
  private final String codecName; // schema的名字,比如基于RS的schema的名字是rs,基于xor的schema的名字是xor

  /**
   * Number of source data units coded
   */
  private final int numDataUnits; // 数据部分的单元数量,比如RS(6,2), numDataUnits=6, numParityUnits=2

  /**
   * Number of parity units generated in a coding
   */
  private final int numParityUnits; // 校验部分的数量

HDFS在启动的时候,会默认加载多个已经默认支持的ECPolicy,这些Policy定义在SystemErasureCodingPolicies中:
RS-3-2-1024k, RS-6-3-1024k, RS-10-4-1024k, RS-LEGACY-6-3-1024k, XOR-2-1-1024k
从Policy的名字就能看出其含义:{编码方式,比如基于RS还是XOR}-{data block的数量}-{parity block的数量}-{一个Cell的大小}

。用户通过官方文档https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HDFSErasureCoding.html去进行EC相关的操作,比如

  • 设定一个目录基于纠删码进行副本管理,而不是基于复制进行,那么这个目录下面新创建的文件都会基于父目录的ECPolicy进行存储,但是当一个目录下面已经存放了基于复制进行副本管理文件,这时候我们设定这些文件的父目录,不会改变已有文件的布局方式,因为这将引起大量的块的拷贝等操作。
  • 修改文件的布局方式,或者将一个基于纠删码的文件拷贝到另外一个基于复制副本的目录下,都将引发文件背后的block的大量拷贝
  • rename(move)一个文件(比如从一个基于纠删码的目录移动到一个基于多副本复制的目录下),不会修改文件的块布局方式,否则一个简单的move操作会带来block的大量拷贝

显然,由于纠删码本身的特殊块布局方式,导致有些情况无法再像基于复制的方式一样支持:

  • 对纠删码文件执行append() 和truncate() 将抛出IOException。
  • 如果文件与不同的纠删码策略或复制的文件混合在一起,concat() 将抛出 IOException。
  • setReplication() 对纠删码文件不执行任何操作。
  • DFSStripedOutputStream 上的 hflush() 和 hsync() 是无操作的。 因此,在纠删码文件上调用 hflush() 或 hsync() 不能保证数据持久。客户端可以使用StreamCapability API查询OutputStream是否支持hflush()和hsync()。 如果客户端希望通过 hflush() 和 hsync() 实现数据持久化,当前的补救措施是在非纠删码目录中创建常规 3x 复制文件,或者使用 FSDataOutputStreamBuilder#replicate() API 在以下位置创建 3x 复制文件: 纠删码目录。

2.2 条带布局和连续布局的BlockID的分配和管理

早期没有条带布局方式时,Logical Block和Internal Block是一一对应,NameNode端的BlockID很好分配,自增即可。但是有了条带布局,Logical Block和Internal Block不再一一对应,因此,为了对这两种情况进行统一管理,HDFS统一了BlockID的分配方式, 一种新的分层块命名协议。 目前HDFS根据块创建时间顺序分配块ID。 相反,该协议将每个块 ID 分为 2~3 个部分,如图 7 所示。每个块 ID 以指示其布局的标志开头(连续 = 0,条带 = 1)。 对于条带块来说,ID的其余部分由两部分组成:中间部分是逻辑块的ID,尾部分表示逻辑块中存储块的索引。 这允许 NameNode 将逻辑块作为其存储块的摘要进行管理。 存储块ID可以通过屏蔽索引映射到其逻辑块; 当NameNode处理DataNode块报告时,这是必需的。
在HDFS中,BlockIdManager负责生成对应的BlockID,它委托SequentialBlockIdGenerator来生成基于复制的连续块布局的BlockID,而SequentialBlockGroupIdGenerator来生成基于条带的连续块布局的BlockID(实际上这是一个BlockGroupID,即按照上面将的分层的块命名协议,低四位为0,即还没有将BlockGroup切分为Internal Block)。下文会讲,NameNode会将BlockGroup进行切分,并把每一个切分成的InternalBlock分配到对应的DataNode上。

------------------------------------- BlockIdManager -------------------------------------
  long nextBlockId(BlockType blockType) {
    switch(blockType) {
    case CONTIGUOUS: return blockIdGenerator.nextValue(); //使用SequentialBlockIdGenerator为连续块布局方式分配Block ID
    case STRIPED: return blockGroupIdGenerator.nextValue(); // 使用SequentialBlockGroupIdGenerator为条带布局方式分配Block ID
    }

在这里插入图片描述

2.3 基于条带布局的纠删码文件的写入过程

纠删码的写入过程与普通的连续布局的写入过程的基本区别是:

  1. 物理写入逐条带进行:由于我们重新区分了逻辑块和物理块,逻辑块代表了文件本身的字节顺序,条带布局情况下,逻辑块内部的物理块顺序不再对应文件的实际字节顺序,代表文件的字节顺序的变成了条带,因此,在条带布局的时候,写入会逐条带进行,而不是逐物理块进行。
  2. 写入并发:由于写入按照条带进行,并且一个条带跨了多个物理块,因此在一个条带的每个Internal Block的每个cell的写入可以异步并行(Ansynchronized)进行,但是条带之间则是串行同步(Synchronized)进行。
  3. 需要计算纠删码:计算纠删码(Parity Block)的过程叫做encode的过程。没写完一个条带(Stipe)的data block部分,就要开始计算纠删码然后写parity block,然后开始写下一个Stipe。纠删码的计算在客户端进行,因此会给客户端引入一定的CPU负载。
    在这里插入图片描述

2.3.1 客户端的流程

基于条带的块布局方式改变了基本的数据存储结构,因此,对于条带布局和连续布局文件的读写方式发生了很大变化,但是基本的写流程没有发生变化:

  1. 客户端通过调用DFSClient.create()创建对应文件

    • 服务器端收到create请求以后,会构建对应文件的INodeFile并维护在内存和Editlog中,INode信息中包含了文件的元数据信息,比如,读写权限,文件名,副本的块冗余形式(纠删码还是复制?)等等信息。注意,这时候还没有创建任何block信息
  2. 客户端收到服务器端返回的包含文件元数据信息的HdfsFileStatus对象,基于该对象,构建DFSOutputStream对象的实现

  3. 客户端开始进行writeChunk操作,并根据需要,调用addBlock()接口开始申请block

    • 服务器端根据block申请,创建对应的的BlockID, 副本的块冗余形式(纠删码还是复制?),调用不同的块放置策略,确定块放置的目标机器,将这些所有信息封装在LocatedBlock对象中,返回给客户端
  4. 创建DataStreamer对象,负责Block的数据写入。在三副本情况下,DataStreamer只会和第一个节点建立Socket连接,然后第一个节点会把收到的packet转发给剩余的两个节点。

    • DataStreamer是一个Daemon,用来进行异步的数据发送。请注意,数据发送的基本单位是Packet。在HDFS中,一个Packet 的默认大小是64KB。checksum的基本单位是chunk,长度为512B,HDFS会对每个chunk计算一个checksum值,长度为4B。一个Packet的总容量是64KB,按照64KB/516B = 127,貌似大概一个packet可以有127个 chunk, 但是由于每一个packet都由header 信息,比如packet的长度信息,以及和protobuf相关的一些信息,因此,一个packet可以承载126个chunk。按照数据和checksum的比例,默认一个128MB的Block会由大概1MB的checksum数据。
     protected void computePacketChunkSize(int psize, int csize) {
        final int bodySize = psize - PacketHeader.PKT_MAX_HEADER_LEN;
        final int chunkSize = csize + getChecksumSize();
        chunksPerPacket = Math.max(bodySize/chunkSize, 1);
        packetSize = chunkSize*chunksPerPacket;
    
    • 客户端在通过DFSOutputStream写数据过程中,写满64KB,数据就会被封装为DFSPacket对象,并将该对象交付给DataStreamer的dataQueue。dataQueue中存放了所有需要发送给pipeline的待发送数据。chunk, checksum和最终组装成Packet入下图所示。可以看到写数据的过程就是将数据按照chunkj进行拆分、到以chunk为单位计算checksum、到组装成packet、到将数据通过socket发送给DataNode的过程
      在这里插入图片描述
  5. DataStream所代表的异步线程负责从自己的dataQueue中poll数据,并发送给pipeline,并将这个数据包从dataQueue中移除,放到ackQueue中,代表这个packet正等待ack

    • 根据HDFS写入数据的 pipeline规则,比如在3副本的情况下,client端只需要把数据发送给第一个target节点,而数据的复制则是由第一个target节点自己像pipeline一样复制给剩下的几台机器。
  6. 响应处理器ResponseProcessor从数据节点接收确认。 当从所有数据节点接收到数据包的成功确认时,响应处理器将从确认队列中删除相应的数据包。

    • 注意,DataStreamer中负责获取ACK的Input Socket和负责发送Packet的Output Socket都只会和第一个DN沟通,在3副本情况下,量外两个副本的ack是返回给第一个节点,然后第一个节点把ack发送给客户端,从而客户端确认所有副本写成功
图-8 HDFS写文件基本框架

由于块布局方式的变化,基于纠删码的条带布局为了尽量减少对已有的基于复制的块布局代码的侵入,采用的继承的方式,比如,StripedDataStreamer继承了DataStreamer, 而DFSStripedOutputStream继承了DFSOutputStream,LocatedStripedBlock继承了LocatedBlock。
在具体写方式上,条带布局方式沿用了连续布局方式的异步写的整体架构,比如:

  1. 文件写还是基于chunk进行checksum校验,当chunk已经写满了Packet的时候,以Packet为单位进行异步发送
  2. 发送依然异步进行,即写满Packet以后,会将Packet 交付给StripedDataStreamer的dataQueue, 发送完成,数据从dataQueue移动到ackQueue,确认完毕,从ackQueue移出。
  3. 文件写的时候还是由DFSOutputStream来进行(stripe模式下DFSStripedOutputStream进行了重载),与pipeline中的DataNode建立通信和发送packet还是由DataStreamer来进行(StripedDataStreamer)。
    DataNode的数据接收逻辑在DataXceiver中,这里不做详细介绍。

但是由于条带布局的逻辑块和物理块不再一一对应,写的逻辑也发生了根本变化。在概念上变化的基本基本原则是,HDFS基于原有的连续布局方式的block到了stripe布局中对应block group,而block group中的具体物理块,在hadoop中叫做internal block。

写方式不同的地方主要包括:

  1. 多Streamer的并行写,一个DFSOutputStream只有一个DataStreamer来和一个DataNode通信,因为是串行写。但是在条带写的情况下,一个DFSOutputStream会创建多个StripedDataStreamer,每个DataStreamer i 负责写Block Group中index为i的一个internal block(可能是数据块,可能是校验块)。显然,比如RS(6,2),会创建8个StripedDataStreamer对象,并行进行数据写。写完一个条带,再写下一个条带。写完一个Block Group,会申请新的Block,然后继续一个条带一个条带的写。但是,写下一个Block Group的时候,不会重新创建StripedDataStreamer,每个StripedDataStreamer在新的Block Group中还是负责index=i的internal block的写。
  2. 由于条带布局以Cell为单位,因此在数据写入过程中,每写完一个Cell,就要切换负责下一个Cell的StripedDataStreamer。可见,在写过程中,StripedDataStreamer是依次轮流被切换。
  3. 副本放置策略:客户端通过addBlock()接口申请一个块,NameNode会根据当前集群的状态申请相应数量的机器。连续布局方式(三副本为例)下,默认使用BlockPlacementPolicyDefault会将第一个replica放在一个rack的某台机器上,另外两个replica会放在另外一个rack的两台不同机器上。这种策略显然不适用条带布局,因为条带布局情况下不希望任何情况下损失多于一个replica,因此使用BlockPlacementPolicyRackFaultTolerant策略,这个策略的基本原则是,将条带布局下的所有replica分配到不同的基架。
    在这里插入图片描述
    下面的DFSStripedOutputStream构造方法显示了在构造DFSStripedOutputStream,会根据ECPolicy所需要的DataStreamer数量(dataBlock + parityBlock),创建对应的StripedDataStreamer,负责和DataNode通信。根据internal block的数量,为每一个internal block构建一个StripedDataStreamer,负责BlockGroup中的一个索引位置的Internal Block的写操作,并且从一个Group到下一个Group以后,之前创建的StripedDataStreamer依然会负责新的BlockGroup中对应的index的internal block的写入操作。
    同时,由于条带布局的写方式需要频繁地切换internal block和负责这个internal block写操作的StripedDataStreamer,因此,特意创建了一个叫做Coordinator的类,负责internal block和对应的StripedDataStreamer的协调和切换。他时刻维护着internal block和StripedDataStreamer的对应关系:
-------------------------------------DFSStripedOutputStream.java ------------------------------------------
/** Construct a new output stream for creating a file. */
  DFSStripedOutputStream(DFSClient dfsClient, String src, HdfsFileStatus stat,
                         EnumSet<CreateFlag> flag, Progressable progress,
                         DataChecksum checksum, String[] favoredNodes)
                         throws IOException {
    super(dfsClient, src, stat, flag, progress, checksum, favoredNodes, false);
    ....
    ecPolicy = stat.getErasureCodingPolicy();
    final int numParityBlocks = ecPolicy.getNumParityUnits();
    cellSize = ecPolicy.getCellSize();
    numDataBlocks = ecPolicy.getNumDataUnits();
    .....

    coordinator = new Coordinator(numAllBlocks);
    cellBuffers = new CellBuffers(numParityBlocks);// 虽然初始化是用的numParityBlocks, 其实内部放的是datablock 和 parity block

    streamers = new ArrayList<>(numAllBlocks);// 并行写一个logical block中的所有internal block,但是串型写不同的logical block
    for (short i = 0; i < numAllBlocks; i++) { // 每一个physical block会有一个DataStreamer与之对应,即StrippedDataStreamer对象,用来与对应的DN进行流数据写操作
      StripedDataStreamer streamer = new StripedDataStreamer(stat,
          dfsClient, src, progress, checksum, cachingStrategy, byteArrayManager,
          favoredNodes, i, coordinator, getAddBlockFlags());
      streamers.add(streamer);
    }
    currentPackets = new DFSPacket[streamers.size()];
    datanodeRestartTimeout = dfsClient.getConf().getDatanodeRestartTimeout();
    setCurrentStreamer(0); // 肯定从第一个streamer开始写
  }
    static class Coordinator {
    /**
     * followingBlocks是一个List<BlockingQueue<T>> queues,每一个BlockingQueue代表index=i的多个LocatedBlock
     */
    private final MultipleBlockingQueue<LocatedBlock> followingBlocks; // 指的是block group的下一个internal block, 每一个index对应了一系列block的list 

下面的代码显示了在写文件过程中,是以chunk为单位计算checksum和写数据到Packet。

  • 在写数据过程中,如果是第一个BlockGroup,或者旧的BlockGroup写完了(判断依据是当前已经写的数据量等于internal block的数据总量),就向NameNode申请新的Block。我门可以理解成通过addBlock()接口申请的Block叫做logical block(即一个Block Group), 申请到的LogicalBlock的对象是LocatedStripedBlock,其中包含了BlockGroup ID,分配到的节点,以及BlockGroup中每一个Internal Block与节点之间的对应关系。拿到LocatedStripedBlock响应以后,客户端会将其作为一个Block Group,切分成data block 和 parity block。
  • 由于Striped本身的特性,如果Cell写满了,就需要切换到下一个StripedDataStreamer。由于Cell默认为1MB,所以可以想见StripedDataStreamer的切换是随着数据写入不断进行的。这就是上面提到的Coordinator的作用。
  • 如果当前写完的Cell是当前Stripe的data block的最后一个Cell,那么意味着下一个Cell就不是写数据了,而是计算parity block然后写parity
    block到指定的机器上。
-------------------------------------DFSStripedOutputStream.java ------------------------------------------
/**
   * chunk是计算checksum的单位,以chunk为单位计算checksum,以packet为单位发送数据
   * 将bytes中[offset, offset+len]的数据作为一个chunk写入packet(从调用者的代码可以看到,这个数据只可能小于等于一个chunk的长度)
   * 通过currentPacket和currentStream来确定是由哪个streamer来负责写这个数据
   *
   * @throws IOException
   */
  @Override
  protected synchronized void writeChunk(byte[] bytes, int offset, int len,
      byte[] checksum, int ckoff, int cklen) throws IOException {
    final int index = getCurrentIndex();
    final int pos = cellBuffers.addTo(index, bytes, offset, len); // 往data block中写入数据,返回更新以后的position
    final boolean cellFull = pos == cellSize; // 根据不同的Stripe Policy确定的,默认是1MB

    //  如果是第一个block,或者当前的block group写完了,需要写到一个新的block group,那么就创建一个新的
    if (currentBlockGroup == null || shouldEndBlockGroup()) {
      // the incoming data should belong to a new block. Allocate a new block.
      allocateNewBlock(); // 对于一个刚刚创建的currentBlockGroup, numBytes=0
    }

    currentBlockGroup.setNumBytes(currentBlockGroup.getNumBytes() + len);// 在写入过程中,numbers逐渐增加
    // note: the current streamer can be refreshed after allocating a new block
    final StripedDataStreamer current = getCurrentStreamer(); // 获取当前的streamer
     // 将当前的chunk写入到Packet中(不代表packet需要enqueue了)
     super.writeChunk(bytes, offset, len, checksum, ckoff, cklen);
     .....

    if (cellFull) { // cell写满了,需要将stream切到下一个,同时,需要写parity block了
      int next = index + 1;
      // 如果不是最后一个data block, 是不需要写parity chunk的
      if (next == numDataBlocks) { // 刚刚写的是这个logic group中的最后一个data block的chunk,意味着下一个chunk是写parity block的chunk
        cellBuffers.flipDataBuffers();
        writeParityCells(); // 在写parity的过程中,stream也是在往后切换的
        ....
      setCurrentStreamer(next);// 将当前stream切换到下一个,因为当前cell写满了。不写满不切换stream
    }
  }
  
  private boolean shouldEndBlockGroup() {
    return currentBlockGroup != null && 
        currentBlockGroup.getNumBytes() == blockSize * numDataBlocks; 
  }

下面的代码可以看到,拿到LocatedStripedBlock以后,客户端会根据LocatedStripedBlock中携带的信息,将这个返回的Group切分成一个一个的Internal Block,每个Internal Block也用LocatedStripedBlock表示,同时,创建对应的DataStreamer,用来和这个Internal Block所分配的机器进行socket通信已实现Block的写操作。

-------------------------------------DFSStripedOutputStream.java ------------------------------------------
private void allocateNewBlock() throws IOException {
    if (currentBlockGroup != null) {
      for (int i = 0; i < numAllBlocks; i++) {
        // sync all the healthy streamers before writing to the new block
        waitEndBlocks(i);
      }
    
    DatanodeInfo[] excludedNodes = getExcludedNodes();
    //......
    final LocatedBlock lb;
    try {
      // 向NameNode申请Block.对于Stripped的场景,是申请一个BlockGroup(Logic block)
      // 这个addBlock是调用的super class的addBlock(), 即DFSStrippedOutputStream和DFSOutputStream都是一样的addBlock()逻辑
      // 从代码来看,这里并没有blocksize的信息
      lb = addBlock(excludedNodes, dfsClient, src,
          prevBlockGroup, fileId, favoredNodes, getAddBlockFlags());
    // assign the new block to the current block group
    currentBlockGroup = lb.getBlock();
    blockGroupIndex++;
    // 从申请到的LocatedBlock(对应了一个Block Group)切分出Internal block
    // 这里的blocks的索引已经是跟internal block在group中的索引一致了,每一个internal block的大小也从Block group中拆分了
    final LocatedBlock[] blocks = StripedBlockUtil.parseStripedBlockGroup(
        (LocatedStripedBlock) lb, cellSize, numDataBlocks,
        numAllBlocks - numDataBlocks);
    for (int i = 0; i < blocks.length; i++) {
        // ......
        // 把这个Block group里面的所有的internal block分配给每一个streamer
        coordinator.getFollowingBlocks().offer(i, blocks[i]);
    }

2.3.2 服务端的流程

在这里插入图片描述

文件的创建

服务端收到create()请求以后,会做以下事情:

  • 权限校验和设置,是否有权限创建文件,是否需要循环创建parent目录
  • 路径校验:路径校验,创建对应parent目录,overwrite模式下需要删除同名文件,非overwrite模式下遇到同名文件会抛出异常,
  • 参数合法性校验, 各种参数是否合法,replica和ec不可同时存在,如果是EC,Blocksize不可以比CellSize还要小
  • 如果是基于replication,将确认replication factor,如果是EC, 将设置对应的ECPolicy。 基本原则是,用户如果在创建请求中制定了 ECPolicy,那么就按照用户请求的ECPolicy存储文件,如果用户请求中使用复制方式存储文件,那就按照复制方式,否则,新创建的文件将继承父目录的副本方式。
  • 创建该文件对应的INodeFile,并挂载到系统的INodeDirectory下面(一个HDFS服务的一个Nameservice只有一个INodeDirectory对象,代表了整个文件系统的文件目录树的根入口),这个INodeFile包含了该文件的所有元数据信息,权限,创建和修改时间戳信息,blockSize,块布局方式blockType,ECPolicy, 副本个数,文件的StoragePolicy
    • StoragePolicy管理的是文件应该存放在哪种存储上,SSD, DISK 等,以及在副本复制replication或者文件创建过程中如果要求的StoragePolicy不存在该怎么选择(SSD不够用了,是否允许用DISK?),这些默认支持的Policy在BlockStoragePolicySuite.createDefaultSuite()中初始化
  • 将文件的租约(lease)交给这个创建者
  • 将文件元数据信息持久化到EditLog
  • 将文件信息封装为HdfsFileStatus返回给客户端
Block的添加

文件创建完成以后,就可以开始进行写操作了,写操作在任何时刻一定是处在某一个Logical block中,因此,刚开始写,或者写完一个 block,都需要向NameNode申请Block。有了条带存储以后,这个addBlock()的block归一化为Logical Block, 即申请一个逻辑块。但是, NameNode在处理申请的时候,创建了一个逻辑块,还会根据分配的机器将这个逻辑块切分成物理块,然后返回给客户端。
连接文件的写过程,必须了解文件在写入过程中的各种状态,NameNode将块写入过程中的信息封装在BlockUnderConstructionFeature对象中。
在NameNode端,每一个块都有一个BlockInfo对象,每一个BlockInfo对象都有一个BlockUnderConstructionFeature uc,BlockUnderConstructionFeature中维护了跟块写入过程相关的信息,最重要的是当前正在写入的块的状态(BlockUCState)。一个块正在被写入的时候,uc是不为null的,当这个块写入完成(NameNode收到了这个块的足够数量的DataNode 的汇报),进入COMPLETE状态,uc会被置为空,代表这个块处于UnderConstruction(UC)的状态。块写入过程中的状态被表示称为BlockUCState:

  enum BlockUCState {
	  UNDER_CONSTRUCTION, //这个block应该正处于被写的状态
	  COMMITTED //  客户端已经写完了,但是还没有DataNode收到确认的汇报,这个状态较COMPLETE要更早
	  COMPLETE, // block的构建完成,block的FINALIZED的副本数量已经达到了最小副本数量,这是block的最后的稳定状态
	  UNDER_RECOVERY,// block写入失败,需要进行recovery操作
  }
  • UNDER_CONSTRUCTION : 这个块处于正常地被写入状态

  • COMMITTED: 这个块已经被写入到Data,但是目前NameNode还没有收到任何一个或者还没有收到足够的DataNode的汇报。

  • COMPLETE: NameNode已经收到了关于这个Block的汇报,汇报数量已经达到的要求的最小块数量,并且这个块不会再被修改。

    • 连续布局:对于连续布局,只要一个块已经存放到dfs.namenode.replication.min所配置的数量的DataNode上(即NameNode已经收到了这么多个DataNode汇报了这个replica),就认为是COMPLETE。默认情况下,dfs.namenode.replication.min为1,即只要这个replica对应的target有一个汇报上来,NameNode端的Block的状态就是COMPLETE。
    • 条带布局:只要NameNode收到了达到numDataBlock个块的DataNode的汇报,就会将这个EC Logical Block的状态置为COMPLETE。对于RS(6,2),只要有6个节点(无论是data block还是parity block)的DataNode汇报给了NameNode,这个logical block 就COMPLETE了。

    从上面的描述看到,COMPLETE状态表达的是一个Block已经到了immutatable的状态了,但是很显然,在COMPLETE状态的副本只要丢失任何一个replica,就会造成数据丢失。

  • UNDER_RECOVERY: 块写入过程中失败,因此正处在恢复之中。
    下文将会讲到的块的reconstruction(重构)都是指的处于COMPLETE状态的replica由于出现一些特殊情况道导致块的副本数量无法满足要求因此进行重构。而Recover(恢复)是指处于UNDER_CONSTRUCTION状态下的块由于异常情况(客户端中断,DataNode崩溃)导致副本出现不一致状态而进行的修复。
    服务器端收到了addBlock()请求以后,会做以下事情:

  1. 相关校验,通过validateAddBlock()方法对addBlock()操作进行校验,把校验结果封装在ValidateAddBlockResult中,校验的内容包括:

    • 租约是否匹配(只有拿到文件租约的客户端才能写文件)

      static ValidateAddBlockResult validateAddBlock(){
      					 ........
      					FileState fileState = analyzeFileState(fsn, iip, fileId, clientName,
                                             previous, onRetryBlock);
                          ........
      }
      
      private static FileState analyzeFileState(
      	      FSNamesystem fsn, INodesInPath iip, long fileId, String clientName,
      	      ExtendedBlock previous, LocatedBlock[] onRetryBlock)
      	      ...........
      	      final INodeFile file = fsn.checkLease(iip, clientName, fileId);
      	      .......
      	      
      
    • 上一个block是否已经彻底完成复制。只要新添加的block不是文件的第一个block,那么在基于副本复制的块布局方式下,必须确认上一个block已经完成了副本复制。

      final INodeFile pendingFile = fileState.inode; // 校验文件块的状态,比如倒数第二个文件块是否已经complete或者committed了
      if (!fsn.checkFileProgress(src, pendingFile, false)) {
        throw new NotReplicatedYetException("Not replicated yet: " + src);
      }
      

    无论是Continuous Block还是 Striped Block,在addBlock()或者close()的时候,都会通过checkFileProgress()方法对该文件的block进行校验,校验的规则是:

    • 客户端通过配置dfs.namenode.file.close.num-committed-allowed代表文件在关闭或者添加新的block的时候,最多允许多少个block仅仅处于COMMITTED的状态而还未到达COMPLETE的状态

      • 如果客户端尝试关闭文件DFSOutputStream.close(),那么要求:
        • 倒数第二个block必须是COMPLETE的状态
        • 当确认倒数第二个Block已经COMPLETE了,NameNode经过校验将文件状态改为COMMITTED,如果已经有一定数量的副本汇报,则把状态改为COMPLETED
        • 保证在blocks.size() - num-committed-allowed之前的所有block都已经是COMPLETE状态,剩余的num-committed-allowed个Block都是COMMIT的状态
      • 如果客户端在写文件,客户端申请添加一个Block,在创建并添加这个新的Block以前,必须确保倒数第二个Block必须是COMPLETE的状态,对倒数第一个Block的状态没有要求。
    • block数量不可以超过单个文件最大的副本数量

      if (pendingFile.getBlocks().length >= fsn.maxBlocksPerFile) {
        throw new IOException("File has reached the limit on maximum number of"
            + " blocks (" + DFSConfigKeys.DFS_NAMENODE_MAX_BLOCKS_PER_FILE_KEY
            + "): " + pendingFile.getBlocks().length + " >= "
            + fsn.maxBlocksPerFile);
      }
      
    • 将校验结果,包括文件所需要的副本数量(对于连续布局,target数量就是该文件的副本数,对于条带布局,target数量就是ECSchema中定义的dataUnit + parityUnit的数量,以及blockSize)和blocksize封装到ValidateAddBlockResult中,供后续创建block使用

      return new ValidateAddBlockResult(blockSize, numTargets, storagePolicyID,
                                            clientMachine, blockType, ecPolicy);
      
  2. 为这个addBlock()请求分配对应的DataNode。 在addBlock()的时候,通过制定的excludedNode,以及favoredNode,和上一步确定的所需要的机器数量,交给对应的PlacementPolicy,交由BlockManager.chooseTarget4NewBlock()进行机器的分配。

    • BlockPlacementPolicies类负责对PlacementPolicy进行管理,在NameNode启动的时候,就会根据配置文件,决定不同的布局方式所应该使用的PlacementPolicy。通过dfs.block.replicator.classname配置基于复制的连续块布局的副本放置策略,默认是BlockPlacementPolicyDefault,典型的3场景,会将第一个replica放在一个rack,剩下两个replica放在另外一个rack的不同host。通过dfs.block.placement.ec.classname配置纠删码的PlacementPolicy,不配置的默认策略是BlockPlacementPolicyRackFaultTolerant,一种尽量将所有块放置到不同Rack上的策略。

      -------------------------------------BlockPlacementPolicies-------------------------------------
        public class BlockPlacementPolicies{
        	public BlockPlacementPolicy getPolicy(BlockType blockType){
      	    switch (blockType) {
      	    case CONTIGUOUS: return replicationPolicy; // 基于复制的布局方式的PlacementPolicy
      	    case STRIPED: return ecPolicy; // 基于条带的布局方式的PlacementPolicy
      	    ......
          }
      
  3. 为这个addBlock()请求分配对应的BlockID。如果是StripedBlock,这个ID仅仅是BlockGroup ID(低四位为0,还没有分配具体的Internal Block)

  4. 分配Internal BlockID,创建Block和文件的对应关系并创建replica和targets之间的对应关系

    • 对于StripedBlock, 这里的BlockID其实是BlockGroup ID,上文已经讲过BlockID的具体格式和分配方式。

    • 将创建的Block挂在到这个文件的INodeFile上,构建对应关系。显然,假如是一个大文件,那么这个INodeFile对应的Block数量不止一个,因此INodeFile通过一个 BlockInfo[] blocks;来维护自己的所有的Block

    • 构建Replica和Datanode的对应关系。这是因为刚刚只是委托PlacementPolicy分配了指定数量的DataNode,基于复制的连续布局很简单,所有的DataNode存储相同的replica,但是在条带布局下,这个申请的Block其实是一个BlockGroup,那么这个Group中的每一个Internal Block 该放到哪个DataNode上去呢?这就是这里需要确定的。

      BlockManager.newLocatedBlock()就是将对应的replica(Internal Block)具体分配到对应的DataNode上面去,并返回一个LocatedStripedBlock对象。LocatedStripedBlock是LocatedBlock针对StripedBlock 的具体实现,最重要的是它有了一个叫做blockIndices的成员,blockIndices[i]的含义是第i个DataNode存放的InternalBlock的block index,而DatanodeInfoWithStorage[] locs中locs[i]则存放了对应位置i的DataNode信息,联合blockIndices和locs,就知道了任何一个Internal Block的存储位置。

      public class LocatedStripedBlock extends LocatedBlock {
        // blockIndices[i]代表了replicas[i]对应的replica在block group中的索引值,
        // 查看BlockUnderConstructionFeature.setExpectedLocations L105
        private final byte[] blockIndices; // physical block在block group中的索引
      
图-8 HDFS写文件基本框架

NameNode 构建完成LocatedStripedBlock信息,就完成了整个StripedBlock的创建过程,包括Block Group ID(上面讲了block id的分层方式,Block Group ID的低四位是0),分配的节点,以及这个BlockGroup 中的每一个InternalBlock 和节点之间的对应关系。在客户端拿到NameNode返回的LocatedStripedBlock以后,就通过方法constructInternalBlock中按照要求将LocatedStripedBlock拆分成一个一个的Internal block(也是一个LocatedBlock对象,由于是Internal Block 那么di四位就是internal block在group中的index),然后将这些internal block绑定给对应的DataStreamer,然后开始数据写操作。

  public static LocatedBlock constructInternalBlock(LocatedStripedBlock bg,
      int idxInReturnedLocs, int cellSize, int dataBlkNum,
      int idxInBlockGroup) {
    final ExtendedBlock blk = constructInternalBlock(
        bg.getBlock(), cellSize, dataBlkNum, idxInBlockGroup);
    .....
      locatedBlock = new LocatedBlock(blk, // 每一个internal都从block group中切分属于自己的location, storage id, storage type
          new DatanodeInfo[]{bg.getLocations()[idxInReturnedLocs]},// 这个internal block对应的DataNode info
          new String[]{bg.getStorageIDs()[idxInReturnedLocs]}, // 这个internal block对应的storage id
          new StorageType[]{bg.getStorageTypes()[idxInReturnedLocs]}, // 这个internal block对应的storage type
          bg.getStartOffset(), bg.isCorrupt(), null);
     return locatedBlock;

2.4 基于条带布局的纠删码文件的读取过程

3. 一些实验

验证在不同文件大小的情况下的基于复制和基于纠删码的块处理

需要验证的问题:

基于RS-3-2-1024k,一个cell是1024KB, 一个Stripe中有3个Data Cell和2个Parity Cell。假如一个Logical Block(Block Group)的大小是128MB, 那么一个Block Group写满数据,大概会有128MB / (3 * 1024KB) = 42个Stripe, 每个Internal Block大概42M左右。那么问题来了,假如数据写不满一个Block Group,比如一个文件本身就很小,或者一个大文件的最后一个 Block Group,NameNode怎么进行块的管理的呢 :

  1. 假如文件大小只有500KB, 很明显,有效数据只会写到第一个 (index = 0 ) Stripe的第一个cell,这时候,index=1和index=2的internal block是生成但是数据都是0,还是压根都不会生成?
  2. 假如文件大小由4MB,很明显,4MB已经超过了一个Stipe,这时候显然这个Block Group的所有的internal block都会有有效数据,但是每个internal group的有效数据部分的大小都很小,那么这时候这些internal Group是固定大小,多余数据补0,还是只存放有效数据部分呢?

环境准备:

由于我的测试环境仅仅由5台测试机且在同一rack上,因此无法基于RS-6-2-1024k进行实验,该Policy明显要求至少6台DataNode并且分布在不同的Rack上。
因此,实验基于RS-3-2-1024k进行。Hadoop的rack-wareness是在NameNode端进行判断的,而不是DataNode进行报告的。参考《Hadoop Rack Awareness》的文档,用户可以自行通过Java、Shell、Python去随心所欲实现hostname到rack的映射关系。Cloudera通过Host -> Rack的映射文件和一个简单的Python脚本实现了Host到Rack的映射,我自己的测试Hadoop环境是开源环境,节约时间,我借用了Cloudera的处理方式,通过一个映射文件和一个Python脚本,将本来实际属于一个Rack的5台机器定义到了5个不同的Rack,让RS-3-2-1024k 能够在我的测试环境运行。

映射文件:

<?xml version="1.0" encoding="UTF-8"?>

<!--Autogenerated by Cloudera Manager-->
<topology>
  <node name="rccd101-6a.sjc2.dev.com" rack="/rccd101-6a"/>
  <node name="10.30.120.121" rack="/rccd101-6a"/>
  <node name="rccd101-6b.sjc2.dev.com" rack="/rccd101-6b"/>
  <node name="10.30.120.122" rack="/rccd101-6b"/>
  <node name="rccd101-6c.sjc2.dev.com" rack="/rccd101-6c"/>
  <node name="10.30.120.123" rack="/rccd101-6c"/>
  <node name="rccd101-7a.sjc2.dev.com" rack="/rccd101-7a"/>
  <node name="10.30.120.125" rack="/rccd101-7a"/>
  <node name="rccd101-7b.sjc2.dev.com" rack="/rccd101-7b"/>
  <node name="10.30.120.126" rack="/rccd101-7b"/>
</topology>

Python脚本:

------------------------------------------------------topology.py------------------------------------------------------
#!/usr/bin/env python
import os
import sys
import xml.dom.minidom

def main():
    MAP_FILE = 'etc/hadoop/topology.map'
    DEFAULT_RACK = '/default'
    max_elements = 1
    map = dict()
    mapFile = open(MAP_FILE, 'r')

    dom = xml.dom.minidom.parse(mapFile)
    for node in dom.getElementsByTagName("node"):
        rack = node.getAttribute("rack")
        max_elements = max(max_elements, rack.count("/"))
        map[node.getAttribute("name")] = node.getAttribute("rack")

    default_rack = "".join([ DEFAULT_RACK for _ in xrange(max_elements)])
    if len(sys.argv)==1:
        print(default_rack)
    else:
        print(" ".join([map.get(i, default_rack) for i in sys.argv[1:]]))
    return 0

if __name__ == "__main__":
    sys.exit(main())

在core-site.xml中添加配置:

  <property>
    <name>net.topology.script.file.name</name>
    <value>etc/hadoop/topology.py</value>
  </property>

过程

创建各种测试需要的文件

当我们尝试enable一个data unit的数量大于rack数量的hadoop集群中enable一个policy的时候,会抛出异常:

hadoop@rccd101-6b:/root$ $HADOOP_HOME/bin/hdfs ec -enablePolicy -policy RS-10-4-1024k
Erasure coding policy RS-10-4-1024k is enabled
Warning: The cluster setup does not support EC policy RS-10-4-1024k. Reason: The number of DataNodes (5) is less than the minimum required number of DataNodes (14) for the erasure coding policies: RS-10-4-1024k

因此我们在集群中Enable了RS-3-2-1024k这个Policy,这个Policy只要求集群由3个rack就够了:

# 创建基于纠删码RS(3,2)的目录
hadoop@rccd101-6b:/root$ $HADOOP_HOME/bin/hdfs dfs  -mkdir hdfs://olap-hdfs-test/test-RS-3-2-1024k
hadoop@rccd101-6b:/root$ $HADOOP_HOME/bin/hdfs ec -setPolicy -path hdfs://olap-hdfs-test/test-RS-3-2-1024k -policy RS-3-2-1024k
Set RS-3-2-1024k erasure coding policy on hdfs://olap-hdfs-test/test-RS-3-2-1024k

# 创建基于3副本复制的目录
hadoop@rccd101-6b:/root$ $HADOOP_HOME/bin/hdfs dfs  -mkdir hdfs://olap-hdfs-test/test-replication

然后我们向这个policy的目录写入一个500KB和一个4000KB的文件:

hadoop@rccd101-6b:/root$ truncate -s 500KB /tmp/500KB.log
hadoop@rccd101-6b:/root$ truncate -s 4000KB /tmp/4000KB.log
hadoop@rccd101-6b:/root$ truncate -s 122MB /tmp/122MB.log
hadoop@rccd101-6b:/root$ truncate -s 420MB /tmp/420MB.log
# 准备好纠删码的测试文件
hadoop@rccd101-6b:/root$ $HADOOP_HOME/bin/hdfs dfs -copyFromLocal /tmp/500KB.log hdfs://olap-hdfs-test/test-RS-3-2-1024k/500KB.log
hadoop@rccd101-6b:/root$ $HADOOP_HOME/bin/hdfs dfs -copyFromLocal /tmp/4000KB.log hdfs://olap-hdfs-test/test-RS-3-2-1024k/4000KB.log
hadoop@rccd101-6b:/root$ $HADOOP_HOME/bin/hdfs dfs -copyFromLocal /tmp/122MB.log hdfs://olap-hdfs-test/test-RS-3-2-1024k/122MB.log
hadoop@rccd101-6b:/root$ $HADOOP_HOME/bin/hdfs dfs -copyFromLocal /tmp/420MB.log hdfs://olap-hdfs-test/test-RS-3-2-1024k/420MB.log


# 准备好基于3副本复制的文件
hadoop@rccd101-6b:/root$ $HADOOP_HOME/bin/hdfs dfs -copyFromLocal /tmp/4000KB.log hdfs://olap-hdfs-test/test-replication/4000KB.log

测试小于一个Stripe的小文件的纠删码块构成

对于基于纠删码编码的500KB的文件,查看NameNode的日志:

2023-11-26 03:37:49,956 DEBUG StateChange: BLOCK* getAdditionalBlock: /test-RS-3-2-1024k/500KB.log._COPYING_  inodeId 16419 for DFSClient_NONMAPREDUCE_-2082370100_1
2023-11-26 03:37:49,959 DEBUG StateChange: DIR* FSDirectory.addBlock: /test-RS-3-2-1024k/500KB.log._COPYING_ with blk_-9223372036854775568_1018 block is added to the in-memory file system
2023-11-26 03:37:49,959 INFO StateChange: BLOCK* allocate blk_-9223372036854775568_1018, replicas=11.37.76.122:9866, 11.37.76.126:9866, 11.37.76.123:9866, 11.37.76.121:9866, 11.37.76.125:9866 for /test-RS-3-2-1024k/500KB.log._COPYING_
2023-11-26 03:37:49,959 DEBUG StateChange: persistNewBlock: /test-RS-3-2-1024k/500KB.log._COPYING_ with new block blk_-9223372036854775568_1018, current total block count is 1
2023-11-26 03:37:50,057 DEBUG BlockManager: Reported block blk_-9223372036854775568_1018 on 11.37.76.122:9866 size 500000 replicaState = FINALIZED
2023-11-26 03:37:50,057 DEBUG BlockManager: In memory blockUCState = UNDER_CONSTRUCTION
2023-11-26 03:37:50,059 DEBUG BlockManager: Reported block blk_-9223372036854775565_1018 on 11.37.76.121:9866 size 500000 replicaState = FINALIZED
2023-11-26 03:37:50,059 DEBUG BlockManager: In memory blockUCState = UNDER_CONSTRUCTION
2023-11-26 03:37:50,061 DEBUG BlockManager: Reported block blk_-9223372036854775564_1018 on 11.37.76.125:9866 size 500000 replicaState = FINALIZED

从上面的日志我们可以看到:

  1. 在NameNode为第一个BlockGroup分配Block和选定目标节点的时候,分配到了5台机器上,这是因为我们的RS(3,2)的policy要求5台机器
  2. 块汇报的时候,只有3台机器汇报了块,这三台汇报块的DataNode包含了一个存放数据的DataNode和两个存放校验码的DataNode。这说明:当一个Block Group的数据不足以写满一个Stripe的时候,没有数据的Internal Block虽然会在NameNode端分配(因为NameNode进行块分配的时候完全不知道这个块会有多大,只能按照EC Policy的Data Unit + Parity Unit的大小进行分配),但是,没有数据的internal block都不会产生,这说明客户端都不会往没有数据的internal block 上写任何东西,自然也不会有Block Report。
  3. 每个internal block(包括 Data Block 和 Parity Block)的大小都是根据文件的实际大小来的,不是固定的。这个文件500KB,那么这个internal block大小就是500KB,校验块也只有500KB。

测试大于一个Stripe但是小于一个Block Group的小文件的纠删码块构成

对于基于纠删码编码的4MB的文件,查看NameNode的日志:

2023-11-26 03:59:02,205 DEBUG StateChange: BLOCK* getAdditionalBlock: /test-RS-3-2-1024k/4000KB.log._COPYING_  inodeId 16420 for DFSClient_NONMAPREDUCE_-856274170_1
2023-11-26 03:59:02,206 DEBUG StateChange: DIR* FSDirectory.addBlock: /test-RS-3-2-1024k/4000KB.log._COPYING_ with blk_-9223372036854775552_1019 block is added to the in-memory file system
2023-11-26 03:59:02,206 INFO StateChange: BLOCK* allocate blk_-9223372036854775552_1019, replicas=11.37.76.122:9866, 11.37.76.125:9866, 11.37.76.121:9866, 11.37.76.123:9866, 11.37.76.126:9866 for /test-RS-3-2-1024k/4000KB.log._COPYING_
2023-11-26 03:59:02,206 DEBUG StateChange: persistNewBlock: /test-RS-3-2-1024k/4000KB.log._COPYING_ with new block blk_-9223372036854775552_1019, current total block count is 1
2023-11-26 03:59:02,455 DEBUG BlockManager: Reported block blk_-9223372036854775552_1019 on 11.37.76.122:9866 size 1902848 replicaState = FINALIZED
2023-11-26 03:59:02,457 DEBUG BlockManager: Reported block blk_-9223372036854775551_1019 on 11.37.76.125:9866 size 1048576 replicaState = FINALIZED
2023-11-26 03:59:02,458 DEBUG BlockManager: Reported block blk_-9223372036854775550_1019 on 11.37.76.121:9866 size 1048576 replicaState = FINALIZED
2023-11-26 03:59:02,460 DEBUG BlockManager: Reported block blk_-9223372036854775549_1019 on 11.37.76.123:9866 size 1902848 replicaState = FINALIZED
2023-11-26 03:59:02,464 DEBUG BlockManager: Reported block blk_-9223372036854775548_1019 on 11.37.76.126:9866 size 1902848 replicaState = FINALIZED

可以看到:

  1. NameNode依然分配了5台DataNode,RS(3,2)的policy要求5台机器
  2. 由于文件的大小超过了一个Stripe, 因此,所有的internal block,包括data block和parity block,都存放了一部分有效数据,都进行了汇报。
  3. 由于数据是4M,因此index=0的internal block会存放两个Stripe的数据,而index = [1,2]的internal block只会存放一个stripe的数据,总的数据量刚好匹配: (1902848B + 1048576B + 1048576B) = 4000KB
  4. 由于index = 0的internal block的size是1902848B,导致两个parity block的大小都会是1902848B,这也完全合理

测试大于一个Block Group的大文件的纠删码块构成

对于文件大小超过128MB * 3 = 402MB的文件,我们写入纠删码目录:

2023-11-26 04:36:45,130 DEBUG StateChange: BLOCK* getAdditionalBlock: /test-RS-3-2-1024k/420MB.log._COPYING_  inodeId 16427 for DFSClient_NONMAPREDUCE_185912574_1
2023-11-26 04:36:45,131 DEBUG StateChange: DIR* FSDirectory.addBlock: /test-RS-3-2-1024k/420MB.log._COPYING_ with blk_-9223372036854775472_1025 block is added to the in-memory file system
2023-11-26 04:36:45,131 INFO StateChange: BLOCK* allocate blk_-9223372036854775472_1025, replicas=11.37.33.122:9866, 11.37.33.125:9866, 11.37.33.123:9866, 11.37.33.126:9866, 11.37.33.121:9866 for /test-RS-3-2-1024k/420MB.log._COPYING_
2023-11-26 04:36:45,131 DEBUG StateChange: persistNewBlock: /test-RS-3-2-1024k/420MB.log._COPYING_ with new block blk_-9223372036854775472_1025, current total block count is 1
2023-11-26 04:36:47,317 DEBUG StateChange: BLOCK* getAdditionalBlock: /test-RS-3-2-1024k/420MB.log._COPYING_  inodeId 16427 for DFSClient_NONMAPREDUCE_185912574_1
2023-11-26 04:36:47,318 DEBUG StateChange: DIR* FSDirectory.addBlock: /test-RS-3-2-1024k/420MB.log._COPYING_ with blk_-9223372036854775456_1026 block is added to the in-memory file system
2023-11-26 04:36:47,319 INFO StateChange: BLOCK* allocate blk_-9223372036854775456_1026, replicas=11.37.33.122:9866, 11.37.33.125:9866, 11.37.33.121:9866, 11.37.33.126:9866, 11.37.33.123:9866 for /test-RS-3-2-1024k/420MB.log._COPYING_
2023-11-26 04:36:47,319 DEBUG StateChange: persistNewBlock: /test-RS-3-2-1024k/420MB.log._COPYING_ with new block blk_-9223372036854775456_1026, current total block count is 2

可以看到,客户端一共向NameNode申请了两次Block,这是因为文件的大小为420MB,由于HDFS默认的块大小是128MB(通过dfs.block.size配置),这个配置对于基于3副本复制的情况很好理解,就是一个replica的大小最大为128MB,但是对于纠删码,它指的并不是一个Block Group的最大大小,而是一个Internal Block的最大大小。因此,在RS(3,2)的情况下,一个Block Group最大为128MB*3 = 402MB,所以一个420MB的文件就需要客户端两次向NameNode申请Block,这里申请的Block是Block Group。

测试小于一个物理块的文件的3副本复制快的管理

对于基于3副本复制的500KB的文件,我们看看文件的size小于一个Block(默认128MB)的情况,查看NameNode的日志:

2023-11-26 04:09:41,604 DEBUG StateChange: DIR* FSDirectory.addBlock: /test-replication/500KB.log._COPYING_ with blk_1073741826_1020 block is added to the in-memory file system
2023-11-26 04:09:41,604 INFO StateChange: BLOCK* allocate blk_1073741826_1020, replicas=11.37.33.122:9866, 11.37.33.123:9866, 11.37.33.126:9866 for /test-replication/500KB.log._COPYING_
2023-11-26 04:09:41,605 DEBUG StateChange: persistNewBlock: /test-replication/500KB.log._COPYING_ with new block blk_1073741826_1020, current total block count is 1
2023-11-26 04:09:41,705 DEBUG BlockManager: Reported block blk_1073741826_1020 on 11.37.33.126:9866 size 500000 replicaState = FINALIZED
2023-11-26 04:09:41,707 DEBUG BlockManager: Reported block blk_1073741826_1020 on 11.37.33.123:9866 size 500000 replicaState = FINALIZED
2023-11-26 04:09:41,708 DEBUG BlockManager: Reported block blk_1073741826_1020 on 11.37.33.122:9866 size 500000 replicaState = FINALIZED

可以看到,在基于副本复制的存储策略下,如果文件大小小于128MB的块大小,那么实际的块的大小是以文件大小为准的,而不是固定的128MB

4. 引用

Understanding the Performance of Erasure Codes in Hadoop Distributed File System
Introduction to HDFS Erasure Coding in Apache Hadoop
HDFS Erasure Coding
Hadoop3.2.1 【 HDFS 】源码分析 : BPOfferService 解析
Rack Awareness
HDFS Erasure Coding
Hadoop3.2.1 【 HDFS 】源码分析 :BlockManager解析
https://issues.apache.org/jira/browse/HDFS-8734

5. 现在遗留的问题

blockGroup的getNumBytes到底是128MB还是128MB * 6 ? 即通过addBlock()申请一个group的时候返回的size,倒是是128MB还是128MB * 6
假如是128MB, 在客户端addBlock申请了一个block group以后,对internal block的大小按照data block的数量进行了切分StripedBlockUtil.parseStripedBlockGroup,那么一个internal block是128MB/6? 这跟Cloudera的官方文档的图一致,但是从代码里面看,下面代码中的blockSize就是创建文件的时候根据配置文件中配置的block size申请以后返回的,应该是128MB,所以怎么解释currentBlockGroup.getNumBytes() == blockSize * numDataBlocks;?如果下面的blockSize=128MB, 那就意味着一个block group的大小应该是128MB * 6 ? 可是blockGroup在进行addBlock()申请的时候返回的值是128MB才对。

如果

 private boolean shouldEndBlockGroup() {
    return currentBlockGroup != null &&
        currentBlockGroup.getNumBytes() == blockSize * numDataBlocks;
  }

把一个基于纠删码的文件copy到一个基于副本副本复制的目录下,会修改块布局方式吗?
创建文件的时候, ecPolicyName传入的是啥?如果是null,NameNode怎么设置所创建的文件的ECPolicy?

  • 10
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值