vlambda博客
学习文章列表

[知识讲解篇]一篇文章说明白HDFS的恢复机制

作者 | 王sir说运维


1背景知识需要先知道


1. 文件存储在HDFS中是怎么存储的?

HDFS 中文件的存储方式是将文件block进行切分,默认一个 block 64MB(这个值在hadoop2.x中是128M)。若文件大小超过一个 block 的容量(64M)就会被切分为多个 block,这些block会根据存储策略存储在不同的 DataNode 上。

若文件大小小于一个 block 的容量(64M),则文件只有一个 block,实际占用的存储空间为文件大小容量加上一点额外的校验数据。例如:当一个1MB的文件存储在一个128M的块中时,文件只使用1M的磁盘空间在加上一点点这个文件的校验数据的空间,而不是64M


2.一个文件至少由一个或多个 block 组成,而一个 block 仅属于一个文件


每个 block 在本地文件系统中由两个文件组成,第一个文件包含文件数据本身,第二个文件则记录 block 的元信息(metadata)。如下所示分别为数据和对这个数据的校验文件(比如checksum)。


[知识讲解篇]一篇文章说明白HDFS的恢复机制


文件数据存储的可靠性依赖多副本保障,对于每一个 DataNode 节点而言,它只需保证自己存储的 block 是完整且无损坏的即可。DataNode 会主动周期性的运行一个 block 扫描器(scanner)通过比对 checksum 来检查 block 是否损坏。


3. HDFS支持哪些文件操作?

HDFS 支持的文件操作包括写入(新增、追加)、读取和删除

  • Open 打开文件

  • Read/Write 读写文件

  • Close 关闭文件


4. HDFS如何写文件?


[知识讲解篇]一篇文章说明白HDFS的恢复机制


在分布式环境下,为了防止多个Client对同一个文件进行写入,因此,引入了租约的概念(lease,本质上是一种分布式锁)。当Client要对文件进行写入的时候,先获取该文件的租约,只有成功获取到这个文件租约的 Client 才可以向该文件写入。

当Client获得写入租约后,NameNode 会给 Client 分配一组(分配几个,是由副本数决定的)用于存放文件数据的 DataNodes。这一组 DataNodes 被组成一条流水线(pipeline)来写入,有效提升写入性能降低写入延迟。

Client 将文件组织成一个个 packet (注意到这里不要和前面的block混了,这里的packet很小,默认是64k)发送给流水线上第一个 DataNode,第一个 DataNode 存储下该 packet 后再转发给第二个 DataNode(注意这里是由第一个datanode负责转发的,而不是Client),依此类推,每个DataNode接受成功后,都会报告给NameNode该block已经在本DataNode存储成功。当所有文件 block 写入完成后,DataNodes 会向 NameNode 报告文件的 block 接收完毕,NameNode 相应去改变文件元数据的状态。

通过上面的分析,你会发现写文件过程涉及 Client、NameNode 和一组 DataNodes,如果其中任何一个环节出现问题,那么写入就会失败。所以,HDFS的设计者要对这个写入流程的所有环节都要进行容错性设计。


5.HDFS如何读文件?


[知识讲解篇]一篇文章说明白HDFS的恢复机制


Client 首先请求 NameNode 定位文件 block 所在的 DataNodes。然后按顺序请求对应的 DataNodes 读取其上存储的 block。关于读取顺序,HDFS 有一个就近读取的优化策略,DataNodes 的读取排序会按照它们离 Client 的距离来确定。距离的概念主要区分以下几种场景:

距离 0,表示在同一个节点上

距离 2,表示同一个机架下的不同节点

距离 4,表示同一个数据中心的不同机架下

距离 8,表示不同的数据中心


6. HDFS如何删文件?

文件删除的处理首先将文件重命名后放进 /trash 目录。文件会在 /trash 目录中存放一段时间(可配置),在时间到期后再自动清理。所以实际上文件删除操作非常轻量级,仅仅是 NameNode 的内存数据结构的变动,真正的物理删除在后续的自动清理时(配置的在回收站的保留周期到期)才做


