Skip to content

Commit 7349989

Browse files
Add Caching for RepositoryData in BlobStoreRepository (elastic#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 d76358c commit 7349989

File tree

4 files changed

+102
-7
lines changed

4 files changed

+102
-7
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
@@ -75,6 +75,8 @@ protected Settings repositorySettings() {
7575
.put(super.repositorySettings())
7676
.put(S3Repository.BUCKET_SETTING.getKey(), "bucket")
7777
.put(S3Repository.CLIENT_NAME.getKey(), "test")
78+
// Don't cache repository data because some tests manually modify the repository data
79+
.put(BlobStoreRepository.CACHE_REPOSITORY_DATA.getKey(), false)
7880
.build();
7981
}
8082

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

Lines changed: 87 additions & 4 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;
@@ -130,6 +133,7 @@
130133
import java.util.concurrent.LinkedBlockingQueue;
131134
import java.util.concurrent.TimeUnit;
132135
import java.util.concurrent.atomic.AtomicLong;
136+
import java.util.concurrent.atomic.AtomicReference;
133137
import java.util.function.Consumer;
134138
import java.util.stream.Collectors;
135139
import java.util.stream.LongStream;
@@ -208,8 +212,16 @@ public abstract class BlobStoreRepository extends AbstractLifecycleComponent imp
208212
public static final Setting<Boolean> ALLOW_CONCURRENT_MODIFICATION =
209213
Setting.boolSetting("allow_concurrent_modifications", false, Setting.Property.Deprecated);
210214

215+
/**
216+
* Setting to disable caching of the latest repository data.
217+
*/
218+
public static final Setting<Boolean> CACHE_REPOSITORY_DATA =
219+
Setting.boolSetting("cache_repository_data", true, Setting.Property.Deprecated);
220+
211221
private final boolean compress;
212222

223+
private final boolean cacheRepositoryData;
224+
213225
private final RateLimiter snapshotRateLimiter;
214226

215227
private final RateLimiter restoreRateLimiter;
@@ -284,8 +296,7 @@ protected BlobStoreRepository(
284296
snapshotRateLimiter = getRateLimiter(metadata.settings(), "max_snapshot_bytes_per_sec", new ByteSizeValue(40, ByteSizeUnit.MB));
285297
restoreRateLimiter = getRateLimiter(metadata.settings(), "max_restore_bytes_per_sec", new ByteSizeValue(40, ByteSizeUnit.MB));
286298
readOnly = metadata.settings().getAsBoolean("readonly", false);
287-
288-
299+
cacheRepositoryData = CACHE_REPOSITORY_DATA.get(metadata.settings());
289300
indexShardSnapshotFormat = new ChecksumBlobStoreFormat<>(SNAPSHOT_CODEC, SNAPSHOT_NAME_FORMAT,
290301
BlobStoreIndexShardSnapshot::fromXContent, namedXContentRegistry, compress);
291302
indexShardSnapshotsFormat = new ChecksumBlobStoreFormat<>(SNAPSHOT_INDEX_CODEC, SNAPSHOT_INDEX_NAME_FORMAT,
@@ -521,13 +532,16 @@ public void deleteSnapshot(SnapshotId snapshotId, long repositoryStateId, boolea
521532
* @param rootBlobs Blobs at the repository root
522533
* @return RepositoryData
523534
*/
524-
private RepositoryData safeRepositoryData(long repositoryStateId, Map<String, BlobMetaData> rootBlobs) {
535+
private RepositoryData safeRepositoryData(long repositoryStateId, Map<String, BlobMetaData> rootBlobs) throws IOException {
525536
final long generation = latestGeneration(rootBlobs.keySet());
526537
final long genToLoad;
538+
final Tuple<Long, BytesReference> cached;
527539
if (bestEffortConsistency) {
528540
genToLoad = latestKnownRepoGen.updateAndGet(known -> Math.max(known, repositoryStateId));
541+
cached = null;
529542
} else {
530543
genToLoad = latestKnownRepoGen.get();
544+
cached = latestKnownRepositoryData.get();
531545
}
532546
if (genToLoad > generation) {
533547
// It's always a possibility to not see the latest index-N in the listing here on an eventually consistent blob store, just
@@ -540,6 +554,9 @@ private RepositoryData safeRepositoryData(long repositoryStateId, Map<String, Bl
540554
throw new RepositoryException(metadata.name(), "concurrent modification of the index-N file, expected current generation [" +
541555
repositoryStateId + "], actual current generation [" + genToLoad + "]");
542556
}
557+
if (cached != null && cached.v1() == genToLoad) {
558+
return repositoryDataFromCachedEntry(cached);
559+
}
543560
return getRepositoryData(genToLoad);
544561
}
545562

@@ -1067,6 +1084,9 @@ public void endVerification(String seed) {
10671084
// and concurrent modifications.
10681085
private final AtomicLong latestKnownRepoGen = new AtomicLong(RepositoryData.UNKNOWN_REPO_GEN);
10691086

1087+
// Best effort cache of the latest known repository data and its generation, cached serialized as compressed json
1088+
private final AtomicReference<Tuple<Long, BytesReference>> latestKnownRepositoryData = new AtomicReference<>();
1089+
10701090
@Override
10711091
public void getRepositoryData(ActionListener<RepositoryData> listener) {
10721092
if (latestKnownRepoGen.get() == RepositoryData.CORRUPTED_REPO_GEN) {
@@ -1100,7 +1120,16 @@ public void getRepositoryData(ActionListener<RepositoryData> listener) {
11001120
genToLoad = latestKnownRepoGen.get();
11011121
}
11021122
try {
1103-
listener.onResponse(getRepositoryData(genToLoad));
1123+
final Tuple<Long, BytesReference> cached = latestKnownRepositoryData.get();
1124+
final RepositoryData loaded;
1125+
// Caching is not used with #bestEffortConsistency see docs on #cacheRepositoryData for details
1126+
if (bestEffortConsistency == false && cached != null && cached.v1() == genToLoad) {
1127+
loaded = repositoryDataFromCachedEntry(cached);
1128+
} else {
1129+
loaded = getRepositoryData(genToLoad);
1130+
cacheRepositoryData(loaded);
1131+
}
1132+
listener.onResponse(loaded);
11041133
return;
11051134
} catch (RepositoryException e) {
11061135
// If the generation to load changed concurrently and we didn't just try loading the same generation before we retry
@@ -1126,6 +1155,59 @@ public void getRepositoryData(ActionListener<RepositoryData> listener) {
11261155
}
11271156
}
11281157

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

13731455
@Override
13741456
public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
1457+
cacheRepositoryData(filteredRepositoryData.withGenId(newGen));
13751458
threadPool.executor(ThreadPool.Names.SNAPSHOT).execute(ActionRunnable.run(listener, () -> {
13761459
// Delete all now outdated index files up to 1000 blobs back from the new generation.
13771460
// 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-";
@@ -313,6 +317,8 @@ public void testMountCorruptedRepositoryData() throws Exception {
313317
assertAcked(client.admin().cluster().preparePutRepository(repoName)
314318
.setType("fs").setSettings(Settings.builder()
315319
.put("location", repo)
320+
// Don't cache repository data because the test manually modifies the repository data
321+
.put(BlobStoreRepository.CACHE_REPOSITORY_DATA.getKey(), false)
316322
.put("compress", false)));
317323

318324
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)