HDFS Standby NameNode Read功能剖析

前言


HDFS有着一套十分成熟的HA的机制来保证其服务的高可用性。在HA模式下,分别对应有Active和Standby NameNode的服务。Active NameNode用于提供对外数据服务,而Standby NameNode则负责做checkpoint的工作以及随时准备接替变成Active NameNode的角色,假设说当前Active NameNode意外不可用的情况发生的话。其实,Standby NameNode日常的工作并不多,除了定期checkpoint和准实时地同步元数据信息外,它并不处理来自外部client发起的读写请求,所以Standby NameNode服务的一个负载是比较低的。当Active NameNode的服务压力越来越大的时候,那么是否我们可以让Standby的NameNode去分流一部分的读压力呢?Hadoop社区在很早的时候就已经提出过此设想并且实现了这个功能。本文笔者将结合部分代码来简单分析分析HDFS Standby Read的实现原理。

HDFS Standby Read的背景及功能要求


首先我们来说说HDFS Standby Read的背景及功能要求。在Active NameNode随着集群规模的不断扩张下,其服务压力将会越来越大的。对于这种情况下,我们一般的做法是通过组建HDFS Federation的方式来达到服务横向扩展的目标。但是这种方式并没有在NameNode本身的服务能力上做更进一步的挖掘优化,而HDFS Standby Read的功能就是在这块的一个大大的补强。

在Standby Read模式下,HDFS原有的写请求依然是被Active NameNode所处理。Standby服务只是支持了读操作的处理,所以这里不会涉及到NameNode主要逻辑上的大改。不过这里面最需要解决的问题是,Standby一致性读的问题。

我们知道Standby NameNode是通过读取JournalNode上的editlog,从而进行transaction的同步的。在Active NameNode写editlog到出去,再到Standby NameNode去读这这批editlog,中间是存在时间gap的。所以在实现Standby Read功能时,我们并不是简简单单地把读请求直接转向Standby NN就完事了,这里会涉及到transaction的等待同步问题。下面笔者会详细介绍这块社区是怎么做的。

Standby NameNode一致性读的控制实现


原理分析


鉴于上小节提到的Standby NameNode状态同步的问题,需要Standby NameNode达到client最近一次的txid后,才能允许其处理client的读请求操作。

上面这句话什么意思呢?对于client而言,在它发起RPC请求时,Active NameNode和Standby NameNode各自有自身当前的txid,且Active NameNode的txid肯定要大于Standby的txid。这里我们标记Active txid为ann txid,Standby为 snn txid。如果这时,client发起后续请求到Active服务,那没有什么数据延时的问题,Active一直都是最新的状态。但是假设我们想让Standby NameNode也能够处理client的请求,那么它至少得达到刚刚client发起RPC时刻起时Active NameNode的txid的状态,即snn txid也达到ann txid值。

此部分过程简单阐述如下所示:

1)Client发起RPC请求前获取到当前Active NameNode的txid值,这里我们叫做lastSeenTxid。
2)随后Client发起读请求到Standby NameNode,在此请求中会带上上步骤的lastSeenTxid的值。
3)Standby NameNode在处理上步骤的RPC请求时,会比较自身当前的txid是否已经达到client的lastSeenTxid值,如果已经达到,则正常处理这个请求,否则将请求重新插入RPC callqueu等待下次被处理。这里的请求重新进queue的操作对于client来说,意味着这个RPC call还没有处理结束。

为了避免Client可能出现长时间等待Standby NameNode达到lastSeenTxid状态的情况,社区在Standby NameNode editlog的同步上做了一部分改进,包括支持editlog_inprogress里的transaction读取以及editlog信息的内存读取等等。

后面笔者来结合实际代码,来对应分析上面的过程。

代码分析


社区在实现的过程里定义了2个类来存放Client和Server端自身能够“看到”的txid值。

Client对应的类叫做ClientGSIContext,Server端(即NameNode)的叫做GlobalStateIdContext。

我们先来说说Client端的这个类,ClientGSIContext。ClientGSIContext类内部维护有lastSeenStateId这个值,代码如下所示:

/**
 * Global State Id context for the client.
 * <p>
 * This is the client side implementation responsible for receiving
 * state alignment info from server(s).
 */
@InterfaceAudience.Private
@InterfaceStability.Evolving
public class ClientGSIContext implements AlignmentContext {
   
        

  private final LongAccumulator lastSeenStateId =
      new LongAccumulator(Math::max, Long.MIN_VALUE);
  ...
}

lastSeenStateId这个值的更新和获取主要发生在接收到RPC response阶段(更新当前的lastSeenStateId值)和RPC请求发送(设置当前的lastSeenStateId值)的时候,代码如下,还是在这个类的逻辑里。

  /**
   * Client接收到请求response时更新当前的lastSeenStateId值。
   */
  @Override
  public void receiveResponseState(RpcResponseHeaderProto header) {
   
        
    lastSeenStateId.accumulate(header.getStateId());
  }

  /**
   * Client发起请求时设置当前的lastSeenStateId值信息到RPC请求里。
   */
  @Override
  public void updateRequestState(RpcRequestHeaderProto.Builder header) {
   
        
    header.setStateId(lastSeenStateId.longValue());
  }

