从HDFS观察者NameNode实现一致性读取

目的

本指南概述了HDFS Observer NameNode功能,以及如何在典型的高可用性集群中配置/安装它。如需了解详细的技术设计概述,请查阅HDFS-12943附带的文档。

背景

在启用高可用性(HA)的HDFS集群中(更多信息请参阅HDFSHighAvailabilityWithQJM),存在一个活跃的NameNode和一个或多个备用NameNode。活跃的NameNode负责处理所有客户端请求,而备用NameNode则通过跟踪JournalNodes的编辑日志来保持命名空间的最新信息,并通过接收所有DataNodes的块报告来维护块位置信息。这种架构的一个缺点是活跃的NameNode可能成为单一瓶颈,并在客户端请求过多时过载,特别是在繁忙的集群中。

HDFS Observer NameNode的一致性读取功能通过引入一种名为Observer NameNode的新型NameNode来解决上述问题。与Standby NameNode类似,Observer NameNode会保持命名空间和块位置信息的最新状态。此外,它还能像Active NameNode一样提供一致性读取服务。由于在典型环境中读取请求占大多数,这有助于均衡NameNode流量负载并提升整体吞吐量。

架构

在新架构中,一个高可用集群可以包含处于三种不同状态的namenode:活跃(active)、备用(standby)和观察者(observer)。状态转换可以在活跃与备用之间、备用与观察者之间发生,但不能直接在活跃与观察者之间转换。

为了确保单个客户端内的读写一致性,RPC头中引入了一个状态ID(在NameNode内部使用事务ID实现)。当客户端通过Active NameNode执行写入操作时,它会使用来自NameNode的最新事务ID更新其状态ID。在执行后续读取时,客户端将此状态ID传递给Observer NameNode,后者将检查自身的事务ID,并确保自身事务ID已赶上请求的状态ID后才会处理该读取请求。这保证了单个客户端的"读取自身写入"语义。关于面对带外通信时如何维护多个客户端间一致性的讨论,请参阅下文"维护客户端一致性"章节。

编辑日志追踪对Observer NameNode至关重要,因为它直接影响事务在Active NameNode中应用与在Observer NameNode中应用之间的延迟。新引入的"快速路径编辑追踪"机制可显著降低这种延迟。该机制基于现有的进行中编辑日志追踪功能构建,并进行了多项改进,例如用基于RPC的追踪替代HTTP、在JournalNode上实现内存缓存等。更多细节请参阅HDFS-13150附带的设计文档。

还引入了新的客户端代理提供程序。ObserverReadProxyProvider继承自现有的ConfiguredFailoverProxyProvider,应使用它来替代后者以实现从Observer NameNode读取数据的功能。当提交客户端读取请求时,该代理提供程序会首先尝试集群中可用的每个Observer NameNode,仅当所有Observer NameNode都失败时才会回退到Active NameNode。类似地,ObserverReadProxyProviderWithIPFailover被引入以在IP故障转移设置中替代IPFailoverProxyProvider。

维护客户端一致性

如上所述,客户端'foo'在每次向Active NameNode发起请求时都会更新其状态ID,这包括所有写入操作。任何指向Observer NameNode的请求都将等待,直到Observer观察到该事务ID,从而确保客户端能够读取自身所有的写入内容。然而,如果'foo'通过带外(即非HDFS)消息通知客户端'bar'某个写入操作已完成,那么'bar'后续的读取可能无法看到'foo'最近的写入。为防止这种不一致行为,新增了一个msync()(即"元数据同步")命令。当客户端调用msync()时,会向Active NameNode更新其状态ID——这是一个非常轻量级的操作——从而保证后续读取在msync()调用时点之前的一致性。因此只要'bar'在执行读取前调用msync(),就能确保看到'foo'所做的写入。

要使用msync(),应用程序不一定需要进行任何代码更改。在启动时,客户端会在对Observer执行任何读取操作之前自动调用msync(),以便客户端初始化之前执行的任何写入操作都可见。此外,ObserverReadProxyProvider支持可配置的"auto-msync"模式,该模式会以可配置的时间间隔自动执行msync(),以防止客户端看到超过时间限制的陈旧数据。由于每次刷新都需要向Active NameNode发起RPC调用,这会带来一定的开销,因此默认情况下该功能是禁用的。

部署

配置

为了确保从Observer NameNode读取数据的一致性,您需要在hdfs-site.xml中添加以下配置:

  • dfs.namenode.state.context.enabled - 用于启用NameNode维护和更新服务器状态及ID的功能。

这将导致NameNode创建对齐上下文实例,用于跟踪当前服务器状态ID。服务器状态ID将被带回客户端。默认情况下此功能处于禁用状态,以优化Observer读取场景的性能。但对于Observer NameNode功能,必须启用此选项

    <property>
       <name>dfs.namenode.state.context.enabled</name>
       <value>true</value>
    </property>
  • dfs.ha.tail-edits.in-progress - 用于在正在进行的编辑日志上启用快速尾部追踪。

这通过处理中的编辑日志以及其他机制(如基于RPC的编辑日志获取、JournalNodes中的内存缓存等)实现了快速的编辑日志尾部读取。该功能默认关闭,但对于Observer NameNode特性必须启用

    <property>
      <name>dfs.ha.tail-edits.in-progress</name>
      <value>true</value>
    </property>
  • dfs.ha.tail-edits.period - Standby/Observer NameNodes从JournalNodes获取编辑日志的频率。

