在中心化管理的存储系统中,当系统内的数据出现个别副本损坏的情况时,作为中心控制中心,它需要能够感知系统内的这个情况,并且能够快速的进行副本的拷贝恢复。我们姑且称此类服务为ReplicationManager,或者叫做类似于ReplicationMonitor的服务。此类服务在分布式存储系统中扮演的功能为定期检查系统内所有数据的副本情况,并进行必要的修复工作或者其它类似操作。本文笔者来聊聊Ozone系统中的这个服务,名称就叫做ReplicationManager。
提到ReplicationManager,首先要说的就是ReplicationManager的核心任务要求。
因为Ozone是基于Container做replication的,因此它的一大核心要求:
保证集群内的所有Container达到规定副本数的数量,不存在Container数少于或多于其设置副本数的数量。
基于这个大的原则前提,它还可以做额外的一些对Container副本数据的优化处理,这里包括但不仅限于:
1) Container副本的placement策略放置,保证数据更高的容错性。
2) 非健康Container副本的检查处理。
另外一点需要注意的是,做Replication的Container的状态必须是closed状态,不能是处于可进行写的open状态的Container。
下面我们结合具体代码逻辑,来了解下ReplicationManager的具体处理过程。
ReplicationManager在Ozone SCM内启动后,作为一个后台线程服务,其执行的run方法如下:
/**
* ReplicationMonitor thread runnable. This wakes up at configured
* interval and processes all the containers in the system.
*/
private synchronized void run() {
try {
while (running) {
final long start = Time.monotonicNow();
// 1.获取集群内所有Container的ID信息
final Set<ContainerID> containerIds =
containerManager.getContainerIDs();
// 2.遍历每个Container,检查其Replica情况是否达到预期的Replica数量
containerIds.forEach(this::processContainer);
LOG.info("Replication Monitor Thread took {} milliseconds for" +
" processing {} containers.", Time.monotonicNow() - start,
containerIds.size());
wait(conf.getInterval());
}
} catch (Throwable t) {
// When we get runtime exception, we should terminate SCM.
LOG.error("Exception in Replication Monitor Thread.", t);
ExitUtil.terminate(1, t);
}
}
我们可以看到,它在每个检查周期内是拿到所有ContainerID信息,然后逐个进行Container副本的判断处理的。
然后我们进入processContainer方法,这个方法里面是针对每个独立唯一的Container,进行副本的检查处理。
这部分逻辑如下:
private void processContainer(ContainerID id) {
lockManager.lock(id);
try {
final ContainerInfo container = containerManager.getContainer(id);
// 1). 获取当前汇报上来的最新副本信息
final Set<ContainerReplica> replicas = containerManager
.getContainerReplicas(container.containerID());
final LifeCycleState state = container.getState();
//..副本状态的逻辑处理...
// 2). 更新inflight的Replication信息,如果当前副本节点包含于inflight中时,
// 则进行对应ContainerId的inflight节点移除,意为此inflight的replication已成功
updateInflightAction(container, inflightReplication,
action -> replicas.stream()
.anyMatch(r -> r.getDatanodeDetails().equals(action.datanode)));
// 3). 更新inflight的replica删除信息,如果当前副本节点不存在于inflight中时,
// 则进行对应ContainerId的inflight节点移除,意为此inflight的删除已经生效
updateInflightAction(container, inflightDeletion,
action -> replicas.stream()
.noneMatch(r -> r.getDatanodeDetails().equals(action.datanode)));
/*
* 4). 进行Container删除逻辑的处理
*/
if (state == LifeCycleState.DELETING) {
handleContainerUnderDelete(container, replicas);
return;
}
// 5). 副本数是否正常的判断
if (isContainerHealthy(container, replicas)) {
// 5.1). 如果Container为空,则进行空Container的移除
if (isContainerEmpty(container, replicas)) {
deleteContainerReplicas(container, replicas);
}
return;
}
// 6). Container副本数量不足的情况处理
if (isContainerUnderReplicated(container, replicas)) {
handleUnderReplicatedContainer(container, replicas);
return;
}
// 7). Container过量副本数的判断处理
if (isContainerOverReplicated(container, replicas)) {
handleOverReplicatedContainer(container, replicas);
return;
}
// 8). 剩余unhealthy container的处理
handleUnstableContainer(container, replicas);
} catch (ContainerNotFoundException ex) {
LOG.warn("Missing container {}.", id);
} catch (Exception ex) {
LOG.warn("Process container {} error: ", id, ex);
} finally {
lockManager.unlock(id);
}
}
这里笔者分段解释上面的逻辑。
首先一开始,先根据ContainerId拿到当前系统汇报上的有效的Container副本信息,此即为当前的Container副本数。
final ContainerInfo container = containerManager.getContainer(id);
// 1). 获取当前汇报上来的最新副本信息
final Set<ContainerReplica> replicas = containerManager
.getContainerReplicas(container.containerID());
final LifeCycleState state = container.getState();
紧接着我们利用汇报上来的Container副本信息去更新inflight的结构信息。
// 2). 更新inflight的Replication信息,如果当前副本节点包含于inflight中时,
// 则进行对应ContainerId的inflight节点移除,意为此inflight的replication已成功
updateInflightAction(container, inflightReplication,
action -> replicas.stream()
.anyMatch(r -> r.getDatanodeDetails().equals(action.datanode)));
// 3). 更新inflight的replica删除信息,如果当前副本节点不存在于inflight中时,
// 则进行对应ContainerId的inflight节点移除,意为此inflight的删除已经生效
updateInflightAction(container, inflightDeletion,
action -> replicas.stream()
.noneMatch(r -> r.getDatanodeDetails().equals(action.datanode)));
这里有个问题,何为inflight的意思?inflight意指那些还在飘着的副本,就是还没有确认执行完的副本信息,包括正在做Replication但还没有确定完成的副本和正在被删除的副本但还没有确定完成的副本。这里通过当前最新汇报上来的副本信息,以此确定哪些inflight的信息是确定完成,然后可以被移除掉了。
当然还有一种情况,inflight超时时间到了还没被确认好,inflight信息也会被移除掉,默认30分钟。
private void updateInflightAction(final ContainerInfo container,
final Map<ContainerID, List<InflightAction>> inflightActions,
final Predicate<InflightAction> filter) {
final ContainerID id = container.containerID();
final long deadline = Time.monotonicNow() - conf.getEventTimeout();
if (inflightActions.containsKey(id)) {
final List<InflightAction> actions = inflightActions.get(id);
actions.removeIf(action ->
nodeManager.getNodeState(action.datanode) != NodeState.HEALTHY);
// inflight超时时间到了,也inflight信息也会被移除掉
actions.removeIf(action -> action.time < deadline);
actions.removeIf(filter);
if (actions.isEmpty()) {
inflightActions.remove(id);
}
}
}
inflight信息更新好之后呢,ReplicationManager就开始正式地进行Container Replica数的检查处理了,这里分出了4种情况:
这里我们挑选副本数不足和副本数过量的情况,来看其逻辑实现:
private boolean isContainerUnderReplicated(final ContainerInfo container,
final Set<ContainerReplica> replicas) {
if (container.getState() != LifeCycleState.CLOSED &&
container.getState() != LifeCycleState.QUASI_CLOSED) {
return false;
}
boolean misReplicated = !getPlacementStatus(
replicas, container.getReplicationFactor().getNumber())
.isPolicySatisfied();
// 通过当前副本数和预计应该达到的副本数量做比较,以此判断是否需要做剩余副本的Replication
return container.getReplicationFactor().getNumber() >
getReplicaCount(container.containerID(), replicas) || misReplicated;
}
/**
* 获取当前副本的数量=当前副本数+正在复制的副本-正在删除的副本数
*/
private int getReplicaCount(final ContainerID id,
final Set<ContainerReplica> replicas) {
return replicas.size()
+ inflightReplication.getOrDefault(id, Collections.emptyList()).size()
- inflightDeletion.getOrDefault(id, Collections.emptyList()).size();
}
从上面我们可以看到RM(ReplicationManager的缩写)是假定那些inflight中的Replica最终是能够成功的,所以采用了当前副本数量+inflightReplication数-inflightDeletion的算法来做当前的副本数量的。
然后根据这个结果,再来决定是否继续给这个Container做发送Replication命令还是Replication delete命令。
RM在发送Replication命令和Replication Delete命令时,会分别加入inflight信息到inflightReplication和inflightDeletion里面去。
以Replication命令为例子:
private void sendReplicateCommand(final ContainerInfo container,
final DatanodeDetails datanode,
final List<DatanodeDetails> sources) {
LOG.info("Sending replicate container command for container {}" +
" to datanode {}", container.containerID(), datanode);
final ContainerID id = container.containerID();
final ReplicateContainerCommand replicateCommand =
new ReplicateContai
nerCommand(id.getId(), sources);
inflightReplication.computeIfAbsent(id, k -> new ArrayList<>());
sendAndTrackDatanodeCommand(datanode, replicateCommand,
action -> inflightReplication.get(id).add(action));
}
private <T extends GeneratedMessage> void sendAndTrackDatanodeCommand(
final DatanodeDetails datanode,
final SCMCommand<T> command,
final Consumer<InflightAction> tracker) {
final CommandForDatanode<T> datanodeCommand =
new CommandForDatanode<>(datanode.getUuid(), command);
eventPublisher.fireEvent(SCMEvents.DATANODE_COMMAND, datanodeCommand);
tracker.accept(new InflightAction(datanode, Time.monotonicNow()));
}
最后对于那些剩余的unhealthy的Container,RM选择了删除Container Replica的处理。
private void handleUnstableContainer(final ContainerInfo container,
final Set<ContainerReplica> replicas) {
// Find unhealthy replicas
List<ContainerReplica> unhealthyReplicas = replicas.stream()
.filter(r -> !compareState(container.getState(), r.getState()))
.collect(Collectors.toList());
...
// Now we are left with the replicas which are either unhealthy or
// the BCSID doesn't match. These replicas should be deleted.
/*
* If we have unhealthy replicas we go under replicated and then
* replicate the healthy copy.
*
* We also make sure that we delete only one unhealthy replica at a time.
*
* If there are two unhealthy replica:
* - Delete first unhealthy replica
* - Re-replicate the healthy copy
* - Delete second unhealthy replica
* - Re-replicate the healthy copy
*
* Note: Only one action will be executed in a single ReplicationMonitor
* iteration. So to complete all the above actions we need four
* ReplicationMonitor iterations.
*/
unhealthyReplicas.stream().findFirst().ifPresent(replica ->
sendDeleteCommand(container, replica.getDatanodeDetails(), false));
}
OK,以上就是Ozone ReplicationManage的工作原理过程了,大家可以侧着理解下它对于inflight副本的处理逻辑过程。因为RM没有同步等待其Replication和Replication delete命令的返回结果,所以它首先预先假定其结果都是成功的。然后后期根据Container汇报过来的最新结果,做inflight信息的确认处理。在这点处理上,还是比较巧妙的。
[1]. https://github.com/apache/ozone/blob/master/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/ReplicationManager.java