Skip to content

Commit 37ab351

Browse files
Deduplicate Index Metadata in BlobStore (#50278)
This PR introduces two new fields in to `RepositoryData` (index-N) to track the blob name of `IndexMetaData` blobs and their content via setting generations and uuids. This is used to deduplicate the `IndexMetaData` blobs (`meta-{uuid}.dat` in the indices folders under `/indices` so that new metadata for an index is only written to the repository during a snapshot if that same metadata can't be found in another snapshot. This saves one write per index in the common case of unchanged metadata thus saving cost and making snapshot finalization drastically faster if many indices are being snapshotted at the same time. The implementation is mostly analogous to that for shard generations in #46250 and piggy backs on the BwC mechanism introduced in that PR (which means this PR needs adjustments if it doesn't go into `7.6`). Relates to #45736 as it improves the efficiency of snapshotting unchanged indices Relates to #49800 as it has the potential of loading the index metadata for multiple snapshots of the same index concurrently much more efficient speeding up future concurrent snapshot delete
1 parent 78fcd05 commit 37ab351

File tree

22 files changed

+633
-128
lines changed

22 files changed

+633
-128
lines changed

plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreRepositoryTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ public void testEnforcedCooldownPeriod() throws IOException {
159159
final RepositoryData repositoryData = getRepositoryData(repository);
160160
final RepositoryData modifiedRepositoryData = repositoryData.withVersions(Collections.singletonMap(fakeOldSnapshot,
161161
SnapshotsService.SHARD_GEN_IN_REPO_DATA_VERSION.minimumCompatibilityVersion()));
162-
final BytesReference serialized =
163-
BytesReference.bytes(modifiedRepositoryData.snapshotsToXContent(XContentFactory.jsonBuilder(), false));
162+
final BytesReference serialized = BytesReference.bytes(modifiedRepositoryData.snapshotsToXContent(XContentFactory.jsonBuilder(),
163+
SnapshotsService.OLD_SNAPSHOT_FORMAT));
164164
PlainActionFuture.get(f -> repository.threadPool().generic().execute(ActionRunnable.run(f, () -> {
165165
try (InputStream stream = serialized.streamInput()) {
166166
repository.blobStore().blobContainer(repository.basePath()).writeBlobAtomic(

qa/repository-multi-version/src/test/java/org/elasticsearch/upgrades/MultiVersionRepositoryAccessIT.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public void testUpgradeMovesRepoToNewMetaVersion() throws IOException {
220220
ensureSnapshotRestoreWorks(client, repoName, "snapshot-2", shards);
221221
}
222222
} else {
223-
if (SnapshotsService.useShardGenerations(minimumNodeVersion()) == false) {
223+
if (SnapshotsService.useIndexGenerations(minimumNodeVersion()) == false) {
224224
assertThat(TEST_STEP, is(TestStep.STEP3_OLD_CLUSTER));
225225
final List<Class<? extends Exception>> expectedExceptions =
226226
List.of(ResponseException.class, ElasticsearchStatusException.class);

server/src/internalClusterTest/java/org/elasticsearch/snapshots/CorruptedBlobStoreRepositoryIT.java

+5-3
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.elasticsearch.common.settings.Settings;
3333
import org.elasticsearch.common.unit.ByteSizeUnit;
3434
import org.elasticsearch.common.xcontent.XContentFactory;
35+
import org.elasticsearch.repositories.IndexMetaDataGenerations;
3536
import org.elasticsearch.repositories.RepositoriesService;
3637
import org.elasticsearch.repositories.Repository;
3738
import org.elasticsearch.repositories.RepositoryData;
@@ -232,11 +233,12 @@ public void testHandlingMissingRootLevelSnapshotMetadata() throws Exception {
232233
SnapshotId::getUUID, Function.identity())),
233234
repositoryData.getSnapshotIds().stream().collect(Collectors.toMap(
234235
SnapshotId::getUUID, repositoryData::getSnapshotState)),
235-
Collections.emptyMap(), Collections.emptyMap(), ShardGenerations.EMPTY);
236+
Collections.emptyMap(), Collections.emptyMap(), ShardGenerations.EMPTY, IndexMetaDataGenerations.EMPTY);
236237

237238
Files.write(repo.resolve(BlobStoreRepository.INDEX_FILE_PREFIX + withoutVersions.getGenId()),
238-
BytesReference.toBytes(BytesReference.bytes(withoutVersions.snapshotsToXContent(XContentFactory.jsonBuilder(),
239-
true))), StandardOpenOption.TRUNCATE_EXISTING);
239+
BytesReference.toBytes(BytesReference.bytes(
240+
withoutVersions.snapshotsToXContent(XContentFactory.jsonBuilder(), Version.CURRENT))),
241+
StandardOpenOption.TRUNCATE_EXISTING);
240242

241243
logger.info("--> verify that repo is assumed in old metadata format");
242244
final SnapshotsService snapshotsService = internalCluster().getCurrentMasterNodeInstance(SnapshotsService.class);

server/src/internalClusterTest/java/org/elasticsearch/snapshots/DedicatedClusterSnapshotRestoreIT.java

+82
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import org.elasticsearch.node.Node;
7171
import org.elasticsearch.plugins.Plugin;
7272
import org.elasticsearch.repositories.RepositoryMissingException;
73+
import org.elasticsearch.repositories.blobstore.BlobStoreRepository;
7374
import org.elasticsearch.rest.AbstractRestChannel;
7475
import org.elasticsearch.rest.RestRequest;
7576
import org.elasticsearch.rest.RestResponse;
@@ -1110,6 +1111,8 @@ public void testSnapshotTotalAndIncrementalSizes() throws IOException {
11101111

11111112
SnapshotStats stats = snapshots.get(0).getStats();
11121113

1114+
final List<Path> snapshot0IndexMetaFiles = findRepoMetaBlobs(repoPath);
1115+
assertThat(snapshot0IndexMetaFiles, hasSize(1)); // snapshotting a single index
11131116
assertThat(stats.getTotalFileCount(), greaterThanOrEqualTo(snapshot0FileCount));
11141117
assertThat(stats.getTotalSize(), greaterThanOrEqualTo(snapshot0FileSize));
11151118

@@ -1142,6 +1145,10 @@ public void testSnapshotTotalAndIncrementalSizes() throws IOException {
11421145
.get();
11431146

11441147
final List<Path> snapshot1Files = scanSnapshotFolder(repoPath);
1148+
final List<Path> snapshot1IndexMetaFiles = findRepoMetaBlobs(repoPath);
1149+
1150+
// The IndexMetadata did not change between snapshots, verify that no new redundant IndexMetaData was written to the repository
1151+
assertThat(snapshot1IndexMetaFiles, is(snapshot0IndexMetaFiles));
11451152

11461153
final int snapshot1FileCount = snapshot1Files.size();
11471154
final long snapshot1FileSize = calculateTotalFilesSize(snapshot1Files);
@@ -1166,6 +1173,65 @@ public void testSnapshotTotalAndIncrementalSizes() throws IOException {
11661173
assertThat(anotherStats.getTotalSize(), greaterThanOrEqualTo(snapshot1FileSize));
11671174
}
11681175

1176+
public void testDeduplicateIndexMetadata() throws Exception {
1177+
final String indexName = "test-blocks-1";
1178+
final String repositoryName = "repo-" + indexName;
1179+
final String snapshot0 = "snapshot-0";
1180+
final String snapshot1 = "snapshot-1";
1181+
final String snapshot2 = "snapshot-2";
1182+
1183+
createIndex(indexName);
1184+
1185+
int docs = between(10, 100);
1186+
for (int i = 0; i < docs; i++) {
1187+
client().prepareIndex(indexName).setSource("test", "init").execute().actionGet();
1188+
}
1189+
1190+
final Path repoPath = randomRepoPath();
1191+
createRepository(repositoryName, "fs", repoPath);
1192+
1193+
logger.info("--> create a snapshot");
1194+
client().admin().cluster().prepareCreateSnapshot(repositoryName, snapshot0)
1195+
.setIncludeGlobalState(true)
1196+
.setWaitForCompletion(true)
1197+
.get();
1198+
1199+
final List<Path> snapshot0IndexMetaFiles = findRepoMetaBlobs(repoPath);
1200+
assertThat(snapshot0IndexMetaFiles, hasSize(1)); // snapshotting a single index
1201+
1202+
docs = between(1, 5);
1203+
for (int i = 0; i < docs; i++) {
1204+
client().prepareIndex(indexName).setSource("test", "test" + i).execute().actionGet();
1205+
}
1206+
1207+
logger.info("--> restart random data node and add new data node to change index allocation");
1208+
internalCluster().restartRandomDataNode();
1209+
internalCluster().startDataOnlyNode();
1210+
ensureGreen(indexName);
1211+
1212+
assertThat(client().admin().cluster().prepareCreateSnapshot(repositoryName, snapshot1).setWaitForCompletion(true).get().status(),
1213+
equalTo(RestStatus.OK));
1214+
1215+
final List<Path> snapshot1IndexMetaFiles = findRepoMetaBlobs(repoPath);
1216+
1217+
// The IndexMetadata did not change between snapshots, verify that no new redundant IndexMetaData was written to the repository
1218+
assertThat(snapshot1IndexMetaFiles, is(snapshot0IndexMetaFiles));
1219+
1220+
// index to some other field to trigger a change in index metadata
1221+
for (int i = 0; i < docs; i++) {
1222+
client().prepareIndex(indexName).setSource("new_field", "test" + i).execute().actionGet();
1223+
}
1224+
assertThat(client().admin().cluster().prepareCreateSnapshot(repositoryName, snapshot2).setWaitForCompletion(true).get().status(),
1225+
equalTo(RestStatus.OK));
1226+
1227+
final List<Path> snapshot2IndexMetaFiles = findRepoMetaBlobs(repoPath);
1228+
assertThat(snapshot2IndexMetaFiles, hasSize(2)); // should have created one new metadata blob
1229+
1230+
assertAcked(client().admin().cluster().prepareDeleteSnapshot(repositoryName, snapshot0, snapshot1).get());
1231+
final List<Path> snapshot3IndexMetaFiles = findRepoMetaBlobs(repoPath);
1232+
assertThat(snapshot3IndexMetaFiles, hasSize(1)); // should have deleted the metadata blob referenced by the first two snapshots
1233+
}
1234+
11691235
public void testDataNodeRestartWithBusyMasterDuringSnapshot() throws Exception {
11701236
logger.info("--> starting a master node and two data nodes");
11711237
internalCluster().startMasterOnlyNode();
@@ -1345,6 +1411,22 @@ private long calculateTotalFilesSize(List<Path> files) {
13451411
}).sum();
13461412
}
13471413

1414+
private static List<Path> findRepoMetaBlobs(Path repoPath) throws IOException {
1415+
List<Path> files = new ArrayList<>();
1416+
Files.walkFileTree(repoPath.resolve("indices"), new SimpleFileVisitor<>() {
1417+
@Override
1418+
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
1419+
final String fileName = file.getFileName().toString();
1420+
if (fileName.startsWith(BlobStoreRepository.METADATA_PREFIX) && fileName.endsWith(".dat")) {
1421+
files.add(file);
1422+
}
1423+
return super.visitFile(file, attrs);
1424+
}
1425+
}
1426+
);
1427+
return files;
1428+
}
1429+
13481430
private List<Path> scanSnapshotFolder(Path repoPath) throws IOException {
13491431
List<Path> files = new ArrayList<>();
13501432
Files.walkFileTree(repoPath, new SimpleFileVisitor<Path>(){

server/src/internalClusterTest/java/org/elasticsearch/snapshots/MetadataLoadingDuringSnapshotRestoreIT.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse;
2424
import org.elasticsearch.action.admin.cluster.snapshots.restore.RestoreSnapshotResponse;
2525
import org.elasticsearch.action.admin.cluster.snapshots.status.SnapshotsStatusResponse;
26+
import org.elasticsearch.action.support.PlainActionFuture;
2627
import org.elasticsearch.cluster.metadata.IndexMetadata;
2728
import org.elasticsearch.cluster.metadata.Metadata;
2829
import org.elasticsearch.cluster.metadata.RepositoryMetadata;
@@ -33,6 +34,7 @@
3334
import org.elasticsearch.repositories.IndexId;
3435
import org.elasticsearch.repositories.RepositoriesService;
3536
import org.elasticsearch.repositories.Repository;
37+
import org.elasticsearch.repositories.RepositoryData;
3638
import org.elasticsearch.rest.RestStatus;
3739
import org.elasticsearch.snapshots.mockstore.MockRepository;
3840

@@ -193,9 +195,10 @@ public Metadata getSnapshotGlobalMetadata(SnapshotId snapshotId) {
193195
}
194196

195197
@Override
196-
public IndexMetadata getSnapshotIndexMetadata(SnapshotId snapshotId, IndexId indexId) throws IOException {
198+
public IndexMetadata getSnapshotIndexMetaData(RepositoryData repositoryData, SnapshotId snapshotId,
199+
IndexId indexId) throws IOException {
197200
indicesMetadata.computeIfAbsent(key(snapshotId.getName(), indexId.getName()), (s) -> new AtomicInteger(0)).incrementAndGet();
198-
return super.getSnapshotIndexMetadata(snapshotId, indexId);
201+
return super.getSnapshotIndexMetaData(PlainActionFuture.get(this::getRepositoryData), snapshotId, indexId);
199202
}
200203
}
201204

server/src/internalClusterTest/java/org/elasticsearch/snapshots/SharedClusterSnapshotRestoreIT.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -2888,7 +2888,8 @@ public void testRestoreSnapshotWithCorruptedIndexMetadata() throws Exception {
28882888
final IndexId corruptedIndex = randomFrom(indexIds.values());
28892889
final Path indexMetadataPath = repo.resolve("indices")
28902890
.resolve(corruptedIndex.getId())
2891-
.resolve("meta-" + snapshotInfo.snapshotId().getUUID() + ".dat");
2891+
.resolve(
2892+
"meta-" + repositoryData.indexMetaDataGenerations().indexMetaBlobId(snapshotInfo.snapshotId(), corruptedIndex) + ".dat");
28922893

28932894
// Truncate the index metadata file
28942895
try(SeekableByteChannel outChan = Files.newByteChannel(indexMetadataPath, StandardOpenOption.WRITE)) {

server/src/main/java/org/elasticsearch/action/admin/cluster/snapshots/status/TransportSnapshotsStatusAction.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ private Map<ShardId, IndexShardSnapshotStatus> snapshotShards(final String repos
318318
final Map<ShardId, IndexShardSnapshotStatus> shardStatus = new HashMap<>();
319319
for (String index : snapshotInfo.indices()) {
320320
IndexId indexId = repositoryData.resolveIndexId(index);
321-
IndexMetadata indexMetadata = repository.getSnapshotIndexMetadata(snapshotInfo.snapshotId(), indexId);
321+
IndexMetadata indexMetadata = repository.getSnapshotIndexMetaData(repositoryData, snapshotInfo.snapshotId(), indexId);
322322
if (indexMetadata != null) {
323323
int numberOfShards = indexMetadata.getNumberOfShards();
324324
for (int i = 0; i < numberOfShards; i++) {

server/src/main/java/org/elasticsearch/repositories/FilterRepository.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ public Metadata getSnapshotGlobalMetadata(SnapshotId snapshotId) {
7070
}
7171

7272
@Override
73-
public IndexMetadata getSnapshotIndexMetadata(SnapshotId snapshotId, IndexId index) throws IOException {
74-
return in.getSnapshotIndexMetadata(snapshotId, index);
73+
public IndexMetadata getSnapshotIndexMetaData(RepositoryData repositoryData, SnapshotId snapshotId, IndexId index) throws IOException {
74+
return in.getSnapshotIndexMetaData(repositoryData, snapshotId, index);
7575
}
7676

7777
@Override

0 commit comments

Comments
 (0)