一文搞定Journal Node原理

【概述】


hdfs的HA机制,具体来说可以分为两部分,一部分是基于zkfc、zookeeper完成nn之间的选主;而另一部分则是nn之间的元数据共享与同步。

从hdfs2.0版本开始,引入了HA using Quroum Journal Manager(QJM)方案。

QJM的思想最初来源于paxos协议。在具体实现中,Journal Node(JN)集群作为NN之间editlog的一致性存储系统,也是HDFS高可用方案中的核心组件。

通过JN,ANN可以尽可能及时的将元数据同步到SNN,ANN通过push的方式将editlog写入JN集群,而SNN通过pull的方式从JN中读取editlog,JN之间彼此不主动进行数据的交互。

由于paxos协议的关系,读写JN都要求至少大多数节点成功返回才认为本次请求是成功的。另外,2N+1台JN组成的集群,最多可以容忍N个JN节点异常。因此整体具有很强的HA能力。

本文就其中的一些细节展开进行说明。

【JN中的概念与持久化文件】


一些概念:

  • epoch

    epoch是paxos协议中的一个概念,可以用于标识leader。

    而具体实现中,epoch是一个单调递增的整数,用来标识每一次ANN的生命周期,每发生一次NN的主备切换,epoch就会加1。

    ANN后续的每个请求中,都会携带epoch,同时JN会对请求进行校验,如果携带的epoch比本地存储的小,则抛出异常;而如果携带的epoch比本地存储的大,则更新本地的epoch。

    这就意味着,不管怎样进行NN的切换,任意时刻都只能有1个NN成功写JN。

  • txid

    每条写入的editlog都有一个对应的递增的事务ID。

  • segment

    多个连续的事务(txid)组成一个segment,持久化到一个对应的editlog文件中。

    这样,对应到具体实现中, 任何时间,JN上只会有一个segment处于正在写的状态(Inprogress),而其他的segment文件则都处于写完关闭的状态(Finalized)

JN中的持久化文件:

|-- current
|   |-- committed-txid
|   |-- edits_0000000000000000001-0000000000000000002
|   |-- edits_inprogress_0000000000000000003
|   |-- last-promised-epoch
|   |-- last-writer-epoch
|   |-- paxos
|   |-- VERSION
|-- in_use.lock
  • last-promised-epoch

    有两个地方会写入,一个是ANN发送新的epoch的rpc请求时,会进行更新;另一个是每次editlog的写请求时,会判断请求中的epoch是否比本地的epoch大,如果是则进行更新,否则会抛出异常。

  • last-writer-epoch

    每写一个新的segment时,检查请求中的epoch是比上次写入的epoch大,如果大,则将当前请求中的epoch写入文件中。

  • committed-txid

    处理每个写editlog的事务请求中,同步更新本地记录的事务ID。

  • edits_$starttxid-$endtxid

    已经写完关闭的segment文件,该文件记录了从开始事务ID到结束事务ID的连续的editlog信息。

  • edits_inprogress_$lasttxid

    当前正在写的segment文件,文件名中记录了开始事务ID

  • VERSION

    记录集群的相关信息,包括命名空间ID,集群ID,创建时间等。

【正常读写流程】


1. 格式化

在集群初始化时,首先会进行格式化动作,具体是由NN触发进行的。

JN收到格式化的请求后,先删除editlog存储目录下的所有文件,然后将NN的相关信息写到该目录下的VERSION文件中。

VERSION文件只有在格式化时才会写入,而如果本地没有VERSION,则会认为没有格式化,所有的请求都无法正确处理。

2. 设置epoch

在HA模式下,格式化完成后,两个NN通过zkfc完成选举动作,成为Active的NN首先向JN集群发送getJournalState的RPC请求,JN收到请求后返回自己保存的已经通过的epoch(lastPromiseEpoch)

NN收到大多数JN返回的epoch后,选择其中最大的epoch,并加1作为当前新的epoch,然后向各个JN发送newEpoch的RPC请求(请求中会带上新的epoch)

