Elasticsearch-PEER RECOVERY(二)

3.2.2 INDEX

  在prepareForIndexRecovery之后,就是consumer处理各种异常,我们先跳过,等到accept处理时再看;可以看到,INDEX阶段此时通过transportService发送了一个内部请求,到源节点(即当前副本分片节点向主分片所在节点发送请求),action为 internal:index/shard/recovery/start_recovery,requets就是上面我们获取的StartRecoveryRequest;获取到response后,阶段置为done。

cancellableThreads.executeIO(() ->transportService.submitRequest(request.sourceNode(), PeerRecoverySourceService.Actions.START_RECOVERY,
request, new TransportResponseHandler<RecoveryResponse>(){...}));

  下面我们看下该请求发送到主分片所在节点后,主分片节点的操作。代码入口:PeerRecoverySourceService.StartRecoveryTransportRequestHandler#messageReceived;通过接收到的request和shard构建一个RecoverySourceHandler去执行对应操作;recoverToTarget先处理异常,再获取主分片操作的permit,接下来进入到处理流程。
  retentionLock获取translog文件和lucene softDelete文件的锁,防止在操作时,文件被修改。通过isSequenceNumberBasedRecovery判断是否可以基于seqNo恢复(即跳过phase1。恢复时分phase1和phase2两个阶段:phase1主要调用Lucene接口,做shard快照,将shard数据复制到副本分片所在的节点,也就是targetNode;phase2主要是对translog做快照,通过replay translog恢复新增的操作);因为phase1需要通过网络复制数据,过程缓慢,因此详细了解下,什么条件下,可以跳过phase1,即isSequenceNumberBasedRecovery为true。

final boolean isSequenceNumberBasedRecovery = request.startingSeqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO &&
                isTargetSameHistory() && shard.hasCompleteHistoryOperations("peer-recovery", request.startingSeqNo());

  通过上述代码可以看到,有下面三个条件:
  1. startingSeqNo是否为已分配状态;该值的获取方式上面已经写过了(包括如何获取globalCheckpoint),不再赘述;
  2. isTargetSameHistory;目标节点分片的historyUUID非空,且目标节点分片的historyUUID与主分片的historyUUID一致;
  3. hasCompleteOperationHistory;判断translog中记录操作序列seqNo是否大于等于当前节点的localCheckpoint,换句话说,如果存在分片中的数据多于translog中记录的,那不能跳过phase1,必须拷贝分片数据,只有translog中记录的数据多于分片中的数据量,那么,只依赖phase2,replay translog就可以做到完全恢复。

/*
 * 对比translog中的操作和localCheckpoint
 */
public boolean hasCompleteOperationHistory(String source, MapperService mapperService, long startingSeqNo) throws IOException {
    // 获取localCheckpoint
    final long currentLocalCheckpoint = getLocalCheckpointTracker().getCheckpoint();
    final LocalCheckpointTracker tracker = new LocalCheckpointTracker(startingSeqNo, startingSeqNo - 1);
    try (Translog.Snapshot snapshot = getTranslog().newSnapshotFromMinSeqNo(startingSeqNo)) {
        Translog.Operation operation;
        while ((operation = snapshot.next()) != null) {
            // 从需要开始恢复的seqNo开始,逐个操作标记,标记实质是更新tracker的checkpoint
            if (operation.seqNo() != SequenceNumbers.UNASSIGNED_SEQ_NO) {
                tracker.markSeqNoAsCompleted(operation.seqNo());
            }
        }
    }
    return tracker.getCheckpoint() >= currentLocalCheckpoint;
}

  满足如上三个条件,即可跳过phase1,拷贝分片数据的部分;如果可以跳过phase1,则没有额外操作,只是将sendFileResult置为空就结束了,因此,我们继续看下不能跳过的情况,也就是phase1的处理。
  先获取shard对应的store(store主要存储当前分片存储信息,例如路径、版本等),从store中获取快照(从上一次提交记录打快照,即safeCommit)的元信息,遍历快照中的文件名,如果有任何一个文件存在,但获取不到元数据,则抛异常;对比source node 和 target node的syncid,如果一致,则跳过lucene文件比对阶段(这里需要注意,这是第二次可以快速跳过INDEX阶段的条件!!!)。

// 获取源分片(PEER过程中的主分片)的syncid
String recoverySourceSyncId = recoverySourceMetadata.getSyncId();
// 获取目标分片(待恢复的分片)的syncid
String recoveryTargetSyncId = request.metadataSnapshot().getSyncId();
final boolean recoverWithSyncId = recoverySourceSyncId != null &&
        recoverySourceSyncId.equals(recoveryTargetSyncId);
if (recoverWithSyncId) {
    final long numDocsTarget = request.metadataSnapshot().getNumDocs();
    final long numDocsSource = recoverySourceMetadata.getNumDocs();
    // syncid一致,且socNum一致(理论来说,如果syncid一致,docNum基本都一致)
    if (numDocsTarget != numDocsSource) {
        throw new IllegalStateException(...);
    }
} else {...}

  如果无法跳过INDEX阶段,则需要比较lucene文件,找出有差异的文件(即上面代码的else分支),即 Store.MetadataSnapshot#recoveryDiff 部分的代码;遍历所有存储文件的元数据信息,这里直接使用了this,解释下,可以看到recoveryDiff属于内部类MetadataSnapshot,该类实现了Iterable,初始化时通过loadMetadata获取到所有lucene文件的信息,并put到map中,重写了iterator迭代返回collection中的value。

