Observer Node [SBN-READ] 原理及实现分析

概述

为了提升集群的吞吐、减轻Active Name负载压力,社区提出了Observer Read的设计。Observer Read是尽可能的将读请求发送到新加入集群的Observer节点来达到提升集群吞吐的目的。但是节点间元数据的同步是有一定有延时的,所以对于Observer读会存在数据一致性的问题。社区提出了元数据的一致性模型,解决了元数据同步的问题,基于此我们看一下社区的处理方式及对应的代码实现。

背景

HA架构中的HDFS使用Standby NameNode来作为Active NameNode的一个热备份,在故障转换时可以快速接管Client的请求。Standby NameNode与Active NameNode通过JournalNode来进行元数据的同步。所以Standby NameNode与Active NameNode是一摸一样的,因此Standby NameNode提供了一个绕开Active NameNode来读取数据的机会。通过Standby NameNode来读取数据会大大降低Active NameNode的压力并且也会极大的提升集群的吞吐。为了区分原有架构中Standby NameNode(不会提供读操作)对于能够提供读操作的Standby NameNode称之为Observer Node.

原理

在现有的HDFS架构中加入Observer节点,Observer节点通过JournalNode进行EditsLog的消费。然后客户端在获取到Active节点的TXID时将读请求发送到Observer节点来达到读写分流的目的,这样去减少Active NameNode的压力。

注:获取Active NameNode的TXID不会加NameNode的全局锁

Observer Read存在问题

Active NameNode与Observer节点的元数据同步使用JournalNode来完成,Observer读取元数据必然会存在一定的延迟,所以对于一些新写入的数据Observer可能无法及时同步导致用户在读取新数据报错。所以实现Observer Read的主要挑战是解决主备之间的元数据的一致性问题。

一致性问题的主要解决思路

元数据一致性问题主要的解决思路是客户端访问Observer时带上客户端已知的Active NameNode的TXID。当客户端访问Observer时,Observer消费的TXID能够达到客户端已知主节点的TXID即可。总而言之:客户端需要尽可能的知道最新的TXID。例如:某一时刻Client1访问主节点,访问主节点时主节点生成的TXID为1000,此时如果Client1要换成访问Observer,此时只要Observer消费的TXID也要达到1000就可以。解决此问题的主要方式是Client要知道访问主节点时主节点的TXID是多少,然后再访问Observer。

一致性模型的实现

Client端

主要思路

获取Active的TXID,并将Active的TXID携带到Observer进行元数据一致性的判断。 获取Active的TXID有三种方式:

  1. 通过ClientProtocol新加msync()方法获取Active节点TXID(客户端初始化时会调用,然后以后可以周期调用此方法通过Active来获取,周期可配。默认情况下是每次读之前都调用msync())。

  2. 客户端在进行写操作的时候直接更新客户端记录的Active的TXID(写操作TXID必然要增加,要保证客户端读到新写的数据,写完必须更新TXID为最新)

  3. 访问Observer时,Observer已知的TXID大于客户端,更新为Observer的TXID。

代码实现

为了完成获取TXID的功能,Client端主要添加了org.apache.hadoop.hdfs.server.namenode.ha.ObserverReadProxyProviderorg.apache.hadoop.hdfs.ClientGSIContext等类并修改了ipc.client中相关的逻辑。允许使用Observer进行读操作的时候,客户端需要配置dfs.client.failover.proxy.provider.nameservice=org.apache.hadoop.hdfs.server.namenode.ha.ObserverReadProxyProviderObserverReadProxyProvider类封装了客户端获取主节点的TXID以及进行Observer Read的实现细节。对应的ClientGSIContext类则主要是用于对客户端记录的TXID进行更新操作。这里主要介绍ObserverReadProxyProvider的实现。

ObserverReadProxyProvider实现

在ObserverReadProxyProvider初始化的时候,会依次初始化以下几个对象:

类名变量名作用
ConfiguredFailoverProxyProviderfailoverProxy用于寻找主节点调用msync()进行TXID的获取、Observer读失败最终退化为Active Read
ClientGSIContextalignmentContext用于记录、更新客户端的TXID
ProxyInfocombinedProxyDFSClient使用的ClientProtocol的代理,包含配置的所有NN,并使用ObserverReadInvocationHandler进行实际逻辑的处理

