前言
在分布式系统中,根据不同的运行情况进行服务配置项的更新修改,重启是一件司空见惯的事情了。但是如果说需要重启的服务所需要的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
来源:oschina
链接:https://my.oschina.net/u/4314826/blog/4479760