HDFS QJM的架构设计
前言
HDFS作为一套成熟的分布式存储系统,它的HA机制很多人可能都比较清楚,但是与之相关联的HDFS QJM机制可能了解它的人就不是那么多了.HDFS-3077(Quorum-based protocol for reading and writing edit logs)对此进行了实现.QJM全称Qurom Journal Manager,它的一个核心原理是基于Quorum((最低)法定人数)的原理,这里默认的数值是一半以上,什么意思呢?通俗的解释就是说,我任何的操作,只需要保证quorum数量的操作成功就认为是一次最终成功的操作了,并不需要保证每次操作的成功返回.QJM作为HDFS内非常经典的一套实现机制,最好的办法还是阅读它的原始设计文档,在阅读完HDFS-3077 JIRA上的设计文档,个人感觉还是非常不错的,于是翻译了其中部分的章节,也就是说,本文是对其设计文档的一个局部译文.(注:本译文为了保持整体的可读性与篇幅大小的控制,略过了QJM内具体的算法操作,原理,感兴趣的读者可自行阅读文章末尾参考资料中的原始设计文档).
目录
概述
1.1 背景
1.2 当前实现的局限性
1.3 其他可行方案的要求
1.4 基于Quorum((最低)法定人数)的方案设计 - 写日志
2.1 模块
2.2 QuorumJournalManager内部流程实现
3.1 Quorum的实现
3.2 JournalManager的实现
3.3 JournalNodeMetrics度量统计
概述
背景
HDFS-1623和其他相关的JIRA在现有HDFS的NameNode基础上增加了HA的支持,但是他们需要依赖一个存放editlog文件的共享存储目录.而且这个共享存储必须也是高可用的,它们会被集群中所有NameNodes同时访问.
目前对于共享的editlog存储目录,一个推荐的做法是通过一个NAS(Network-attached storage,网络相关联的存储)设备,然后挂载到NFS上.然后这个挂载好的目录允许Active NameNode写editlog到上面,同时Standby NameNode可以通过tail的方式去读这些文件中新写入的数据.如果在这期间NameNode failover切换发生了,这里还需要部署一个脚本能够停止之前的Active NameNode或者能阻止之前的Active NameNode继续访问共享存储目录.
当前实现的局限性
以上提到的许多要求在许多环境下是可以满足的.比如我们可以依赖一个HA的filer(一种特定存储设备的称呼)来满足多个产品内的高可用性.限制脚本的需求我们可以通过远程控制分布式单元(PDU)或者在NAS设备上实现一个协议的方式来完成.
然而,在某些场合下,这个解决方案是不能被满足的,以下是几点原因:
1.定制硬件.在某些时候,一个NAS设备和远程控制单元(PDU)的价格是很昂贵的.这个与一些”filer-free”组织内部所使用的标准设备还是不一样的.
2.复杂的部署.即使HDFS已经安装部署完成,但是管理者还要花额外的步骤来配置NFS挂载目录,自定义的fencing限制脚本等等.这些操作会复杂化HA的部署过程,而且这会容易导致配置误配造成集群不可用.
3.NFS客户端实现的简陋.在Linux的许多版本中,NFS客户端的实现不够健壮,很容易导致误配.比如说当NameNode处于中断服务场景的时候,管理员非常容易错误配置挂载的选项.
其他可行方案的要求
区别不同的要求
此设计文档主要阐述了一个新的方案,主要满足以下几个关键点:
1.没有对特定的硬件的要求.此设计应该只需要使用普通的商业硬件即可,比如说Hadoop集群中已经部署的哪些节点.
2.没有对fencing限制脚本的配置要求.所有需要fencing的操作应该只需要发生在软件层面,封装到系统中实现.
3.没有单点问题.作为HA解决方案的一部分,editlog的存储方案也应该是HA高可用的.
以上3点表明了editlog日志将会保存在多个节点上.从现在起,我们将会称呼这些节点为journal replicas.
相同的要求
当然,我们还是要保留现有HDFS editlog内的一些基本要求:
1.任何同步好的edit必须不能够被遗落.如果NameNode已经成功调用了FSEditLog.logSync()方法,那么所有同步好的edit必须被持久化和永久地记录,不管其中是否有任何的失败.
2.一个没有同步好的edit可能被遗落或没遗落.如果NameNode写了一个edit然后在logSync()操作之前或正在操作时发生崩溃中止了,那么系统可能记录了这个edit也可能遗落了这个edit.
3.如果一个edit已经被读过,则它一定没有被遗落.如果任何一个Standby NameNode tail读取一个edit并且成功读取出一个edit,则此edit一定没有被遗落.
4.对于任何给定的txid,一定存在对应唯一的一个有效的事务.如果任何一个节点通过给定的id读取一个事务,那么其他任何一个节点同样通过此id读取到的事务数据与前者读取到的一定是相同的.
额外的要求
另外,以下几点是此次设计中提供的一些不是强制性的需求点:
1.失败容忍数的配置.如果一个管理员想要容忍超过一个节点失败的情况,他或她需要在系统中配置额外的节点来达到期望的容错效果.具体地说,当我们配置了2N+1个节点的时候,此时我们能够容忍N个节点失败的情况.
2.一个慢的journal replica不应该影响到整体的延时.如果存储editlog的其中一个节点突然变慢或出现失败的情况时,系统应该继续进行操作,并不会有延时的影响.当一个节点失败的时候,我们没有必要一定要在超时时间内去此客户端上读写edit.
3.增加journal replica不应该增加延时时间.为了容忍超过1个节点失败的情况,管理员可能会配置5个或5个更多的journal replica.外界对这些journal replica的通信应该是并行执行的,所以增加journal replica数并不会导致延时时间的线性增长.
实际的要求
以下额外的要求并不是本节算法所特别指定的,但是作为HDFS安装部署的一部分来说这还是很重要的.
1.Metrics/logging.任何被额外引进并作为系统一部分的后台驻留程序都应该与现有HDFS存在的的metric统计和logging日志系统进行集成.这对于加强现有的监控体系是很有必要的.
2.配置.任何有必要的配置应该与HDFS现有的配置方式一致,都应采用基于xml格式的配置文件.这样对于操作者而言会更加熟悉.
3.安全.任何涉及跨越多个节点的操作必须是做到互相认证的,其次是传输内容必须经过加密的,加密的原理可以采用Hadoop现有的一些算法.比如说,任何IPC/RPC的传输可以使用基于SASL的方式进行传输,同时使用Kerberos提供的认证机制.另外一个例子,任何使用Zookeeper的场景都应该支持Zookeeper的ACL访问列表控制和它的认证机制.
基于Quorum((最低)法定人数)的方案
本文所要阐述的设计方案叫做Quorum Journal Manager.这是一种可行的方案来解决以上所提出的问题,同时能满足我们上面的要求以及额外的实现目标.
这个设计依赖了一个叫做quorum commit的概念,而这些quorum commit来自于集群中的守护进程程序,在这里我们称之为JournalNode.每个JournalNode将会暴露一个简单的RPC接口,允许NameNode去读写存在各自磁盘上的editlog日志.当一个NameNode写一个edit,它将发送此edit到集群中所有的JournalNode,然后等待大多数的JournalNode的返回.一旦大多数的JournalNode已经回应了成功的返回码,则此edit被认为是已经提交成功的.
下面的小节我们来更加细致的谈论此设计.
设计 - 写日志
模块
系统主要依赖于下面几个模块:
1.QuorumJournalManager的实现,此服务将会运行在每个NameNode之上.这个组件实现了HDFS现有存在的JournalManager接口.它主要负责集群其他组件与JournalNode之间的RPC通信,比如发送edit,执行fencing脚本和一些同步操作等.
2.JournalNode守护进程,运行在集群中N个节点上.每个这样的守护进程通过Hadoop IPC向外界暴露一个接口,以此让QuourumJournalManager能够远程地写edit到它们本地的磁盘中.它使用了已存在的类FileJournalManager来实现本地存储的管理.
我们预料在正常的部署当中,管理者一般会配置3个JournalNode.这些JournalNodes将会运行在3个同样的物理硬件上:第一个NameNode,第二个Standby NameNode,第三个JobTracker.这3个地方是足够具有吸引力的,因为它们是现成已提供的用户活动较少,以及磁盘使用率较低的一些节点.在绝大多数环境下,为JournalNode节点选出一块存储editlog的理想的磁盘应该是非常简单的事情.
如果一个用户希望承受住2个失败的情况,或者在失败的的时候还想维持系统正常的运转,他或她可能需要配置5个或5个更多的JournalNodes.给定N个JournalNodes,系统能够容忍(N-1)/2个失败的情况.
QuorumJournalManager内部流程
当QJM准备开始去写editlog的时候,它将会执行下面的一些操作:
1.杀死早先的写操作对象.当前的写操作对象必须保证没有更早之前的写对象还在写editlog.这就是fencing限制原理的一个操作,即使2个NameNode都认为它们是active的节点,但是还是只能有1个节点能够成功的执行edit相关操作.可以阅读下面更详细的信息.
2.恢复正在进行中的log.如果写操作对象之前写的log失败了,在最终的时候是有可能造成不同JounalNode节点上日志的长度不相同.(可能一种情况是早先的写对象类在失败前只将edit发送给了3个JournalNode中的1个).我们必须同步好这些log,并保证它们的长度一致.
3.开始一个新的log segment.在JournalManager的实现中,这是一个写edit log时会经过的一个正常的流程.
4.写edit日志操作.对于每个将要被写的edit log,写操作对象会将edit发送到集群中所有的JournalNode节点.一旦它接收到了quorum数量JournalNode节点的成功返回码,此次操作就认为是一次成功的写入.这个写操作对象包含了一个管道来写edit向每个JournalNode,所以任何一个临时写入较慢的节点将不会影响系统整体的吞吐量以及延时.
如果一个JournalNode节点接收edit失败了,或者由于待写入edit的队列长度超过配置的最大值导致回应的速度非常慢的时候,这时JournalNode将会被标记为outOfSync,并且不再用来写入当前的log segment.只要quorum数量的JournalNode还处于活跃状态,这就不是一个问题.早先的落后的节点将会在下一轮的写edit中进行重试并追上这些edit.
- 5.确认log segment,在现有的实现逻辑中,QJM可以通过一次对JournalNode的RPC调用来确认一个log segment.当它接收到来自quorum数量JournalNodes的确认信息,则这个log则被认为是已经确认好的状态,下一次写log操作也可以开始了.
实现
本次整体的设计将易测性作为最为关注的一点.为了达到此目标,所有的组件应该以满足后面的2种方式被构建:1.系统可以在运行的时候主动注入错误.2.系统可以运行在同一个jvm中.额外在可能的情况下,它应该能够尽量地在不真实写入磁盘的情况下完成核心算法的测试.这样我们就可以启动压力测试来做更快速以及更大吞吐量的测试.
本篇文档主要定位于描述主要的组件,但是不去深入地讲具体过多的细节.可以阅读社区JIRA上提供的patch来查看更多的实现细节.单元测试的代码同样能够展示一些使用的例子.
Quorum的实现
Quorum部分的代码大量的使用了Guava的ListenableFuture类来处理异步的操作.这个公共类提供了一种简单的方式对将要提交到ExecutorService的异步调用进行回调的注册和异常处理.而且,这些future对象能够很容易的整合入更加复杂的抽象场景中.
AsyncLogger
这个接口用ListenableFutures封装了一个远程的JournalNode,借此实现了RPC的异步调用过程.举例来说,方法void finalizeLogSegment(long startId, long endId)将会被封装为ListenableFuture finalizeLogSegment(long startId, long endId).
这个接口的具体实现类为IPCLoggerChannel.这个类简单低封装了一个Hadoop RPC的代理.每次调用将会被提交到单线程ExecutorService中,并且那个调用将会被一个ListenableFuture进行封装.这个类是非常模板化的包装代码,尽管它额外在每个RPC消息中增加写操作对象类的epoch值.
QuorumCall
这是一类包装了ListenableFuture列表实例对象以及允许一个调用方等待quorum个数量的回复(许多异常,或者一个超时)的这样一个通用类.比如说,一个调用方可能会选择等待以下回复消息中的一种:
- 1.绝大多数节点返回成功的回复.
- 2.任何一个节点返回的一个异常.
- 3.等待20s的执行时间.
上述场景不管哪个先满足了都会导致等待的返回.
AsyncLoggerSet
这个类封装了一个AsyncLoggers的集合来做quorum次的调用.比如说,它包含了很多公共的方法来创建QuorumCall对象实例,以此进行log edit,创建新的log segment等操作.这个类同样包含了一些公共的代码用来等待quorum数量的返回信息.
JournalManager的实现
QuorumJournalManager类主要是用来配置quorum,开始segment和确认segment操作的.对于每个segment,它会创建一个QuorumOutputStream实例对象.这个对象类主要负责发送edit给所有JournalNode中的其中quorum个.
JournalNode
JournalNode的用途是非常直接的,它简单监听了一个IPC协议并暴露一个正确的做法.可以查看TestJournalNode类中的单元测试例子,那里具体显示了其中的一些操作方法.
Metrics度量统计
本次设计将会包含许多统计度量指标,分别包括客户端的和服务端的.这些指标包括:
- 落后统计.对于每个JournalNode,跟踪此节点相比于quorum值已经落后了多少.此落后值包括了时间值和事务数.
- 延时统计.此延时值包括客户端写edit时的RPC调用的往返延时,以及调用写磁盘相关方法fsync()时的延时.客户端的统计指标包含2种类型,一个只包含RPC自身,另外一个则是包括进入队列的延时(当队列开始变得拥堵的时候).
- 队列大小.因为客户端在队列中会不断积累数据,所以这需要提供一些指标能够观察里面队列的长度,包括原始数据的大小以及事务的数量.
全部统计指标类型可以在IPCLoggerChannelMetrics.java和JournalMetrics.java文件中进行查阅.
参考资料
1.https://issues.apache.org/jira/secure/attachment/12547598/qjournal-design.pdf
2.百度百科.Quorum
3.维基百科.NAS
4.https://issues.apache.org/jira/browse/HDFS-1623
5.https://issues.apache.org/jira/browse/HDFS-3077