当DFSClient拿到ClientProtocol的代理对象的时候会传入对应的handler对象,然后调用ClientProtocol提供的方法,通过代理会使用对应Handler的invoke进行处理,当使用ObserverReadProxyProvider时会使用ObserverReadInvocationHandler.invoke来处理对于ClientProtocol方法的处理。客户端Observer Read的主要逻辑也被包含在了ObserverReadInvocationHandler.invoke中。其主要逻辑如下:

在这里我们了解,使用observer进行读取的时候会优先通过ConfiguredFailoverProxyProvider寻找主节点然后调用msync()方法进行TXID的的获取,获取完成后再将操作转移到observer节点。获取observer节点的是通过getHAServiceState方法获取节点的状态然后找到状态为OBSERVER的节点进行访问。对于写操作都是直接与主节点进行交互,交互的同时会更新客户端自己的TXID。

更新TXID,在客户端启动的时候会更新。后面如果配了自动更新时间间隔会在达到更新间隔后更新(**默认是每次读之前都通过msync进行同步(这样保证获取到最新的操作)**),或者在写操作时更新。

observer如果都尝试完毕后都无法完成操作,最终会退化到active read。

我们可以看到在ObserverReadInvocationHandler.invoke()方法中没有对TXID进行更新的操作?那何时会更新客户端的记录的TXID呢?

客户端TXID更新

客户端会通过序列引擎来确定序列化方式,并在创建序列化引擎的时候创建具体与NameNode地址进行通信的的对象即ipc.Client。当使用ObserverReadProxyProvider时,会将创建与NameNode链接的代理对象的工厂类传入客户端的ClientGSIContext对象,并在创建与NN具体通信的代理对象时会将ClientGSIContext对象传入对应的Invoker对象中.在接受到返回值时会将当server端的TXID更新到ClientGSIContext中,这样让客户端记录主节点的TXID。

问题:Observer中访问Active NameNode与Observer都会更新txid。那么用户先访问了Observer然后又访问了active,但是访问Active的先返回了,后续如果访问Observer返回的时候直接更新TXID就会有问题,那怎么解决的呢?

社区使用LongAccumulator类来记录TXID,并在创建L、ongAccumulator对象的时候传入Math.max()算数运算符,在更新的时候使用LongAccumulator.accumulate()方法来更新便可以保证用户获取到的都是最新的TXID。访问Observer也可以更新TXID也保证了客户端的TXID在Observer消费TXID大于客户端记录TXID以及客户端没有调用msync()或者写操作的时候保持尽可能的新。

Server端

主要思路

在Server端的新加要给HA的状态HAServiceState.OBSERVER来允许读操作。其他的更改主要是在ipc.Server类中。主要的思路是将客户端的C-TXID与当前NN的N-TXID对比并进行处理。在Server端对TXID记录是由GlobalStateIdContext类对象,此对象与客户端ClientGSIContext都是实现了AlignmentContext接口,但是GlobalStateIdContext类对象会通过fsnamesystem获取NN的TXID,并判断客户端传入的TXID。此对象也会在NN启动时传入ipc.server对象中。

  1. 如果C-TXID<N-TXID(NN已经追上),直接由Handler执行

  2. 如果C-TXID>N-TXID&&NN is far behind,抛出RetriableException由客户端重试(如果Server消费落后的TXID耗费时间会超过客户端连接最长无请求时间)

  3. 如果C-TXID>N-TXID&&NN is not far behind, reEnQueue 等待NN追上。

代码实现

Server.Connection#processRpcRequest

if(alignmentContext != null && call.rpcRequest != null &&
          (call.rpcRequest instanceof ProtobufRpcEngine2.RpcProtobufRequest)) { //Client的TXID的开始
        String methodName;
        String protoName;
        ProtobufRpcEngine2.RpcProtobufRequest req =
            (ProtobufRpcEngine2.RpcProtobufRequest) call.rpcRequest;
        try {
          methodName = req.getRequestHeader().getMethodName();
          protoName = req.getRequestHeader().getDeclaringClassProtocolName();
          if (alignmentContext.isCoordinatedCall(protoName, methodName)) {
            call.markCallCoordinated(true);//在connection中进行封装
            long stateId;
            stateId = alignmentContext.receiveRequestState(//判断当前Client的TXID与NN的TXID
                header, getMaxIdleTime());
            call.setClientStateId(stateId);
          }
        } catch (IOException ioe) {
          throw new RpcServerException("Processing RPC request caught ", ioe);
        }
      }

