Hadoop服务配置热替换框架的设计实现

心不动则不痛 提交于 2020-10-02 10:28:05

前言


在分布式系统中,根据不同的运行情况进行服务配置项的更新修改,重启是一件司空见惯的事情了。但是如果说需要重启的服务所需要的cost非常高的时候,配置更新可能就不能做出频繁非常高的操作行为了。比如某些分布式存储系统比如HDFS NameNode重启一次,要load元数据这样的过程,要花费小时级别的启动时间,当其内部存储了亿级别量级的文件数的时候。那很显然对于这种高cost重启的服务来说,我们不能每次依赖重启做快速的配置更新,使得系统服务能使用新的配置值进行服务。于是一个新的名词在这里诞生了:服务的配置热替换更新。简单理解即我们可以通过RPC命令来动态地更改服务内部加载的某项配置值,然后让其使用新的配置值生效运行。本文笔者来聊聊Hadoop内部是如何实现了这么一套配置热替换更新的框架实现的。

服务热替换更新需要解决的问题点


要实现服务配置的热替换更新,我们首选需要知道有哪些主要的问题点,需要我们去考虑到。

第一点,如何让服务能够感知到那些“更新”了的配置。

这里一般有下面两种做法:
1)以命令行参数的形式,传入需要动态更新的配置key以及对应的value。
2)修改服务本地配置文件,然后触发一个动态刷config的命令到服务。

上述方案第二种比第一种更好一些,因为第一种命令行执行完后,其实配置并没有落到本地配置中去,只在服务内存里改了,容易造成下次服务重启配置被倒刷回去的情况。

第二点,服务如何将待更新的配置值安全地更新并生效。

这里着重注意的点是五个字:安全的更新。安全的更新意味着什么呢?配置值更新的原子性,因为我们要假设存在并发更新配置值的情况。当然了预防的办法也有很多,通过写锁保护或者用Atomic类型或者使用volatile关键词。

后面在实际Hadoop代码实现中,我们可以再来看具体的方法使用。

第三点,服务哪些配置能够进行热替换更新。

这点考虑的是我们打算热替换更新哪些具体类型的配置值。
这里大体可以分为以下几大类:

1)单纯的Int,long类型的值变量,此类配置比如heartbeat间隔时间,网络带宽值,线程数等等。
2)String值类型,用的比较多的是路径名。
3)服务内部的对象类型更新,比如某些instance的policy重新生成更新。

总结归纳一句话,只要服务内部能够原子的更新好配置值,并且同时能够平滑处理配置更新前后的逻辑处理。那么这一套配置热替换更新就没有什么大的问题了。

OK,下面我们以Hadoop内部实现的配置热替换框架为例,来做进一步地了解。

Hadoop服务热替换更新配置框架代码实现


Hadoop内部服务同样存在着热替换更新配置的需求点,在社区JIRA HADOOP-7001中实现了这套热配置更新的代码实现。

首先,它内部定义了一个可重配置的基类。

/**
 * Utility base class for implementing the Reconfigurable interface.
 *
 * Subclasses should override reconfigurePropertyImpl to change individual
 * properties and getReconfigurableProperties to get all properties that
 * can be changed at run time.
 */
public abstract class ReconfigurableBase 
  extends Configured implements Reconfigurable {
  
  private static final Logger LOG =
      LoggerFactory.getLogger(ReconfigurableBase.class);
  // Use for testing purpose.
  private ReconfigurationUtil reconfigurationUtil = new ReconfigurationUtil();

  /** 后台更新配置线程. */
  private Thread reconfigThread = null;
  private volatile boolean shouldRun = true;
  private Object reconfigLock = new Object();
  ...
}