7. DataNode如何汇报自己拥有的block?

DataNode 通过定期(默认1个小时)的向 NameNode 发送 block report 信息来汇报自己拥有的block,block report 的内容说白了就是 DataNode 上存储了哪些文件 block 的列表

此外,DataNode 将定时向 NameNode 发送心跳(默认 3 秒,可配置)来报告自身的存活性。一段时间(默认 10 分钟)收不到 DataNode 最近的心跳,NameNode 会认定其死亡,并不会再将 I/O 请求转发到其上

心跳除了用于 DataNode 报告其存活性,NameNode 也通过心跳回复来捎带控制命令要求 DataNode 执行,因为 NameNode 设计上不直接调用 DataNode 其控制命令都是通过心跳回复来执行,所以心跳的默认间隔比较短。


8. HDFS在遇到异常时如何恢复(HDFS Recovery Processes )?
HDFS写入过程中会发生哪些异常?

通过在背景知识中回顾的写入所涉及到的三个组件(Client、NameNode和DataNode),我们可以想到对应的异常场景,可以分为如下三种:

Client 在写入过程中,自己挂了

Client 在写入过程中,有 DataNode 挂了

Client 在写入过程中,NameNode 挂了


Client 在写入过程中,自己挂了

当 Client 在写入过程中,自己挂了。由于 Client 在写文件之前需要向 NameNode 申请该文件的租约(lease),只有持有租约才允许写入,而且租约需要定期续约。所以当 Client 挂了后租约会超时,HDFS 在超时后会释放该文件的租约并关闭该文件,避免文件一直被这个挂掉的 Client 独占导致其他人不能写入。这个过程称为 lease recovery

试想一个场景,当一个file正在被写入,突然,Client挂了,那么这个文件的最后的block 会没有完全写完,这个block在各个节点写入的 packet 的大小也不会一致,那么这种异常的场景就需要进行恢复

在租约恢复导致文件关闭之前,必须确保最后一个块的所有副本具有相同的长度,这个过程称为block recovery,这个过程只能在 lease recovery 过程中发起。


当 Client 在写入过程中,有 DataNode 挂了

HDFS的写入操作不会失败,HDFS将尝试自行恢复,将挂了的 DataNode 从流水线中剔除,并尝试将数据写入其它DataNode,这个过程称为 pipeline recovery,当然如果选择的其它的DataNode写入也失败了,那么本次写入就会失败。


当 Client 在写入过程中,NameNode 挂了

注意这里的先决条件是 NameNode 已经开始写入了,所以 NameNode 已经完成了对 DataNode 的分配,若写之前 NameNode 就挂了,整个 HDFS 是不可用的所以也无法开始写入

流水线写入过程中,当一个 block 写完后需向 NameNode 报告其状态,这时 NameNode 挂了,状态报告失败,但不影响 DataNode 的流线工作,数据先被保存下来,但最后一步 Client 写完向 NameNode 请求关闭文件时会出错,由于 NameNode 的单点特性(如果有HA则没事),所以无法自动恢复,需人工介入恢复。

下面在详细的讲解恢复过程之前,需要了解文件分别在DataNode和NameNode的存储状态,因为写文件过程中异常和恢复会对数据状态产生影响。


Replica 状态

Replica 在 DataNode 中存在的状态列表如下:

  • FINALIZED:表明 replica 的写入已经完成,长度已确定,除非该 replica 被重新打开并追加写入。一个块的所有finalized 的副本具有相同的数据和 generation stamp(GS或校验).

  • RBW:该状态是 Replica Being Written 的缩写,表明该 replica 正在被写入(创建写入或者重新打开追加写入两种),正在被写入的 replica 总是打开文件的最后一个块。处于这个状态的副本,表示数据正在被写入而且没有结束,此时,这个副本的数据对于客户端是可见的。如果在此过程中出现任何故障,HDFS将尝试将数据保存在RBW副本中。

  • RWR:该状态是 Replica Waiting to be Recovered 的缩写,假如,写入过程中 DataNode 挂了,然后重启后,其上处于 RBW 状态的 replica 将被变更为 RWR 状态,这个状态说明其数据需要恢复或者丢弃,因为在 DataNode 挂掉期间其上的数据可能过时了。

  • RUR:该状态是 Replica Under Recovery 的缩写,表明该 replica 正处于恢复过程中。当一个非临时状态的副本参与lease recovery 时,它的状态将会被更改为RUR.

  • TEMPORARY:一个临时状态的 replica 是因为复制或者集群平衡的需要而创建的,若复制失败或其所在的 DataNode 发生重启,所有临时状态的 replica 会被删除。临时态的 replica 对外部 Client 来说是不可见的。

