Skip to content

Commit 7e81740

Browse files
authored
Testing indices query cache memory stats (elastic#135298) (elastic#137754)
(cherry picked from commit 1e1bf28)
1 parent 198d868 commit 7e81740

File tree

2 files changed

+197
-12
lines changed

2 files changed

+197
-12
lines changed

server/src/main/java/org/elasticsearch/indices/IndicesQueryCache.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,19 @@ private static QueryCacheStats toQueryCacheStatsSafe(@Nullable Stats stats) {
9191
return stats == null ? new QueryCacheStats() : stats.toQueryCacheStats();
9292
}
9393

94-
private long getShareOfAdditionalRamBytesUsed(long cacheSize) {
94+
private long getShareOfAdditionalRamBytesUsed(long itemsInCacheForShard) {
9595
if (sharedRamBytesUsed == 0L) {
9696
return 0L;
9797
}
9898

99-
// We also have some shared ram usage that we try to distribute proportionally to the cache footprint of each shard.
99+
/*
100+
* We have some shared ram usage that we try to distribute proportionally to the number of segment-requests in the cache for each
101+
* shard.
102+
*/
100103
// TODO avoid looping over all local shards here - see https://github.com/elastic/elasticsearch/issues/97222
101-
long totalSize = 0L;
104+
long totalItemsInCache = 0L;
102105
int shardCount = 0;
103-
if (cacheSize == 0L) {
106+
if (itemsInCacheForShard == 0L) {
104107
for (final var stats : shardStats.values()) {
105108
shardCount += 1;
106109
if (stats.cacheSize > 0L) {
@@ -112,7 +115,7 @@ private long getShareOfAdditionalRamBytesUsed(long cacheSize) {
112115
// branchless loop for the common case
113116
for (final var stats : shardStats.values()) {
114117
shardCount += 1;
115-
totalSize += stats.cacheSize;
118+
totalItemsInCache += stats.cacheSize;
116119
}
117120
}
118121

@@ -123,12 +126,20 @@ private long getShareOfAdditionalRamBytesUsed(long cacheSize) {
123126
}
124127

125128
final long additionalRamBytesUsed;
126-
if (totalSize == 0) {
129+
if (totalItemsInCache == 0) {
127130
// all shards have zero cache footprint, so we apportion the size of the shared bytes equally across all shards
128131
additionalRamBytesUsed = Math.round((double) sharedRamBytesUsed / shardCount);
129132
} else {
130-
// some shards have nonzero cache footprint, so we apportion the size of the shared bytes proportionally to cache footprint
131-
additionalRamBytesUsed = Math.round((double) sharedRamBytesUsed * cacheSize / totalSize);
133+
/*
134+
* Some shards have nonzero cache footprint, so we apportion the size of the shared bytes proportionally to the number of
135+
* segment-requests in the cache for this shard (the number and size of documents associated with those requests is irrelevant
136+
* for this calculation).
137+
* Note that this was a somewhat arbitrary decision. Calculating it by number of documents might have been better. Calculating
138+
* it by number of documents weighted by size would also be good, but possibly more expensive. But the decision to attribute
139+
* memory proportionally to the number of segment-requests was made a long time ago, and we're sticking with that here for the
140+
* sake of consistency and backwards compatibility.
141+
*/
142+
additionalRamBytesUsed = Math.round((double) sharedRamBytesUsed * itemsInCacheForShard / totalItemsInCache);
132143
}
133144
assert additionalRamBytesUsed >= 0L : additionalRamBytesUsed;
134145
return additionalRamBytesUsed;

server/src/test/java/org/elasticsearch/indices/IndicesQueryCacheTests.java

Lines changed: 178 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.apache.lucene.search.ScorerSupplier;
2727
import org.apache.lucene.search.Weight;
2828
import org.apache.lucene.store.Directory;
29+
import org.apache.lucene.util.Accountable;
2930
import org.elasticsearch.common.lucene.index.ElasticsearchDirectoryReader;
3031
import org.elasticsearch.common.settings.Settings;
3132
import org.elasticsearch.core.IOUtils;
@@ -34,26 +35,44 @@
3435
import org.elasticsearch.index.shard.ShardId;
3536
import org.elasticsearch.test.ESTestCase;
3637

38+
import java.io.Closeable;
3739
import java.io.IOException;
40+
import java.util.ArrayList;
41+
import java.util.List;
42+
import java.util.concurrent.atomic.AtomicReference;
43+
44+
import static org.hamcrest.Matchers.closeTo;
45+
import static org.hamcrest.Matchers.equalTo;
46+
import static org.hamcrest.Matchers.lessThan;
3847

3948
public class IndicesQueryCacheTests extends ESTestCase {
4049

41-
private static class DummyQuery extends Query {
50+
private static class DummyQuery extends Query implements Accountable {
4251

43-
private final int id;
52+
private final String id;
53+
private final long sizeInCache;
4454

4555
DummyQuery(int id) {
56+
this(Integer.toString(id), 10);
57+
}
58+
59+
DummyQuery(String id) {
60+
this(id, 10);
61+
}
62+
63+
DummyQuery(String id, long sizeInCache) {
4664
this.id = id;
65+
this.sizeInCache = sizeInCache;
4766
}
4867

4968
@Override
5069
public boolean equals(Object obj) {
51-
return sameClassAs(obj) && id == ((DummyQuery) obj).id;
70+
return sameClassAs(obj) && id.equals(((DummyQuery) obj).id);
5271
}
5372

5473
@Override
5574
public int hashCode() {
56-
return 31 * classHash() + id;
75+
return 31 * classHash() + id.hashCode();
5776
}
5877

5978
@Override
@@ -81,6 +100,10 @@ public boolean isCacheable(LeafReaderContext ctx) {
81100
};
82101
}
83102

103+
@Override
104+
public long ramBytesUsed() {
105+
return sizeInCache;
106+
}
84107
}
85108

86109
public void testBasics() throws IOException {
@@ -397,4 +420,155 @@ public void testDelegatesScorerSupplier() throws Exception {
397420
cache.onClose(shard);
398421
cache.close();
399422
}
423+
424+
public void testGetStatsMemory() throws Exception {
425+
/*
426+
* This test creates 2 shards, one with two segments and one with one. It makes unique queries against all 3 segments (so that each
427+
* query will be cached, up to the max cache size), and then asserts various things about the cache memory. Most importantly, it
428+
* asserts that the memory the cache attributes to each shard is proportional to the number of segment-queries for the shard in the
429+
* cache (and not to the number of documents in the query).
430+
*/
431+
String indexName = randomIdentifier();
432+
String uuid = randomUUID();
433+
ShardId shard1 = new ShardId(indexName, uuid, 0);
434+
ShardId shard2 = new ShardId(indexName, uuid, 1);
435+
List<Closeable> closeableList = new ArrayList<>();
436+
// We're going to create 2 segments for shard1, and 1 segment for shard2:
437+
int shard1Segment1Docs = randomIntBetween(11, 1000);
438+
int shard1Segment2Docs = randomIntBetween(1, 10);
439+
int shard2Segment1Docs = randomIntBetween(1, 10);
440+
IndexSearcher shard1Segment1Searcher = initializeSegment(shard1, shard1Segment1Docs, closeableList);
441+
IndexSearcher shard1Segment2Searcher = initializeSegment(shard1, shard1Segment2Docs, closeableList);
442+
IndexSearcher shard2Searcher = initializeSegment(shard2, shard2Segment1Docs, closeableList);
443+
444+
final int maxCacheSize = 200;
445+
Settings settings = Settings.builder()
446+
.put(IndicesQueryCache.INDICES_CACHE_QUERY_COUNT_SETTING.getKey(), maxCacheSize)
447+
.put(IndicesQueryCache.INDICES_QUERIES_CACHE_ALL_SEGMENTS_SETTING.getKey(), true)
448+
.build();
449+
IndicesQueryCache cache = new IndicesQueryCache(settings);
450+
shard1Segment1Searcher.setQueryCache(cache);
451+
shard1Segment2Searcher.setQueryCache(cache);
452+
shard2Searcher.setQueryCache(cache);
453+
454+
assertEquals(0L, cache.getStats(shard1).getMemorySizeInBytes());
455+
456+
final long largeQuerySize = randomIntBetween(100, 1000);
457+
final long smallQuerySize = randomIntBetween(10, 50);
458+
459+
final int shard1Queries = randomIntBetween(20, 50);
460+
final int shard2Queries = randomIntBetween(5, 10);
461+
462+
for (int i = 0; i < shard1Queries; ++i) {
463+
shard1Segment1Searcher.count(new DummyQuery("ingest1-" + i, largeQuerySize));
464+
}
465+
long shard1Segment1CacheMemory = calculateActualCacheMemoryForSegment(shard1Queries, largeQuerySize, shard1Segment1Docs);
466+
assertThat(cache.getStats(shard1).getMemorySizeInBytes(), equalTo(shard1Segment1CacheMemory));
467+
assertThat(cache.getStats(shard2).getMemorySizeInBytes(), equalTo(0L));
468+
for (int i = 0; i < shard2Queries; ++i) {
469+
shard2Searcher.count(new DummyQuery("ingest2-" + i, smallQuerySize));
470+
}
471+
/*
472+
* Now that we have cached some smaller things for shard2, the cache memory for shard1 has gone down. This is expected because we
473+
* report cache memory proportional to the number of segments for each shard, ignoring the number of documents or the actual
474+
* document sizes. Since the shard2 requests were smaller, the average cache memory size per segment has now gone down.
475+
*/
476+
assertThat(cache.getStats(shard1).getMemorySizeInBytes(), lessThan(shard1Segment1CacheMemory));
477+
long shard1CacheBytes = cache.getStats(shard1).getMemorySizeInBytes();
478+
long shard2CacheBytes = cache.getStats(shard2).getMemorySizeInBytes();
479+
long shard2Segment1CacheMemory = calculateActualCacheMemoryForSegment(shard2Queries, smallQuerySize, shard2Segment1Docs);
480+
481+
long totalMemory = shard1Segment1CacheMemory + shard2Segment1CacheMemory;
482+
// Each shard has some fixed overhead that we need to account for:
483+
long shard1Overhead = calculateOverheadForSegment(shard1Queries, shard1Segment1Docs);
484+
long shard2Overhead = calculateOverheadForSegment(shard2Queries, shard2Segment1Docs);
485+
long totalMemoryMinusOverhead = totalMemory - (shard1Overhead + shard2Overhead);
486+
/*
487+
* Note that the expected amount of memory we're calculating is based on the proportion of the number of queries to each shard
488+
* (since each shard currently only has queries to one segment)
489+
*/
490+
double shard1Segment1CacheMemoryShare = ((double) shard1Queries / (shard1Queries + shard2Queries)) * (totalMemoryMinusOverhead)
491+
+ shard1Overhead;
492+
double shard2Segment1CacheMemoryShare = ((double) shard2Queries / (shard1Queries + shard2Queries)) * (totalMemoryMinusOverhead)
493+
+ shard2Overhead;
494+
assertThat((double) shard1CacheBytes, closeTo(shard1Segment1CacheMemoryShare, 1)); // accounting for rounding
495+
assertThat((double) shard2CacheBytes, closeTo(shard2Segment1CacheMemoryShare, 1)); // accounting for rounding
496+
497+
// Now we cache just more "big" searches on shard1, but on a different segment:
498+
for (int i = 0; i < shard1Queries; ++i) {
499+
shard1Segment2Searcher.count(new DummyQuery("ingest3-" + i, largeQuerySize));
500+
}
501+
long shard1Segment2CacheMemory = calculateActualCacheMemoryForSegment(shard1Queries, largeQuerySize, shard1Segment2Docs);
502+
totalMemory = shard1Segment1CacheMemory + shard2Segment1CacheMemory + shard1Segment2CacheMemory;
503+
// Each shard has some fixed overhead that we need to account for:
504+
shard1Overhead = shard1Overhead + calculateOverheadForSegment(shard1Queries, shard1Segment2Docs);
505+
totalMemoryMinusOverhead = totalMemory - (shard1Overhead + shard2Overhead);
506+
/*
507+
* Note that the expected amount of memory we're calculating is based on the proportion of the number of queries to each segment.
508+
* The number of documents and the size of documents is irrelevant (aside from computing the fixed overhead).
509+
*/
510+
shard1Segment1CacheMemoryShare = ((double) (2 * shard1Queries) / ((2 * shard1Queries) + shard2Queries)) * (totalMemoryMinusOverhead)
511+
+ shard1Overhead;
512+
shard1CacheBytes = cache.getStats(shard1).getMemorySizeInBytes();
513+
assertThat((double) shard1CacheBytes, closeTo(shard1Segment1CacheMemoryShare, 1)); // accounting for rounding
514+
515+
// Now make sure the cache only has items for shard2:
516+
for (int i = 0; i < (maxCacheSize * 2); ++i) {
517+
shard2Searcher.count(new DummyQuery("ingest4-" + i, smallQuerySize));
518+
}
519+
assertThat(cache.getStats(shard1).getMemorySizeInBytes(), equalTo(0L));
520+
assertThat(
521+
cache.getStats(shard2).getMemorySizeInBytes(),
522+
equalTo(calculateActualCacheMemoryForSegment(maxCacheSize, smallQuerySize, shard2Segment1Docs))
523+
);
524+
525+
IOUtils.close(closeableList);
526+
cache.onClose(shard1);
527+
cache.onClose(shard2);
528+
cache.close();
529+
}
530+
531+
/*
532+
* This calculates the memory that actually used by a segment in the IndicesQueryCache. It assumes queryCount queries are made to the
533+
* segment, and query is querySize bytes in size. It assumes that the shard contains numDocs documents.
534+
*/
535+
private long calculateActualCacheMemoryForSegment(long queryCount, long querySize, long numDocs) {
536+
return (queryCount * (querySize + 24)) + calculateOverheadForSegment(queryCount, numDocs);
537+
}
538+
539+
/*
540+
* This computes the part of the recorded IndicesQueryCache memory that is assigned to a segment and *not* divided up proportionally
541+
* when the cache reports the memory usage of each shard.
542+
*/
543+
private long calculateOverheadForSegment(long queryCount, long numDocs) {
544+
return queryCount * (112 + (8 * ((numDocs - 1) / 64)));
545+
}
546+
547+
/*
548+
* This returns an IndexSearcher for a single new segment in the given shard.
549+
*/
550+
private IndexSearcher initializeSegment(ShardId shard, int numDocs, List<Closeable> closeableList) throws Exception {
551+
AtomicReference<IndexSearcher> indexSearcherReference = new AtomicReference<>();
552+
/*
553+
* Usually creating an IndexWriter like this results in a single segment getting created, but sometimes it results in more. For the
554+
* sake of keeping the calculations in this test simple we want just a single segment. So we do this in an assertBusy.
555+
*/
556+
assertBusy(() -> {
557+
Directory dir = newDirectory();
558+
IndexWriter indexWriter = new IndexWriter(dir, newIndexWriterConfig());
559+
for (int i = 0; i < numDocs; i++) {
560+
indexWriter.addDocument(new Document());
561+
}
562+
DirectoryReader directoryReader = DirectoryReader.open(indexWriter);
563+
indexWriter.close();
564+
directoryReader = ElasticsearchDirectoryReader.wrap(directoryReader, shard);
565+
IndexSearcher indexSearcher = new IndexSearcher(directoryReader);
566+
indexSearcherReference.set(indexSearcher);
567+
indexSearcher.setQueryCachingPolicy(TrivialQueryCachingPolicy.ALWAYS);
568+
closeableList.add(directoryReader);
569+
closeableList.add(dir);
570+
assertThat(indexSearcher.getLeafContexts().size(), equalTo(1));
571+
});
572+
return indexSearcherReference.get();
573+
}
400574
}

0 commit comments

Comments
 (0)