此基类内部包含一个主要用来做后台配置更新的线程。下面我们主要进入上面reconfigThread的内部逻辑中。

  /**
   * A background thread to apply configuration changes.
   */
  private static class ReconfigurationThread extends Thread {
    // 带有conf信息的类,并且conf是此类中生效的
    private ReconfigurableBase parent;

    ReconfigurationThread(ReconfigurableBase base) {
      this.parent = base;
    }

    // See {@link ReconfigurationServlet#applyChanges}
    public void run() {
      LOG.info("Starting reconfiguration task.");
      // 从parent类中获取当前内存的中的conf信息,即老的conf
      final Configuration oldConf = parent.getConf();
      // 再获取新的conf,此新的conf为从本地再load最新的conf
      final Configuration newConf = parent.getNewConf();
      // 比较新老conf值,获取更新过了的配置值
      final Collection<PropertyChange> changes =
          parent.getChangedProperties(newConf, oldConf);
      Map<PropertyChange, Optional<String>> results = Maps.newHashMap();
      ConfigRedactor oldRedactor = new ConfigRedactor(oldConf);
      ConfigRedactor newRedactor = new ConfigRedactor(newConf);
      
      for (PropertyChange change : changes) {
        String errorMessage = null;
        String oldValRedacted = oldRedactor.redact(change.prop, change.oldVal);
        String newValRedacted = newRedactor.redact(change.prop, change.newVal);
        if (!parent.isPropertyReconfigurable(change.prop)) {
          LOG.info(String.format(
              "Property %s is not configurable: old value: %s, new value: %s",
              change.prop,
              oldValRedacted,
              newValRedacted));
          continue;
        }
        LOG.info("Change property: " + change.prop + " from \""
            + ((change.oldVal == null) ? "<default>" : oldValRedacted)
            + "\" to \""
            + ((change.newVal == null) ? "<default>" : newValRedacted)
            + "\".");
        try {
          // 遍历需要更新的配置值,然后调用parent类的重配置方法,触发热替换更新操作
          String effectiveValue =
              parent.reconfigurePropertyImpl(change.prop, change.newVal);
          if (change.newVal != null) {
            //如果上述热替换操作成功,然后更新当前内存中的conf里对应的配置值
            oldConf.set(change.prop, effectiveValue);
          } else {
            oldConf.unset(change.prop);
          }
        } catch (ReconfigurationException e) {
          Throwable cause = e.getCause();
          errorMessage = cause == null ? e.getMessage() : cause.getMessage();
        }
        results.put(change, Optional.ofNullable(errorMessage));
      }

      ...
    }
  }

从上述代码可见,上面的ReconfigurationThread线程主要是在执行ReconfigurableBase的一些方法。

我们以HDFS NameNode的配置热更新替换为例。
首先,NameNode是继承了这个基类的,然后我们能够看到NameNode对ReconfigurableBase部分接口方法的实现如下,

public class NameNode extends ReconfigurableBase implements
    NameNodeStatusMXBean, TokenVerifier<DelegationTokenIdentifier> {

  protected String reconfigurePropertyImpl(String property, String newVal)
      throws ReconfigurationException {
    final DatanodeManager datanodeManager = namesystem.getBlockManager()
        .getDatanodeManager();
    
    // NN热配置更新执行方法,
    if (property.equals(DFS_HEARTBEAT_INTERVAL_KEY)) {
      return reconfHeartbeatInterval(datanodeManager, property, newVal);
    } else if (property.equals(DFS_NAMENODE_HEARTBEAT_RECHECK_INTERVAL_KEY)) {
      return reconfHeartbeatRecheckInterval(datanodeManager, property, newVal);
    } else if (property.equals(FS_PROTECTED_DIRECTORIES)) {
      return reconfProtectedDirectories(newVal);
    } else if (property.equals(HADOOP_CALLER_CONTEXT_ENABLED_KEY)) {
      return reconfCallerContextEnabled(newVal);
    } else if (property.equals(ipcClientRPCBackoffEnable)) {
      return reconfigureIPCBackoffEnabled(newVal);
    } else if (property.equals(DFS_STORAGE_POLICY_SATISFIER_MODE_KEY)) {
      return reconfigureSPSModeEvent(newVal, property);
    } else if (property.equals(DFS_NAMENODE_REPLICATION_MAX_STREAMS_KEY)
        || property.equals(DFS_NAMENODE_REPLICATION_STREAMS_HARD_LIMIT_KEY)
        || property.equals(
            DFS_NAMENODE_REPLICATION_WORK_MULTIPLIER_PER_ITERATION)) {
      return reconfReplicationParameters(newVal, property);
    } else if (property.equals(DFS_BLOCK_REPLICATOR_CLASSNAME_KEY) || property
        .equals(DFS_BLOCK_PLACEMENT_EC_CLASSNAME_KEY)) {
      reconfBlockPlacementPolicy();
      return newVal;
    } else {
      throw new ReconfigurationException(property, newVal, getConf().get(
          property));
    }
  }
  
  @Override  // ReconfigurableBase
  protected Configuration getNewConf() {
   // 从NN本地load最新的配置
    return new HdfsConfiguration();
  }
}