这决定了Observer NameNode相对于Active的陈旧程度。如果数值过大,RPC时间将会增加,因为客户端请求会在RPC队列中等待更长时间,直到Observer跟踪编辑日志并追上Active的最新状态。默认值为1分钟。强烈建议将此值配置为更低的数值。同时建议在使用较低值时启用退避机制;详情请见下文。

    <property>
      <name>dfs.ha.tail-edits.period</name>
      <value>0ms</value>
    </property>
  • dfs.ha.tail-edits.period.backoff-max - 备用/观察者NameNode在追踪编辑日志时是否应执行退避机制。

这决定了Standby/Observer智能体在尝试从JournalNodes尾部获取编辑日志但发现没有可用编辑时的行为。当编辑日志尾部获取周期设置得很短但集群负载不高时,这种情况很常见。若不配置此参数,这种情况会导致Standby/Observer智能体持续尝试读取编辑日志(尽管没有可用日志),从而造成高资源占用。启用此配置后,当编辑日志尾部获取尝试返回0条编辑时,系统将执行指数退避策略。此配置指定了两次编辑日志尾部获取尝试之间的最大等待时间。

    <property>
      <name>dfs.ha.tail-edits.period.backoff-max</name>
      <value>10s</value>
    </property>
  • dfs.journalnode.edit-cache-size.bytes - JournalNodes上的内存缓存大小,以字节为单位。

这是JournalNode端用于存储编辑操作的内存缓存大小,单位为字节。该缓存用于通过基于RPC的尾部读取方式提供编辑服务。仅当dfs.ha.tail-edits.in-progress选项启用时生效。

    <property>
      <name>dfs.journalnode.edit-cache-size.bytes</name>
      <value>1048576</value>
    </property>
  • dfs.journalnode.edit-cache-size.fraction - 该分数表示JVM最大内存的比例。

用于计算JournalNode内存中保留的编辑缓存大小。此配置是dfs.journalnode.edit-cache-size.bytes的替代方案。它用于通过基于RPC的机制为尾部编辑提供服务,并且仅在dfs.ha.tail-edits.in-progress为true时启用。事务大小不一,但平均约为200字节,因此默认的1MB可以存储约5000个事务。因此我们可以根据最大内存配置一个合理的值。推荐值小于0.9。如果我们设置了dfs.journalnode.edit-cache-size.bytes,此参数将不会生效。

    <property>
      <name>dfs.journalnode.edit-cache-size.fraction</name>
      <value>0.5f</value>
    </property>
  • dfs.namenode.accesstime.precision - 是否启用HDFS文件的访问时间记录。

强烈建议禁用此配置。如果启用,这将使getBlockLocations调用转变为写入调用,因为它需要持有写入锁来更新已打开文件的时间。因此,该请求将在所有观察者NameNode上失败,并最终回退到活动节点。结果会导致RPC性能下降。

    <property>
      <name>dfs.namenode.accesstime.precision</name>
      <value>0</value>
    </property>

新增管理命令

引入了一个新的HA管理命令,用于将备用NameNode转换为观察者状态:

haadmin -transitionToObserver

注意,此操作只能在备用名称节点(Standby NameNode)上执行。如果在活动名称节点(Active NameNode)上调用此操作,将会抛出异常。

类似地,现有的transitionToStandby也可以在Observer NameNode上运行,将其转换为备用状态。

注意:Observer NameNode参与故障转移的功能尚未实现。因此,如下一节所述,您应仅使用transitionToObserver来启动观察者。可以在Observer NameNode上启用ZKFC,但当NameNode处于Observer状态时ZKFC不会执行任何操作。在NameNode转换为standby状态后,ZKFC才会参与Active的选举。

部署详情

要启用观察者支持,首先需要一个支持高可用性(HA)的HDFS集群,且包含两个以上的名称节点。然后,您需要将备用名称节点(Standby NameNode)转换为观察者状态。最小配置是在集群中运行3个名称节点:一个活跃节点、一个备用节点和一个观察者节点。对于大型HDFS集群,我们建议根据读取请求的强度和HA需求运行两个或更多观察者节点。

请注意,当前Observer NameNode在启用自动故障转移时并未完全集成。如果开启了dfs.ha.automatic-failover.enabled参数,在Observer NameNode上运行ZKFC的唯一好处是:当您将NameNode转换为Standby状态后,它会自动参与Active节点的选举。如果不需要此功能,可以在Observer NameNode上禁用ZKFC。此外,您还需要在transitionToObserver命令中添加forcemanual标志:

haadmin -transitionToObserver -forcemanual

未来,这一限制将被取消。

客户端配置

希望使用Observer NameNode进行读取访问的客户端可以在客户端的hdfs-site.xml配置文件中指定ObserverReadProxyProvider类作为代理提供程序实现:

<property>
    <name>dfs.client.failover.proxy.provider.<nameservice></name>
    <value>org.apache.hadoop.hdfs.server.namenode.ha.ObserverReadProxyProvider</value>
</property>

不希望使用Observer NameNode的客户端仍可使用现有的ConfiguredFailoverProxyProvider,且不会看到任何行为变化。

希望使用"auto-msync"功能的客户端应调整以下配置。这将指定一个时间段,如果在此期间客户端的状态ID未从Active NameNode更新,则将自动执行msync()。如果将此值指定为0,则将在每次读取操作之前执行msync()。如果这是一个正的时间段,则每当请求读取操作且超过该时间段未联系Active时,将执行msync()。如果此值为负数(默认值),则不会执行自动msync()

<property>
    <name>dfs.client.failover.observer.auto-msync-period.<nameservice></name>
    <value>500ms</value>
</property>