public static final class MetadataSnapshot implements Iterable<StoreFileMetaData>, Writeable {
    private final Map<String, StoreFileMetaData> metadata;
    MetadataSnapshot(IndexCommit commit, Directory directory, Logger logger) throws IOException {
        LoadedMetadata loadedMetadata = loadMetadata(commit, directory, logger);
        /.../
    }
    @Override
    public Iterator<StoreFileMetaData> iterator() {
        return metadata.values().iterator();
    }
}

  如果文件名为segments.gen,则跳过(恢复时,不考虑此文件);获取到文件名及后缀(例如"_1.fdt",获取到文件名"_1"和文件类型"fdt";“segments_n”,获取到的文件名为"segments",类型为null),如果文件名是segments,或文件类型为"del"、“liv”,归为一类,放置perCommitStoreFiles,其他的为一类,放置perSegment;如果target node(待恢复节点)不存在此文件,则认为是missing丢失的;如果存在该文件,但不同,则认为有变化different;其余的认为是相同的identical;

// 相同的文件
final List<StoreFileMetaData> identical = new ArrayList<>();
// 不同的文件
final List<StoreFileMetaData> different = new ArrayList<>();
// 缺失的文件
final List<StoreFileMetaData> missing = new ArrayList<>();
for (StoreFileMetaData meta : segmentFiles) {
    StoreFileMetaData storeFileMetaData = recoveryTargetSnapshot.get(meta.name());
    // 待恢复节点无此文件
    if (storeFileMetaData == null) {
        consistent = false;
        missing.add(meta);
    } else if (storeFileMetaData.isSame(meta) == false) {
      // 待恢复节点有此文件,但不同
        consistent = false;
        different.add(meta);
    } else {
      // 待恢复节点拥有相同的文件
        identicalFiles.add(meta);
    }
}
// 如果文件的长度和checksum都相同,则认为是同一份文件(测试环境发现,相同的操作,对应lucene段文件checksum很可能不一致
// 因此,
最好基于syncid跳过phase1)
public boolean isSame(StoreFileMetaData other) {
    if (checksum == null || other.checksum == null) {
        return false;
    }
    return length == other.length && checksum.equals(other.checksum) && hash.equals(other.hash);
}

  将所有相同的文件放到phase1ExistingFileNames;不同的和丢失的放到phase1Files;然后将phase1Files文件,通过sendFiles方法发送到待恢复节点。逐个遍历需要拷贝的文件,以512KB为单位,逐块发送文件。

// RecoverySettings中设置了chunk size
private volatile ByteSizeValue chunkSize = DEFAULT_CHUNK_SIZE;
// 默认512KB
public static final ByteSizeValue DEFAULT_CHUNK_SIZE = new ByteSizeValue(512, ByteSizeUnit.KB);
// 发送需要恢复的文件
void sendFiles(Store store, StoreFileMetaData[] files, Supplier<Integer> translogOps) throws Exception {
    final byte[] buffer = new byte[chunkSizeInBytes];
    // 初始化一个tracker,用来记录操作数,当前初始化nextSeqNo从0开始,checkpoint从-1开始
    final LocalCheckpointTracker requestSeqIdTracker = new LocalCheckpointTracker(-1, -1);
    for (final StoreFileMetaData md : files) {
      // 有异常就跳出
        if (error.get() != null) {
            break;
        }
        // 以512KB为单位,读取文件
        try (IndexInput indexInput = store.directory().openInput(md.name(), IOContext.READONCE);
             InputStream in = new InputStreamIndexInput(indexInput, md.length())) {
            // 记录当前的位置
            long position = 0;
            int bytesRead;
            while ((bytesRead = in.read(buffer, 0, buffer.length)) != -1) {
                // 当前需要发送的文件内容
                final BytesArray content = new BytesArray(buffer, 0, bytesRead);
                // 如果偏移量+内容长度=文件总长度,则认为是最后一个chunk
                final boolean lastChunk = position + content.length() == md.length();
                // 获取上面初始化的tranker的seqNo,该seqNo每次都会加1
                final long requestSeqId = requestSeqIdTracker.generateSeqNo();
                // 控制并发,即多少个chunk一起拷贝
                cancellableThreads.execute(() -> requestSeqIdTracker.waitForOpsToComplete(requestSeqId - maxConcurrentFileChunks));
                cancellableThreads.checkForCancel();
                if (error.get() != null) {
                    break;
                }
                // 获取当前偏移量(位置),在写操作,即writeFileChunk时带入
                final long requestFilePosition = position;
                // 发送文件到目的节点
                cancellableThreads.executeIO(() ->
                    recoveryTarget.writeFileChunk(...));
                // 偏移量(位置)更新
                position += content.length();
            }
        } catch (Exception e) {
            error.compareAndSet(null, Tuple.tuple(md, e));
            break;
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值