DataNode 会持久化存储 replica 的状态,每个数据目录都包含了三个子目录:

  • current:目录包含了 FINALIZED 状态 replicas。注意:有的版本rbw目录是放在current下面的。

  • tmp:目录包含了 TEMPORARY 状态的 replicas,这里面的数据当DataNode重启时,会被删除。

  • rbw:目录则包含了 RBW、RWR 和 RUR 三种状态的 relicas,从该目录下加载的 replicas 默认都处于 RWR 状态。

当Client 请求第一次创建副本时,它被放在rbw目录中。

当为了复制或集群平衡而首次创建副本时,将其放在tmp目录中。

当副本的状态为FINALIZED时,将其移动到 current 目录。

当一个DataNode重新启动时,tmp目录中的副本将被删除,rbw目录中的副本将作为rwr副本加载,current 目录中的副本将作为FINALIZED副本加载。

从目录看出实际上只持久化了三种状态,而在内存中则有五种状态,从下面的 replica 状态变迁图也可以看出这点

[知识讲解篇]一篇文章说明白HDFS的恢复机制

replica 的状态变迁图,主要有如下几种

从Init出发,一个新创建的 replica 有两种情况:

  1. Client 请求,新建的 replica 用于写入,状态为 RBW

  2. NameNode 请求,新建的 replica 用于复制或集群间再平衡拷贝,状态为 TEMPORARY

从RBW出发,有三种情况:

  1. Client 写完并关闭文件后,切换到 FINALIZED 状态。

  2. replica 所在的 DataNode 发生重启,切换到 RWR 状态,重启期间数据可能过时了,可以被丢弃。

  3. replica 参与 block recovery 过程,切换到 RUR 状态。

从TEMPORARY出发,有两种情况:

  1. 复制或集群间再平衡拷贝成功后,切换FINALIZED 状态。

  2. 复制或集群间再平衡拷贝失败或者所在 DataNode 发生重启,该状态下的 replica 将被删除,注意这个状态状态上图未有体现,但这里是存在的,需要注意一下。

从RWR出发,有两种情况:

  1. 所在 DataNode 挂了,重启后又回到 RWR 状态,自己到自己。

  2. replica 参与 block recovery 过程,切换到 RUR 状态。

从RUR出发,有两种情况

  1. DataNode 挂了,就变回了 RBW 状态,重启后只会回到 RWR 状态,看是否还有必要参与恢复还是过时直接被丢弃。

  2. 恢复完成,切换到 FINALIZED 状态。

从FINALIZED出发,有两种情况:

  1. 文件重新被打开追加写入,文件的最后一个 block 对应的所有 replicas 切换到 RBW

  2. replica 参与 block recovery 过程,切换到 RUR 状态。

Block 在 NameNode 中存在的状态列表如下
  • UNDER_CONSTRUCTION:当新创建一个 block 或一个旧的 block 被重新打开追加时处于该状态,处于改状态的总是一个打开文件的最后一个 block。它的长度和生成戳仍然是可变的,NameNode中的UNDER_CONSTRUCTION 状态的块会跟踪该块写入的 Pipeline (有效RBW副本的位置)及其RWR副本的位置。

  • UNDER_RECOVERY:当文件租约超时,一个处于 UNDER_CONSTRUCTION 状态下 block 在 block recovery 过程开始后会变更为该状态。

  • COMMITTED:表明 block 数据已经不会发生变化,但向 NameNode 报告处于 FINALIZED 状态的 replica 数量少于最小副本数要求。为了为读请求提供服务,COMMITTED状态的块必须跟踪RBW副本的位置、GS和FINALIZED的副本的长度。

  • COMPLETE:当 NameNode 收到处于 FINALIZED 状态的 replica 数量达到最小副本数要求后,则切换到该状态。只有当文件的所有 block 处于该状态才可被关闭。

