Skip to content

Commit f5ca487

Browse files
Add Caching for RepositoryData in BlobStoreRepository (#52341)
Cache latest `RepositoryData` on heap when it's absolutely safe to do so (i.e. when the repository is in strictly consistent mode). `RepositoryData` can safely be assumed to not grow to a size that would cause trouble because we often have at least two copies of it loaded at the same time when doing repository operations. Also, concurrent snapshot API status requests currently load it independently of each other and so on, making it safe to cache on heap and assume as "small" IMO. The benefits of this move are: * Much faster repository status API calls * listing all snapshot names becomes instant * Other operations are sped up massively too because they mostly operate in two steps: load repository data then load multiple other blobs to get the additional data * Additional cloud cost savings * Better resiliency, saving another spot where an IO issue could break the snapshot * We can simplify a number of spots in the current code that currently pass around the repository data in tricky ways to avoid loading it multiple times in follow ups.
1 parent cbd224d commit f5ca487

File tree

4 files changed

+102
-5
lines changed

4 files changed

+102
-5
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ protected Settings repositorySettings() {
9696
.put(super.repositorySettings())
9797
.put(S3Repository.BUCKET_SETTING.getKey(), "bucket")
9898
.put(S3Repository.CLIENT_NAME.getKey(), "test")
99+
// Don't cache repository data because some tests manually modify the repository data
100+
.put(BlobStoreRepository.CACHE_REPOSITORY_DATA.getKey(), false)
99101
.build();
100102
}
101103

server/src/main/java/org/elasticsearch/repositories/blobstore/BlobStoreRepository.java

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,11 @@
6464
import org.elasticsearch.common.bytes.BytesReference;
6565
import org.elasticsearch.common.collect.Tuple;
6666
import org.elasticsearch.common.component.AbstractLifecycleComponent;
67+
import org.elasticsearch.common.compress.CompressorFactory;
6768
import org.elasticsearch.common.compress.NotXContentException;
6869
import org.elasticsearch.common.io.Streams;
6970
import org.elasticsearch.common.io.stream.BytesStreamOutput;
71+
import org.elasticsearch.common.io.stream.StreamOutput;
7072
import org.elasticsearch.common.lucene.Lucene;
7173
import org.elasticsearch.common.lucene.store.InputStreamIndexInput;
7274
import org.elasticsearch.common.metrics.CounterMetric;
@@ -77,6 +79,7 @@
7779
import org.elasticsearch.common.util.concurrent.AbstractRunnable;
7880
import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
7981
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
82+
import org.elasticsearch.common.xcontent.XContentBuilder;
8083
import org.elasticsearch.common.xcontent.XContentFactory;
8184
import org.elasticsearch.common.xcontent.XContentParser;
8285
import org.elasticsearch.common.xcontent.XContentType;
@@ -129,6 +132,7 @@
129132
import java.util.concurrent.LinkedBlockingQueue;
130133
import java.util.concurrent.TimeUnit;
131134
import java.util.concurrent.atomic.AtomicLong;
135+
import java.util.concurrent.atomic.AtomicReference;
132136
import java.util.function.Consumer;
133137
import java.util.stream.Collectors;
134138
import java.util.stream.LongStream;
@@ -205,8 +209,16 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp
205209
public static final Setting<Boolean> ALLOW_CONCURRENT_MODIFICATION =
206210
Setting.boolSetting("allow_concurrent_modifications", false, Setting.Property.Deprecated);
207211

212+
/**
213+
* Setting to disable caching of the latest repository data.
214+
*/
215+
public static final Setting<Boolean> CACHE_REPOSITORY_DATA =
216+
Setting.boolSetting("cache_repository_data", true, Setting.Property.Deprecated);
217+
208218
private final boolean compress;
209219

220+
private final boolean cacheRepositoryData;
221+
210222
private final RateLimiter snapshotRateLimiter;
211223

212224
private final RateLimiter restoreRateLimiter;
@@ -282,6 +294,7 @@ protected BlobStoreRepository(
282294
snapshotRateLimiter = getRateLimiter(metadata.settings(), "max_snapshot_bytes_per_sec", new ByteSizeValue(40, ByteSizeUnit.MB));
283295
restoreRateLimiter = getRateLimiter(metadata.settings(), "max_restore_bytes_per_sec", new ByteSizeValue(40, ByteSizeUnit.MB));
284296
readOnly = metadata.settings().getAsBoolean("readonly", false);
297+
cacheRepositoryData = CACHE_REPOSITORY_DATA.get(metadata.settings());
285298
this.basePath = basePath;
286299

287300
indexShardSnapshotFormat = new ChecksumBlobStoreFormat<>(SNAPSHOT_CODEC, SNAPSHOT_NAME_FORMAT,
@@ -510,13 +523,16 @@ public void deleteSnapshot(SnapshotId snapshotId, long repositoryStateId, Versio
510523
* @param rootBlobs Blobs at the repository root
511524
* @return RepositoryData
512525
*/
513-
private RepositoryData safeRepositoryData(long repositoryStateId, Map<String, BlobMetaData> rootBlobs) {
526+
private RepositoryData safeRepositoryData(long repositoryStateId, Map<String, BlobMetaData> rootBlobs) throws IOException {
514527
final long generation = latestGeneration(rootBlobs.keySet());
515528
final long genToLoad;
529+
final Tuple<Long, BytesReference> cached;
516530
if (bestEffortConsistency) {
517531
genToLoad = latestKnownRepoGen.updateAndGet(known -> Math.max(known, repositoryStateId));
532+
cached = null;
518533
} else {
519534
genToLoad = latestKnownRepoGen.get();
535+
cached = latestKnownRepositoryData.get();
520536
}
521537
if (genToLoad > generation) {
522538
// It's always a possibility to not see the latest index-N in the listing here on an eventually consistent blob store, just
@@ -529,6 +545,9 @@ private RepositoryData safeRepositoryData(long repositoryStateId, Map<String, Bl
529545
throw new RepositoryException(metadata.name(), "concurrent modification of the index-N file, expected current generation [" +
530546
repositoryStateId + "], actual current generation [" + genToLoad + "]");
531547
}
548+
if (cached != null && cached.v1() == genToLoad) {
549+
return repositoryDataFromCachedEntry(cached);
550+
}
532551
return getRepositoryData(genToLoad);
533552
}
534553

@@ -1057,6 +1076,9 @@ public void endVerification(String seed) {
10571076
// and concurrent modifications.
10581077
private final AtomicLong latestKnownRepoGen = new AtomicLong(RepositoryData.UNKNOWN_REPO_GEN);
10591078

1079+
// Best effort cache of the latest known repository data and its generation, cached serialized as compressed json
1080+
private final AtomicReference<Tuple<Long, BytesReference>> latestKnownRepositoryData = new AtomicReference<>();
1081+
10601082
@Override
10611083
public void getRepositoryData(ActionListener<RepositoryData> listener) {
10621084
if (latestKnownRepoGen.get() == RepositoryData.CORRUPTED_REPO_GEN) {
@@ -1090,7 +1112,16 @@ public void getRepositoryData(ActionListener<RepositoryData> listener) {
10901112
genToLoad = latestKnownRepoGen.get();
10911113
}
10921114
try {
1093-
listener.onResponse(getRepositoryData(genToLoad));
1115+
final Tuple<Long, BytesReference> cached = latestKnownRepositoryData.get();
1116+
final RepositoryData loaded;
1117+
// Caching is not used with #bestEffortConsistency see docs on #cacheRepositoryData for details
1118+
if (bestEffortConsistency == false && cached != null && cached.v1() == genToLoad) {
1119+
loaded = repositoryDataFromCachedEntry(cached);
1120+
} else {
1121+
loaded = getRepositoryData(genToLoad);
1122+
cacheRepositoryData(loaded);
1123+
}
1124+
listener.onResponse(loaded);
10941125
return;
10951126
} catch (RepositoryException e) {
10961127
// If the generation to load changed concurrently and we didn't just try loading the same generation before we retry
@@ -1116,6 +1147,59 @@ public void getRepositoryData(ActionListener<RepositoryData> listener) {
11161147
}
11171148
}
11181149

1150+
/**
1151+
* Puts the given {@link RepositoryData} into the cache if it is of a newer generation and only if the repository is not using
1152+
* {@link #bestEffortConsistency}. When using {@link #bestEffortConsistency} the repository is using listing to find the latest
1153+
* {@code index-N} blob and there are no hard guarantees that a given repository generation won't be reused since an external
1154+
* modification can lead to moving from a higher {@code N} to a lower {@code N} value which mean we can't safely assume that a given
1155+
* generation will always contain the same {@link RepositoryData}.
1156+
*
1157+
* @param updated RepositoryData to cache if newer than the cache contents
1158+
*/
1159+
private void cacheRepositoryData(RepositoryData updated) {
1160+
if (cacheRepositoryData && bestEffortConsistency == false) {
1161+
final BytesReference serialized;
1162+
BytesStreamOutput out = new BytesStreamOutput();
1163+
try {
1164+
try (StreamOutput tmp = CompressorFactory.COMPRESSOR.streamOutput(out);
1165+
XContentBuilder builder = XContentFactory.jsonBuilder(tmp)) {
1166+
updated.snapshotsToXContent(builder, true);
1167+
}
1168+
serialized = out.bytes();
1169+
final int len = serialized.length();
1170+
if (len > ByteSizeUnit.KB.toBytes(500)) {
1171+
logger.debug("Not caching repository data of size [{}] for repository [{}] because it is larger than 500KB in" +
1172+
" serialized size", len, metadata.name());
1173+
if (len > ByteSizeUnit.MB.toBytes(5)) {
1174+
logger.warn("Your repository metadata blob for repository [{}] is larger than 5MB. Consider moving to a fresh" +
1175+
" repository for new snapshots or deleting unneeded snapshots from your repository to ensure stable" +
1176+
" repository behavior going forward.", metadata.name());
1177+
}
1178+
// Set empty repository data to not waste heap for an outdated cached value
1179+
latestKnownRepositoryData.set(null);
1180+
return;
1181+
}
1182+
} catch (IOException e) {
1183+
assert false : new AssertionError("Impossible, no IO happens here", e);
1184+
logger.warn("Failed to serialize repository data", e);
1185+
return;
1186+
}
1187+
latestKnownRepositoryData.updateAndGet(known -> {
1188+
if (known != null && known.v1() > updated.getGenId()) {
1189+
return known;
1190+
}
1191+
return new Tuple<>(updated.getGenId(), serialized);
1192+
});
1193+
}
1194+
}
1195+
1196+
private RepositoryData repositoryDataFromCachedEntry(Tuple<Long, BytesReference> cacheEntry) throws IOException {
1197+
return RepositoryData.snapshotsFromXContent(
1198+
XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY,
1199+
LoggingDeprecationHandler.INSTANCE,
1200+
CompressorFactory.COMPRESSOR.streamInput(cacheEntry.v2().streamInput())), cacheEntry.v1());
1201+
}
1202+
11191203
private RepositoryException corruptedStateException(@Nullable Exception cause) {
11201204
return new RepositoryException(metadata.name(),
11211205
"Could not read repository data because the contents of the repository do not match its " +
@@ -1362,6 +1446,7 @@ public void onFailure(String source, Exception e) {
13621446

13631447
@Override
13641448
public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
1449+
cacheRepositoryData(filteredRepositoryData.withGenId(newGen));
13651450
threadPool.executor(ThreadPool.Names.SNAPSHOT).execute(ActionRunnable.run(listener, () -> {
13661451
// Delete all now outdated index files up to 1000 blobs back from the new generation.
13671452
// If there are more than 1000 dangling index-N cleanup functionality on repo delete will take care of them.

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ public void testConcurrentlyChangeRepositoryContents() throws Exception {
6666
.setType("fs").setSettings(Settings.builder()
6767
.put("location", repo)
6868
.put("compress", false)
69+
// Don't cache repository data because the test manually modifies the repository data
70+
.put(BlobStoreRepository.CACHE_REPOSITORY_DATA.getKey(), false)
6971
.put("chunk_size", randomIntBetween(100, 1000), ByteSizeUnit.BYTES)));
7072

7173
createIndex("test-idx-1", "test-idx-2");
@@ -250,6 +252,8 @@ public void testHandlingMissingRootLevelSnapshotMetadata() throws Exception {
250252
.setType("fs").setSettings(Settings.builder()
251253
.put("location", repo)
252254
.put("compress", false)
255+
// Don't cache repository data because the test manually modifies the repository data
256+
.put(BlobStoreRepository.CACHE_REPOSITORY_DATA.getKey(), false)
253257
.put("chunk_size", randomIntBetween(100, 1000), ByteSizeUnit.BYTES)));
254258

255259
final String snapshotPrefix = "test-snap-";
@@ -315,6 +319,8 @@ public void testMountCorruptedRepositoryData() throws Exception {
315319
assertAcked(client.admin().cluster().preparePutRepository(repoName)
316320
.setType("fs").setSettings(Settings.builder()
317321
.put("location", repo)
322+
// Don't cache repository data because the test manually modifies the repository data
323+
.put(BlobStoreRepository.CACHE_REPOSITORY_DATA.getKey(), false)
318324
.put("compress", false)));
319325

320326
final String snapshot = "test-snap";

test/framework/src/main/java/org/elasticsearch/common/settings/MockSecureSettings.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
*/
3737
public class MockSecureSettings implements SecureSettings {
3838

39-
private Map<String, SecureString> secureStrings = new HashMap<>();
39+
private Map<String, String> secureStrings = new HashMap<>();
4040
private Map<String, byte[]> files = new HashMap<>();
4141
private Map<String, byte[]> sha256Digests = new HashMap<>();
4242
private Set<String> settingNames = new HashSet<>();
@@ -65,7 +65,11 @@ public Set<String> getSettingNames() {
6565
@Override
6666
public SecureString getString(String setting) {
6767
ensureOpen();
68-
return secureStrings.get(setting);
68+
final String s = secureStrings.get(setting);
69+
if (s == null) {
70+
return null;
71+
}
72+
return new SecureString(s.toCharArray());
6973
}
7074

7175
@Override
@@ -81,7 +85,7 @@ public byte[] getSHA256Digest(String setting) {
8185

8286
public void setString(String setting, String value) {
8387
ensureOpen();
84-
secureStrings.put(setting, new SecureString(value.toCharArray()));
88+
secureStrings.put(setting, value);
8589
sha256Digests.put(setting, MessageDigests.sha256().digest(value.getBytes(StandardCharsets.UTF_8)));
8690
settingNames.add(setting);
8791
}

0 commit comments

Comments
 (0)