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;
}
}
}