GlobalStateIdContext#receiveRequestState():判断客户端的TXID与当前NN的TXID

  public long receiveRequestState(RpcRequestHeaderProto header,
      long clientWaitTime) throws IOException {
    if (!header.hasStateId() &&
        HAServiceState.OBSERVER.equals(namesystem.getState())) {//客户但没配Observer并且达到obser的时候
      throw new StandbyException("Observer Node received request without "
          + "stateId. This mostly likely is because client is not configured "
          + "with " + ObserverReadProxyProvider.class.getSimpleName());
    }
    long serverStateId = getLastSeenStateId();//通过FSIMage获取最新的TXID
    long clientStateId = header.getStateId();
    FSNamesystem.LOG.trace("Client State ID= {} and Server State ID= {}",
        clientStateId, serverStateId);
​
    if (clientStateId > serverStateId &&
        HAServiceState.ACTIVE.equals(namesystem.getState())) {//大于active的时候设置为active的TXID。一般不会出现
      FSNamesystem.LOG.warn("The client stateId: {} is greater than "
          + "the server stateId: {} This is unexpected. "
          + "Resetting client stateId to server stateId",
          clientStateId, serverStateId);
      return serverStateId;
    }
    if (HAServiceState.OBSERVER.equals(namesystem.getState()) &&
        clientStateId - serverStateId >//如果observer的txid落后太多,会抛重试异常由客户端重试
        ESTIMATED_TRANSACTIONS_PER_SECOND
            * TimeUnit.MILLISECONDS.toSeconds(clientWaitTime)
            * ESTIMATED_SERVER_TIME_MULTIPLIER) {
      throw new RetriableException(//根据时间来判断的
          "Observer Node is too far behind: serverStateId = "
              + serverStateId + " clientStateId = " + clientStateId);
    }
    return clientStateId;
  }

Server.Handler#run()

   call = callQueue.take(); // pop the queue; maybe blocked here
          startTimeNanos = Time.monotonicNowNanos();
          if (alignmentContext != null && call.isCallCoordinated() &&
              call.getClientStateId() > alignmentContext.getLastSeenStateId()) { //协作的操作可以重新放回call队列中
            /*
             * The call processing should be postponed until the client call's
             * state id is aligned (<=) with the server state id.
            // Re-queue the call and continue
            */
            requeueCall(call);//重新放到callqueue中
            continue;
          }

流程综述

HDFS Observer流程综述图:

JN的优化

JournalNode对于所有成功事务都需要持久化到磁盘当中,当Standby读取EditsLog的时候JN需要大量的磁盘操作。为了加速对于EditsLog的回访,社区对于新写的EditsLog加入了缓存,此时Standby可以通过RPC消费缓存中的EditsLog,这样会减少大量的I/O操作,以及减少读操作的等待TXID回放时间。具体实现请参考:Allow SbNN to tail in-progress edits from JN via RPC

Observer failover

Observer NameNode参与故障转移的功能尚未实现。 因此,仅应使用haadmin -transitionToObserver -forcemanual来启动Observer并将其放置在ZooKeeper控制的故障转移组之外。 因此Observer NameNode的主机不能运行ZKFC。

主要配置

<property>
    <name>dfs.client.failover.proxy.provider.nameservice</name>
    <value>org.apache.hadoop.hdfs.server.namenode.ha.ObserverReadProxyProvider</value>
    <description>希望使用Observer NameNode进行读取访问的客户端可以在客户端的hdfs-site.xml配置文件中设置提供远程代理对象的实现类</description>
</property>
    
<property>
    <name>hadoop.rpc.socket.factory.class.default<nameservice></name>
    <value>org.apache.hadoop.net.StandardSocketFactory</value>
    <description>连接NameNode使用的socket工厂类</description>
</property>
    
 <property>
    <name>dfs.namenode.state.context.enabled<nameservice></name>
    <value>false</value>
    <description>默认false,</description>
</property>   
     
 <property>
     <name>dfs.client.failover.observer.probe.retry.period</name>
      <value> 60 * 10 * 1000</value>
      <description>默认十分钟</description>
 </property> 
     
  <property>   
     <name>dfs.client.failover.observer.auto-msync-period.nnhost</name>
      <value>-1</value>
      <description>自动获取TXID的间隔,默认客户端启动后不会自动获取TXID</description>
    </property>    

参考

(jira: HDFS-12943) Consistent Reads from Standby Node

(jira: HDFS-13150) Allow SbNN to tail in-progress edits from JN via RPC

(CSDN: blog) HDFS Standby NameNode Read功能剖析

(apache: doc) Consistent Reads from HDFS Observer NameNode

(jira: HDFS-13182) Allow Observer to participate in NameNode failover

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值