NameNode 不会持久化存储这些状态,一旦 NameNode 发生重启,它将所有打开文件的最后一个 block 设置为 UNDER_CONSTRUCTION 状态,其他则全部设置为 COMPLETE 状态。


从 Init 出发,只有当 Client 新建或追加文件写入时新创建的 block 处于 UNDER_CONSTRUCTION 状态。

从UNDER_CONSTRUCTION出发,有三种情况:

  1. 当客户端发起 add block 或 close 请求,若处于 FINALIZED 状态的 replica 数量少于最小副本数要求,则切换到 COMMITTED 状态,这里 add block(Client每写一个block就会发起一个add block的操作,比如:一个文件有2个block,那么就会发起2次add block操作,所以,后面降到影响的是倒数第二个block) 操作影响的是文件的倒数第二个 block 的状态,而 close 影响文件最后一个 block 的状态。

  2. 当客户端发起 add block 或 close 请求,若处于 FINALIZED 状态的 replica 数量达到最小副本数要求,则切换到 COMPLETE 状态

  3. 若发生 block recovery,状态切换到 UNDER_RECOVERY

从UNDER_RECOVERY,有三种情况:

  1. 0 字节长度的 replica 将直接被删除。

  2. 恢复成功,切换到 COMPLETE

  3. NameNode 发生重启,所有打开文件的最后一个 block 会恢复成 UNDER_CONSTRUCTION 状态。

从COMMITTED出发,有两种情况:

  1. 若处于 FINALIZED 状态的 replica 数量达到最小副本数要求或者文件被强制关闭或者 NameNode 重启且不是最后一个 block,则直接切换为 COMPLETE 状态。

  2. NameNode 发生重启,所有打开文件的最后一个 block 会恢复成 UNDER_CONSTRUCTION 状态。


从 COMPLETE 出发,只有在 NameNode 发生重启,其打开文件的最后一个 block 会恢复成 UNDER_CONSTRUCTION 状态。这种情况,若 Client 依然存活,有 Client 来关闭文件,否则由 lease recovery 过程来恢复。

2Lease Recovery 和 Block Recovery

前文讲到 lease recovery 的目的是当 Client 在写入过程中挂了后,经过一定的超时时间后,收回租约并关闭文件。但在收回租约关闭文件前,需要确保文件 block 的多个副本数据一致(分布式环境下很多异常情况都可能导致存储该block的多个数据节点副本不一致),若不一致就会进入 block recovery 过程进行恢复。

下面是block recovery 恢复处理流程:

  1. 获取包含文件最后一个 block 的所有 DataNodes。

  2. 指定其中一个 DataNode 作为主导恢复的节点。

  3. 主导节点向其他DataNode节点请求获得它们上面存储的该Block的 replica 信息。

  4. 主导节点收集了包含该Block信息的所有DataNode节点上的 replica 信息后,就可以计算出那个节点上面的 replica 的长度是最小的。

  5. 主导节点向其他节点发起更新,将各自 replica 更新为最小长度值(一般是pipeline流中的最后一个节点的replica的长度是最小的),保持各节点 replica 长度一致。

  6. 所有 DataNode 都同步后,主导节点向 NameNode 报告更新一致后的最终结果。

  7. NameNode 更新文件 block 元数据信息,收回该文件租约,并关闭文件。


为什么在多个副本中选择最小长度作为最终更新一致的标准?

想想写入流水线过程,如果 Client 挂掉导致写入中断后,对于流水线上的多个 DataNode 收到的数据在正常情况下应该是一致的。但在异常情况下,排在首位的收到的数据理论上最多,末位的最少,由于数据接收的确认是从末位按反方向传递到首位再到 Client 端。所以排在末位的 DataNode 上存储的数据都是实际已被确认的数据,而它上面的数据实际在不一致的情况也是最少的,所以算法里选择多个节点上最小的数据长度为标准来同步到一致状态。