在NN的热配置更新方法里,我们能够看到NN目前已经能够支持多个不同配置的热更新替换了。比如heartbeat心跳间隔方法是通过lock来做更新原子性的保证的。

  private String reconfHeartbeatInterval(final DatanodeManager datanodeManager,
      final String property, final String newVal)
      throws ReconfigurationException {
    namesystem.writeLock();
    try {
      if (newVal == null) {
        // set to default
        datanodeManager.setHeartbeatInterval(DFS_HEARTBEAT_INTERVAL_DEFAULT);
        return String.valueOf(DFS_HEARTBEAT_INTERVAL_DEFAULT);
      } else {
        long newInterval = getConf()
            .getTimeDurationHelper(DFS_HEARTBEAT_INTERVAL_KEY,
                newVal, TimeUnit.SECONDS);
        datanodeManager.setHeartbeatInterval(newInterval);
        return String.valueOf(datanodeManager.getHeartbeatInterval());
      }
    } catch (NumberFormatException nfe) {
      throw new ReconfigurationException(property, newVal, getConf().get(
          property), nfe);
    } finally {
      namesystem.writeUnlock();
      LOG.info("RECONFIGURE* changed heartbeatInterval to "
          + datanodeManager.getHeartbeatInterval());
    }
  }

另外一类是通过volatile保护的方式来做原子性的更新,

  /** for block replicas placement */
  private volatile BlockPlacementPolicies placementPolicies;
  
  private void reconfBlockPlacementPolicy() {
    getNamesystem().getBlockManager()
        .refreshBlockPlacementPolicy(getNewConf());
  }
 
   public void refreshBlockPlacementPolicy(Configuration conf) {
    BlockPlacementPolicies bpp =
        new BlockPlacementPolicies(conf, datanodeManager.getFSClusterStats(),
            datanodeManager.getNetworkTopology(),
            datanodeManager.getHost2DatanodeMap());
    placementPolicies = bpp;
  }

上面部分显示的配置的热更新替换,那么还有另外一半配置的外部传入NN是怎么做的呢?

HDFS在这边定义了另外一个protocol做reconfig的行为触发。

/**********************************************************************
 * ReconfigurationProtocol is used by HDFS admin to reload configuration
 * for NN/DN without restarting them.
 **********************************************************************/
@InterfaceAudience.Private
@InterfaceStability.Evolving
public interface ReconfigurationProtocol {

  long VERSIONID = 1L;

  /**
   * Asynchronously reload configuration on disk and apply changes.
   */
  @Idempotent
  void startReconfiguration() throws IOException;

  /**
   * Get the status of the previously issued reconfig task.
   * @see org.apache.hadoop.conf.ReconfigurationTaskStatus
   */
  @Idempotent
  ReconfigurationTaskStatus getReconfigurationStatus() throws IOException;

  /**
   * Get a list of allowed properties for reconfiguration.
   */
  @Idempotent
  List<String> listReconfigurableProperties() throws IOException;
}

当我们在本地改完配置,下一步就是调用上面startReconfiguration方法来触发服务配置的热替换更操作了。这里HDFS是把这个接口实现通过dfsadmin的方式开放了出来,

  int startReconfigurationDispatch(final String nodeType,
      final String address, final PrintStream out, final PrintStream err)
      throws IOException {
    if ("namenode".equals(nodeType)) {
      // 触发执行NameNode服务的startReconfiguration方法
      ReconfigurationProtocol reconfProxy = getNameNodeProxy(address);
      reconfProxy.startReconfiguration();
      return 0;
    } else if ("datanode".equals(nodeType)) {
      ClientDatanodeProtocol reconfProxy = getDataNodeProxy(address);
      reconfProxy.startReconfiguration();
      return 0;
    } else {
      System.err.println("Node type " + nodeType
          + " does not support reconfiguration.");
      return 1;
    }
  }

随后NameNode的startReconfiguration就被触发执行到了,即启动ReconfigurationThread线程,执行一次reload配置的行为。

  @Override // ReconfigurationProtocol
  public void startReconfiguration() throws IOException {
    checkNNStartup();
    String operationName = "startNamenodeReconfiguration";
    namesystem.checkSuperuserPrivilege(operationName);
    nn.startReconfigurationTask();
    namesystem.logAuditEvent(true, operationName, null);
  }
  /**
   * Start a reconfiguration task to reload configuration in background.
   */
  public void startReconfigurationTask() throws IOException {
    synchronized (reconfigLock) {
      if (!shouldRun) {
        String errorMessage = "The server is stopped.";
        LOG.warn(errorMessage);
        throw new IOException(errorMessage);
      }
      if (reconfigThread != null) {
        String errorMessage = "Another reconfiguration task is running.";
        LOG.warn(errorMessage);
        throw new IOException(errorMessage);
      }
      reconfigThread = new ReconfigurationThread(this);
      reconfigThread.setDaemon(true);
      reconfigThread.setName("Reconfiguration Task");
      reconfigThread.start();
      startTime = Time.now();
    }
  }

综上,整个配置热替换更新的过程还是比较清晰的,采用异步化的更新也能使得client端无须等待热替换更新的实质结束完成。

引用


[1].https://issues.apache.org/jira/browse/HADOOP-7001
[2].https://issues.apache.org/jira/browse/HDFS-9000

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!