然后我们再来看Server端的GlobalStateIdContext是怎么处理的。首先是GlobalStateIdContext的类定义:

/**
 * This is the server side implementation responsible for passing
 * state alignment info to clients.
 */
@InterfaceAudience.Private
@InterfaceStability.Evolving
class GlobalStateIdContext implements AlignmentContext {
   
        
  /**
   * Estimated number of journal transactions a typical NameNode can execute
   * per second. The number is used to estimate how long a client's
   * RPC request will wait in the call queue before the Observer catches up
   * with its state id.
   */
  private static final long ESTIMATED_TRANSACTIONS_PER_SECOND = 10000L;

  /**
   * The client wait time on an RPC request is composed of
   * the server execution time plus the communication time.
   * This is an expected fraction of the total wait time spent on
   * server execution.
   */
  private static final float ESTIMATED_SERVER_TIME_MULTIPLIER = 0.8f;
  /** FSNamesystem用来获取当前最新的txid值 */
  private final FSNamesystem namesystem;
  private final HashSet<String> coordinatedMethods;
  ...
}

GlobalStateIdContext在处理client的RPC请求时,主要做下面两个事情:

  • 1)接受到RPC请求,从RPC 请求中提取client的lastSeenTxid,并且和自身最新的txid做比较。
  • 2)处理完RPC后,设置RPC response时,设置自身最新的txid到client的lastSeenTxid里,意为client此时已经see到一个更新的txid状态。