每个JN收到新的epoch后,首先检查这个新的epoch是否比它本地保存的lastPromiseEpoch大。

如果大的话就把lastPromiseEpoch更新为新的epoch,并且向NN返回自己本地磁盘上最新的一个editlogSegment的起始事务ID,为后续的数据恢复做准备;

如果小于或等于就向NN返回错误,表示该JN接收了更新的提案。

NN收到大多数JN对newEpoch的成功响应之后,就会认为生成新的Epoch成功。

在此之后进行editlog的恢复动作(对于新部署的,或者正常结束的集群,不会触发进行),这里暂时不展开,后面会讲解

完成恢复之后,则可以进行editlog的写入。

3. 读写editlog

ANN向JN写editlog的流程其实比较简单,就是向所有JN发送一个RPC请求,当收到多数节点的成功响应后,认为本次editlog的写入是成功的。

从JN中读取editlog的工作,则是在SNN中发生的。

SNN启动后,内部会创建一个EditLogTail的线程。具体读逻辑包括下面四个步骤

1)向所有JN请求从指定事务之后的editlog文件集合

2)对所有返回的editlog文件清单,按起始事务ID进行排序,并去除重复的文件

3)依次向editlog文件对应的jn发送请求,通过Http下载editlog文件

4)对下载后的editlog文件,依次读取每一条editlog并进行动作重放,即更新内存中inode、block等元数据信息。

注意:SNN的EditlogTail会定时读取从JN读取editlog信息,读取editlog并进行对应的重放后,只是在内存中对元数据进行了更新,并没有实际持久化到本地盘中。

如下图所示:

f0e236cfda22de71cf5ef6d73ab6bc75.png

并且从nn的日志中可以看出是有定时请求读取editlog信息的。

433e5ffaff8e0a4829bcc68b35ffed41.png

只有在定时触发checkpoint的时候,此时会再次读取editlog的信息(每次读取后在内存中更新了最新的事务ID,如果异常重启则是从本地文件中读取最后更新的事务ID),然后将元数据信息以fsimage的形式存储在本地,同时记录最后一次的事务ID。

【JN异常后的同步】


正常运行过程中,如果某个JN出现了异常,ANN内部会对该JN进行标记,后续不再给该JN发送editlog。直到触发进行滚动日志操作,此后如果异常的JN恢复正常,则继续向该JN写editlog。

也就是说,当JN出现异常后,ANN先标记该JN为异常,后续的editlog不写入。正在写入的editlog会有重试机制继续尝试;另外,当开始写新的segment文件时(滚动日志),如果该JN已经恢复正常,则接着写入,之前segment没有写入的editlog不会进行补写。

如下所示,JN异常时,从1465开始的editlog没有正确保存到segment文件中,以及之后的一段都因JN异常没有写入。

edits_0000000000000001478-0000000000000001479则是JN恢复正常之后写入的segment文件,中间缺失的editlog不会补偿写入。

-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:03 edits_0000000000000001457-0000000000000001458
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:05 edits_0000000000000001459-0000000000000001460
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:07 edits_0000000000000001461-0000000000000001462
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:09 edits_0000000000000001463-0000000000000001464
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:40 edits_0000000000000001478-0000000000000001479
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:40 edits_0000000000000001480-0000000000000001481
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:40 edits_0000000000000001482-0000000000000001483
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:40 edits_0000000000000001484-0000000000000001485
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:40 edits_0000000000000001486-0000000000000001487
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:40 edits_0000000000000001488-0000000000000001489
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:40 edits_0000000000000001490-0000000000000001491
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:40 edits_0000000000000001492-0000000000000001493
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:40 edits_0000000000000001494-0000000000000001495
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:40 edits_0000000000000001496-0000000000000001497
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:40 edits_0000000000000001498-0000000000000001499
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:40 edits_0000000000000001500-0000000000000001501
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:41 edits_0000000000000001502-0000000000000001503
-rw-r--r--. 1 hadoop hadoop      42 Oct 19 22:43 edits_0000000000000001504-0000000000000001505
-rw-r--r--. 1 hadoop hadoop 1048576 Oct 19 22:09 edits_inprogress_0000000000000001465
-rw-r--r--. 1 hadoop hadoop 1048576 Oct 19 22:43 edits_inprogress_0000000000000001506

