一:索引恢复介绍
索引恢复是ES数据恢复过程。比如当集群宕机或者异常重启后,写入磁盘的数据先到文件系统缓存中,没有来的级刷盘,如果不通过某种方式把数据找回来,则会丢失一部分数据,找回数据丢失的过程就是索引恢复过程。
根据数据分片的性质,索引分为主副分片,那么数据恢复就要分为主分片恢复和副分片恢复。
主分片主要从Translog中自我恢复,尚未执行flush到磁盘的分段可以从tanslog中重建
副分片需要从主分片中拉取Lucene分段和tanslog进行恢复,但有机会调过拉取Lucene分段阶段
恢复工作一般经历如下图几个阶段
二:索引恢复流程概述
索引恢复是由 clusterChanged 触发的,然后到IndexShard类的startRecovery执行一个特定分片的恢复流程,根据不同的恢复流程执行不同的恢复过程,其逻辑代码如下:
assert recoveryState.getRecoverySource().equals(shardRouting.recoverySource());
switch (recoveryState.getRecoverySource().getType()) {
case EMPTY_STORE:
case EXISTING_STORE: //主分片本地恢复
case PEER: //副分片从远处主分片恢复
case SNAPSHOT: //从镜像恢复
break;
} }
从上面源码可以看出,会根据recoveryState的类型选择不同的恢复模式,其EXISTING_STORE为主分片恢复,PEER为副分片恢复;下面就对主副分片不同的恢复流程分别解析
2.1 主分片恢复
当恢复类型为EXISTING_STORE 时候,则进行了主分片恢复,其代码进入索引恢复流程,其会初始化状态,也就是INIT阶段
case EXISTING_STORE:
//标记开始状态恢复
markAsRecovering("from store", recoveryState); // mark the shard as recovering on the cluster state thread
threadPool.generic().execute(() -> {
try {
//主分片恢复主要方法
if (recoverFromStore()) {
recoveryListener.onRecoveryDone(recoveryState);
}
} catch (Exception e) {
recoveryListener.onRecoveryFailure(recoveryState,
new RecoveryFailedException(recoveryState, null, e), true);
}
});
接着下来会进入StoreRecovery类的internalRecoverFromStore方法,其首先更新INDEX状态,其源码关键步骤如下,其流程可
//更新状态
indexShard.prepareForIndexRecovery();
long version = -1;
SegmentInfos si = null;
final Store store = indexShard.store();
//读取最后一次提交的分段信息,
si = store.readLastCommittedSegmentsInfo();
//获取其版本号
version = si.getVersion();
//更新其版本号
recoveryState.getIndex().updateVersion(version);
上面已经到了INDEX阶段,后续将进入VERIFY_INDEX阶段,此阶段主要验证当前分片是否损坏,将检查物理和逻辑损坏,这将消耗大量的CPU资源。
经过VERIFY_INDEX 阶段后,就进入索引恢复的最重要阶段TRANSLOG阶段,其阶段会遍历所有分段,根据最后一次提交信息来确定事务日志哪些数据要重放,其代码实现在InternalEngine类的recoverFromTranslogInternal 方法中,其源码实现如下,
private void recoverFromTranslogInternal(TranslogRecoveryRunner translogRecoveryRunner, long recoverUpToSeqNo) throws IOException {
//根据最后一次提交信息生成Tranlog的快照
Translog.TranslogGeneration translogGeneration = translog.getGeneration();
final int opsRecovered;
final long translogFileGen = Long.parseLong(lastCommittedSegmentInfos.getUserData().get(Translog.TRANSLOG_GENERATION_KEY));
try (Translog.Snapshot snapshot = translog.newSnapshotFromGen(
new Translog.TranslogGeneration(translog.getTranslogUUID(), translogFileGen), recoverUpToSeqNo)) {
//重放日志
opsRecovered = translogRecoveryRunner.run(this, snapshot);
} catch (Exception e) {
throw new EngineException(shardId, "failed to recover from translog", e);
}
// flush if we recovered something or if we have references to older translogs
// note: if opsRecovered == 0 and we have older translogs it means they are corrupted or 0 length.
assert pendingTranslogRecovery.get() : "translogRecovery is not pending but should be";
pendingTranslogRecovery.set(false); // we are good - now we can commit
//刷新磁盘
if (opsRecovered > 0) {
logger.trace("flushing post recovery from translog. ops recovered [{}]. committed translog id [{}]. current id [{}]",
opsRecovered, translogGeneration == null ? null :
translogGeneration.translogFileGeneration, translog.currentFileGeneration());
commitIndexWriter(indexWriter, translog, null);
refreshLastCommittedSegmentInfos();
refresh("translog_recovery");
}
translog.trimUnreferencedReaders();
}
从上面代码可知,其会根据最后提交点生成日志快照,然后对日志进行重放。
以上就是主分片主要恢复流程,当然其后续还要经过FINALIZE阶段,这个阶段会把缓冲文件刷新到系统缓冲中,然后再执行DONE阶段,刷新数据。以上就是主分片恢复过程。
2.2 副分片恢复过程
当恢复类型为PEER状态时候,其流程就是走副本恢复流程,其核心流程就是从主分片拉取Lucene分段和translog进行恢复,并按数据传递方向,主分片节点称为Source节点,副分片节点为Target。为什么还要拉取主分片的Translog呢?因为在副分片恢复期间,主分片是容许新的写入操作的,这些新的写入操作,需要从主分片进行重放。
副分片恢复恢复要比主分片复杂好多,其主要可以分两个阶段:
阶段一:在主分片所在节点,获取translog保留锁,从此时开始,会保留translog不受其刷盘清空的影响,然后调用Lucene接口把shard做快照,并把shard数据复制到副节点.
阶段二:对translog做快照,把translog快照发到副本节点进行重放
由于阶段1 需要通过网络复制大量的数据,过程十分长,故在后来的优化中,有两个条件可以跳过阶段一的
1:如果可以基于请求中的sequenceNumber进行恢复,则跳过phase1
2: 如果主副两个分片有相同的syncid且doc数量相同,也跳过阶段1
下面就对其具体流程详细讲解,当恢复类型为PEER的状态时候,将进行副本恢复,其恢复阶段如下:
INIT阶段 本阶段是在副本节点执行的,其把恢复任务开始时设置为INIT阶段,其副本准备向主分片节点发送StartRecoveryRequest的请求,其请求中包含本次要恢复的shard相关信息,如shardId等。
INDEX阶段,主要负责将分片的Lucene数据复制到副分片中。
TRANSLOG阶段 主要是讲主分片的translog数据发送到副分片节点进行重放。
FINALIZE和DONE阶段和主分片的执行过程基本一样。
2.3 副分片恢复流程中主分片节点处理过程
副分片把请求发送到主节点后,主分片会对此请求进行处理,其处理流程如下图
主分片节点的主要流程是在RecoverySourceHandler类recoverToTarget方法中,其关键步骤代码如下
//分片加锁
final Closeable retentionLock = shard.acquireRetentionLock();
resources.add(retentionLock);
final long startingSeqNo;
//判断是否可以基于sequenceNumber进行恢复
inal boolean isSequenceNumberBasedRecovery = request.startingSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO &&
isTargetSameHistory() && shard.hasCompleteHistoryOperations("peer-recovery", request.startingSeqNo());
//若可以基于序列号进行恢复,则获取开始的序列号
if (isSequenceNumberBasedRecovery) {
logger.trace("performing sequence numbers based recovery. starting at [{}]", request.startingSeqNo());
//获取开始序列号
startingSeqNo = request.startingSeqNo();
//发送的文件设置为空
sendFileResult = SendFileResult.EMPTY;
}else {
//若不能基于序列号进行恢复,则进行阶段1的逻辑
final Engine.IndexCommitRef phase1Snapshot;
//也会获取startSeqNo
startingSeqNo = Long.parseLong(
phase1Snapshot.getIndexCommit().getUserData().get(SequenceNumbers.LOCAL_CHECKPOINT_KEY)) + 1;
//获取主分片的镜像
phase1Snapshot = shard.acquireSafeIndexCommit();
//指定阶段一的逻辑
sendFileResult = phase1(phase1Snapshot.getIndexCommit(), () -> estimateNumOps);
}