如上图所示,pipeline 写入包括三个阶段:

  1. pipeline setup:Client 发送一个写请求沿着 pipeline 传递下去,最后一个 DataNode 收到后发回一个确认消息。Client 收到确认后,pipeline 设置准备完毕,可以往里面发送数据了。

  2. data streaming:Client 将一个 block 拆分为多个 packet 来发送(默认一个 block 64MB,太大所以需要拆分)。Client 持续往 pipeline 发送 packet,在收到 packet ack 之前允许发送 n 个 packet,n 就是 Client 的发送窗口大小(类似 TCP 滑动窗口)。

  3. close:Client 在所有发出的 packet 都收到确认后发送一个 Close 请求,pipeline 上的 DataNode 收到 Close 后将相应 replica 修改为 FINALIZED 状态,并向 NameNode 发送 block 报告。NameNode 将根据报告的 FINALIZED 状态的 replica 数量是否达到最小副本要求来改变相应 block 状态为 COMPLETE

上述三个阶段中任何一个阶段都有可能发生 Pipeline recovery ,只要和写入相关的DataNode 遭遇网络或自身故障,那么都需要进行恢复。


从 pipeline setup 错误中恢复

在 pipeline 准备阶段发生错误,分两种情况:

  1. 新写文件:Client 重新请求 NameNode 分配 block 和 DataNodes,重新设置 pipeline。

  2. 追加文件:Client 从 pipeline 中移除出错的 DataNode,然后继续。

从 data streaming 错误中恢复

  1. 当 pipeline 中的某个 DataNode 检测到写入磁盘出错(可能是磁盘故障或者网络故障等等),它自动退出 pipeline,关闭相关的 TCP 连接。

  2. 当 Client 检测到 pipeline 有 DataNode 出错,先停止发送数据,并基于剩下正常的 DataNode 重新构建 pipeline 再继续发送数据。

  3. Client 恢复发送数据后,从没有收到确认的 packet 开始重发,其中有些 packet, 前面的 DataNode 可能已经收过了,则忽略存储过程直接传递到下游节点

从 close 错误中恢复

到了 close 阶段才出错,实际数据已经全部写入了 DataNodes 中,所以影响很小了。Client 依然根据剩下正常的 DataNode 重建 pipeline,让剩下的 DataNode 继续完成 close 阶段需要做的工作。


总结:

当一个Datanode is bad时,它会把自己从pipeline中remove掉。在pipeline recovery的过程中, client可能会利用剩下的Datanodes(client可能会使用新的Datanode来替换bad Datanode,也可能不会替换。这取决于DataNode replacement policy)来rebuild一个新的pipeline。replication monitor会关注与复制block来满足我们配置的replication factor。

  1. NEVER:从不替换,针对 Client 的行为

  2. DISABLE:禁止替换,DataNode 服务端抛出异常,表现行为类似 Client 的 NEVER 策略,看下面英文的

  3. DEFAULT:默认根据副本数要求来决定,简单来说若配置的副本数为 3,如果坏了 2 个 DataNode,则会替换,否则不替换

  4. ALWAYS:总是替换


DataNode Replacement Policy upon Failure(英文原文)

There are four configurable policies regarding whether to add additional DataNodes to replace the bad ones when setting up a pipeline for recovery with the remaining DataNodes:

  1. DISABLE: Disables DataNode replacement and throws an error (at the server); this acts like NEVER at the client.

  2. NEVER: Never replace a DataNode when a pipeline fails (generally not a desirable action).

  3. DEFAULT: Replace based on the following conditions:

    a. Let r be the configured replication number.

    b. Let n be the number of existing replica datanodes.

    c. Add a new DataNode only if r >= 3 and EITHER

    • floor(r/2) >= n; OR

    • r > n and the block is hflushed/appended.

  4. ALWAYS: Always add a new DataNode when an existing DataNode failed. This fails if a DataNode can’t be replaced.