【JN异常后的同步】


当ANN出现异常时,通过ZKFC以及ZK,SNN会成为新的ANN,由于原来的ANN异常时,JN集群中的不同节点,其写入的editlog可能不一致,因此新的ANN先要保证所有JN的数据一致后,才能进行写入,这个过程就是JN的数据恢复流程,具体包括如下几个步骤:

1. 向所有JN获取当前的epoch,从结果中选取最大的epoch。

2. 将最大的epoch加1后,作为新的epoch,然后写入所有JN中。

3. 从所有JN获取最新segment的起始事务ID(txid),该步骤实际上是上一步骤请求的响应结果中带回来的,这样就减少了一次rpc请求。

例如下面,两个JN,正在写的segment文件,起始的事务ID均为16,因此在newEpoch的请求中,都返回了16。

6ae691caa4b9fa1c71352cb6bfc129e8.png

4. 从结果中选取最大的事务ID作为参数,然后向所有JN发送prepareRecovery的RPC请求。

5. JN收到请求后,根据自身情况,返回指定起始事务ID的segment信息,包括起始事务ID、结束事务ID、以及当前segment是否为正在写。

接着上图的流程,jn-1中,已经处理了事务ID为28的editlog请求,因此返回16-28。而jn2仅处理到16的事务ID(之前出现了异常,导致后续的editlog没有写进来),因此返回16-16。

047efcec19dc81c96ea3352782a20a9e.png

6. ANN从请求结果中,选取最优的segment,并组装为URL,包括该segment所在的JN、命名空间信息、以及segment的信息,将该URL作为参数,向JN发送AcceptRecovery的RPC请求,要求进行数据的同步。

7. JN收到请求后,对URL进行合法性校验,然后向指定的JN发送HTTP请求,下载对应的segment。

ann收到请求响应后,选择需要恢复的segment,显然16-28是最新最全的数据,因此将该起始段的segment作为待恢复的数据,发送给所有jn。

jn1收到数据后,发现本地已经是最新的了,因此忽略处理。而jn2发现本地数据有落后,因此解析segment的url,向jn1发送数据同步的http请求,完成数据下载。

b45c6e27e34d7e697c49710ac7f54d33.png

8. JN在发送完AcceptRecovery的RPC请求后,会继续向JN发送一条finalizedSegment的请求,要求关闭当前正在写的segment,然后打开新的segment进行写。

074637a5fb4fee8a4c67023210f9b9a4.png

对照jn1、jn2的日志也能更好的理解这一流程。

jn1的日志如下图所示:

434686c9695b030cec129c6da76adbd7.png

jn2的日志如下图所示:

f656d6575f525da71a0f1d4435a510ed.png

【其他常见问题】


  • JN写editlog是试试刷新到本地磁盘的吗?

    不是,JN将editlog写入本地磁盘和NN写editlog到本地是复用了同一份代码,定时触发刷盘。

  • SNN从JN读editlog是实时读取的吗?

    不是,周期性进行读取,默认时间间隔为1分钟。

  • JN存储的editlog会自动删除吗?

    不会,SNN触发执行checkpoint时才会进行删除,同时还会进行一定数量的事务的保存。

【巨人的肩膀和推荐阅读】


1. https://hexiaoqiao.github.io/blog/2018/03/30/the-analysis-of-basic-principle-of-hdfs-ha-using-qjm/

2. HDFS——editlog

3. HDFS——fsimage

好了,本文就介绍到这里,原创不易,点赞,在看,分享是最好的支持, 谢谢~

b22f6f3af226b58d8119812994c26b94.png

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值