上面两部对应的操作方法如下所示:

  /**
   * Server端处理完RPC后,设置RPC response时,设置自身最新的txid到client的lastSeenTxid里。
   */
  @Override
  public void updateResponseState(RpcResponseHeaderProto.Builder header) {
   
        
    // Using getCorrectLastAppliedOrWrittenTxId will acquire the lock on
    // FSEditLog. This is needed so that ANN will return the correct state id
    // it currently has. But this may not be necessary for Observer, may want
    // revisit for optimization. Same goes to receiveRequestState.
    header.setStateId(getLastSeenStateId());
  }

  /**
   * Server端请求状态的判断处理逻辑。
   */
  @Override
  public long receiveRequestState(RpcRequestHeaderProto header,
      long clientWaitTime) throws IOException {
   
        
    if (!header.hasStateId() &&
        HAServiceState.OBSERVER.equals(namesystem.getState())) {
   
        
      // This could happen if client configured with non-observer proxy provider
      // (e.g., ConfiguredFailoverProxyProvider) is accessing a cluster with
      // observers. In this case, we should let the client failover to the
      // active node, rather than potentially serving stale result (client
      // stateId is 0 if not set).
      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();
    long clientStateId = header.getStateId();
    FSNamesystem.LOG.trace("Client State ID= {} and Server State ID= {}",
        clientStateId, serverStateId);

    if (clientStateId > serverStateId &&
        HAServiceState.ACTIVE.equals(namesystem.getState())) {
   
        
      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;
    }
    
    // 如果当前client的lastSeenTxid值远远大于当前server端的txid值,则抛出异常。
    // 如果是小于serverStateId或者在正常范围内,则继续处理。
    if (HAServiceState.OBSERVER.equals(namesystem.getState()) &&
        clientStateId - serverStateId >
        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;
  }

  // 获取自身当前最新的txid值
  @Override
  public long getLastSeenStateId() {
   
        
    // Should not need to call getCorrectLastAppliedOrWrittenTxId()
    // see HDFS-14822.
    return namesystem.getFSImage().getLastAppliedOrWrittenTxId();
  }

注意上面的receiveRequestState只是client请求进入rpc queue的一个前期验证处理,在后续Handler从rpc queue中获取这个call处理的时候,还会做一次client lastSeenTxid和server txid的比较。

  /** Handles queued calls . */
  private class Handler extends Thread {
   
        
    public Handler(int instanceNumber) {
   
        
      this.setDaemon(true);
      this.setName("IPC Server handler "+ instanceNumber +
          " on default port " + port);
    }

    @Override
    public void run() {
   
        
      LOG.debug(Thread.currentThread().getName() + ": starting");
      SERVER.set(Server.this);
      while (running) {
   
        
        TraceScope traceScope = null;
        Call call = null;
        long startTimeNanos = 0;
        // True iff the connection for this call has been dropped.
        // Set to true by default and update to false later if the connection
        // can be succesfully read.
        boolean connDropped = true;

        try {
   
        
          1)从call queue 中获取一个call进行处理
          call = callQueue.take(); // pop the queue; maybe blocked here
          startTimeNanos = Time.monotonicNowNanos();
          // 如果这个call是支持Standby Read,且其client seen txid大于server端txid,则执行此call的重新进queue操作,延迟这个call的处理,等待server端的txid reach到client 的txid值
          if (alignmentContext != null && call.isCallCoordinated() &&
              call.getClientStateId() > alignmentContext.getLastSeenStateId()) {
   
        
            /*
             * The call processing should be postponed until the client call's
             * state id is aligned (<=) with the server state id.

             * NOTE:
             * Inserting the call back to the queue can change the order of call
             * execution comparing to their original placement into the queue.
             * This is not a problem, because Hadoop RPC does not have any
             * constraints on ordering the incoming rpc requests.
             * In case of Observer, it handles only reads, which are
             * commutative.
             */
            // Re-queue the call and continue
            requeueCall(call);
            continue;
          }
          ...
  }
}

分析到这里,Standby Read的Server逻辑分析的差不多了,不过再回到刚刚上面的步骤里:

1)Client发起RPC请求前获取到当前Active NameNode的txid值,这里我们叫做lastSeenTxid。
2)随后Client发起读请求到Standby NameNode,在此请求中会带上上步骤的lastSeenTxid的值。


Client是如何做到 先发起请求到Active NameNode获取最新txid,然后随后向Standby NameNode发起后续read请求的,这里涉及到了2个RPC call。

社区实现了新的ProxyProvider类ObserverReadProxyProvider来封装了此部分的逻辑。在ObserverReadInvocationHandler的逻辑里,它会在每次发起读请求到Standby NameNode前,先行发送一次msync call到Active NameNode来同步Client端的ClientGSIContext里的lastSeenStateId(在Client 处理response方法里会调用到ClientGSIContext#receiveResponseState操作)。

此部分逻辑如下,ObserverReadProxyProvider类。

  private class ObserverReadInvocationHandler implements RpcInvocationHandler {
   
        

    @Override
    public Object invoke(Object proxy, final Method method, final Object[] args)
        throws Throwable {
   
        
      lastProxy = null;
      Object retVal;

      // 如果开启了Standby Read的功能并且,RPC call的请求方法是Read类型的
      if (observerReadEnabled && shouldFindObserver() && isRead(method)) {
   
        
        if (!msynced) {
   
        
          // An msync() must first be performed to ensure that this client is
          // up-to-date with the active's state. This will only be done once.
          initializeMsync();
        } else {
   
        
          // 在每次发起请求时,先执行一遍msync操作方法到Active NameNode,进行client lastSeemTxid的同步
          autoMsyncIfNecessary();
        }

        int failedObserverCount = 0;
        int activeCount = 0;
        int standbyCount = 0;
        int unreachableCount = 0;
        // 后续发起请求到Standby NameNode进行读请求的处理
        for (int i = 0; i < nameNodeProxies.size(); i++) {
   
        
          NNProxyInfo<T> current = getCurrentProxy();
          HAServiceState currState = current.getCachedState();
          if (currState != HAServiceState.OBSERVER) {
   
        
            if (currState == HAServiceState.ACTIVE) {
   
        
              activeCount++;
            } else if (currState == HAServiceState.STANDBY) {
   
        
              standbyCount++;
            } else if (currState == null) {
   
        
              unreachableCount++;
            }
            LOG.debug("Skipping proxy {} for {} because it is in state {}",
                current.proxyInfo, method.getName(),
                currState == null ? "unreachable" : currState);
            changeProxy(current);
            continue;
          }
          ...
      }
       
      // 其它非读类型的请求,还是访问Active NameNode
      LOG.debug("Using failoverProxy to service {}", method.getName());
      ProxyInfo<T> activeProxy = failoverProxy.getProxy();
      try {
   
        
        retVal = method.invoke(activeProxy.proxy, args);
      } catch (InvocationTargetException e) {
   
        
        // This exception will be handled by higher layers
        throw e.getCause();
      }
      // If this was reached, the request reached the active, so the
      // state is up-to-date with active and no further msync is needed.
      msynced = true;
      lastMsyncTimeMs = Time.monotonicNow();
      lastProxy = activeProxy;
      return retVal;
    }
}

流程分析图


结合上述代码逻辑以及过程分析,HDFS Standby Read功能的过程图如下所示:
在这里插入图片描述
上图中Observer NameNode是Standby Read feature中引入的一种新的角色,它本质上来说是更轻量级的Standby NameNode,它和原有Standby的主要区别是它不做checkpoint这类的操作。NameNode Observer和Standby的状态能够进行互相转化,但是Observer NameNode不能和Active NameNode进行直接的状态切换。

在HDFS Standby Read的实现中,还有一大半实现是在SNN快速读取editlog的优化里,这部分感兴趣的同学可阅读参考链接处。

参考链接


[1]. https://issues.apache.org/jira/browse/HDFS-12943 . Consistent Reads from Standby Node

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章