Skip to content

Commit 95b23fc

Browse files
authored
Reduce memory usage in field-caps responses (#88042)
We have reduced the memory usage of field-caps requests targeting many indices in 8.2+ (see #83494). Unfortunately, we still receive OOM reports in 7.17. I think we should push some contained improvements to reduce the memory usage for those requests in 7.17. I have looked into several options. This PR reduces the memory usage of field-caps responses by replace HashMap with ArrayList for the field responses to eliminate duplicated string names and internal nodes of Map.
1 parent 5e2915c commit 95b23fc

File tree

8 files changed

+126
-50
lines changed

8 files changed

+126
-50
lines changed

docs/changelog/88042.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 88042
2+
summary: Reduce memory usage in field-caps responses
3+
area: Search
4+
type: bug
5+
issues: []

server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesFetcher.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public FieldCapabilitiesIndexResponse fetch(final FieldCapabilitiesIndexRequest
5858
);
5959

6060
if (canMatchShard(request, searchExecutionContext) == false) {
61-
return new FieldCapabilitiesIndexResponse(request.index(), Collections.emptyMap(), false);
61+
return new FieldCapabilitiesIndexResponse(request.index(), Collections.emptyList(), false);
6262
}
6363

6464
Set<String> fieldNames = new HashSet<>();
@@ -118,7 +118,7 @@ public FieldCapabilitiesIndexResponse fetch(final FieldCapabilitiesIndexRequest
118118
}
119119
}
120120
}
121-
return new FieldCapabilitiesIndexResponse(request.index(), responseMap, true);
121+
return new FieldCapabilitiesIndexResponse(request.index(), responseMap.values(), true);
122122
}
123123
}
124124

server/src/main/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesIndexResponse.java

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,29 @@
1515
import org.elasticsearch.common.io.stream.Writeable;
1616

1717
import java.io.IOException;
18-
import java.util.Map;
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.List;
1922
import java.util.Objects;
2023

2124
public class FieldCapabilitiesIndexResponse extends ActionResponse implements Writeable {
2225
private final String indexName;
23-
private final Map<String, IndexFieldCapabilities> responseMap;
26+
private final Collection<IndexFieldCapabilities> fields;
2427
private final boolean canMatch;
2528
private final transient Version originVersion;
2629

27-
FieldCapabilitiesIndexResponse(String indexName, Map<String, IndexFieldCapabilities> responseMap, boolean canMatch) {
30+
FieldCapabilitiesIndexResponse(String indexName, Collection<IndexFieldCapabilities> fields, boolean canMatch) {
2831
this.indexName = indexName;
29-
this.responseMap = responseMap;
32+
this.fields = fields;
3033
this.canMatch = canMatch;
3134
this.originVersion = Version.CURRENT;
3235
}
3336

3437
FieldCapabilitiesIndexResponse(StreamInput in) throws IOException {
3538
super(in);
3639
this.indexName = in.readString();
37-
this.responseMap = in.readMap(StreamInput::readString, IndexFieldCapabilities::new);
40+
this.fields = readFields(in);
3841
this.canMatch = in.getVersion().onOrAfter(Version.V_7_9_0) ? in.readBoolean() : true;
3942
this.originVersion = in.getVersion();
4043
}
@@ -51,18 +54,35 @@ public boolean canMatch() {
5154
}
5255

5356
/**
54-
* Get the field capabilities map
57+
* Get the field capabilities
5558
*/
56-
public Map<String, IndexFieldCapabilities> get() {
57-
return responseMap;
59+
public Collection<IndexFieldCapabilities> getFields() {
60+
return fields;
5861
}
5962

60-
/**
61-
*
62-
* Get the field capabilities for the provided {@code field}
63-
*/
64-
public IndexFieldCapabilities getField(String field) {
65-
return responseMap.get(field);
63+
private static Collection<IndexFieldCapabilities> readFields(StreamInput in) throws IOException {
64+
// Previously, we serialize fields as a map from field name to field-caps
65+
final int size = in.readVInt();
66+
if (size == 0) {
67+
return Collections.emptyList();
68+
}
69+
final List<IndexFieldCapabilities> fields = new ArrayList<>(size);
70+
for (int i = 0; i < size; i++) {
71+
final String fieldName = in.readString(); // the fieldName will be discarded - it's used in assertions only
72+
final IndexFieldCapabilities fieldCaps = new IndexFieldCapabilities(in);
73+
assert fieldName.equals(fieldCaps.getName()) : fieldName + " != " + fieldCaps.getName();
74+
fields.add(fieldCaps);
75+
}
76+
return fields;
77+
}
78+
79+
private static void writeFields(StreamOutput out, Collection<IndexFieldCapabilities> fields) throws IOException {
80+
// Previously, we serialize fields as a map from field name to field-caps
81+
out.writeVInt(fields.size());
82+
for (IndexFieldCapabilities field : fields) {
83+
out.writeString(field.getName());
84+
field.writeTo(out);
85+
}
6686
}
6787

6888
Version getOriginVersion() {
@@ -72,7 +92,7 @@ Version getOriginVersion() {
7292
@Override
7393
public void writeTo(StreamOutput out) throws IOException {
7494
out.writeString(indexName);
75-
out.writeMap(responseMap, StreamOutput::writeString, (valueOut, fc) -> fc.writeTo(valueOut));
95+
writeFields(out, fields);
7696
if (out.getVersion().onOrAfter(Version.V_7_9_0)) {
7797
out.writeBoolean(canMatch);
7898
}
@@ -83,11 +103,11 @@ public boolean equals(Object o) {
83103
if (this == o) return true;
84104
if (o == null || getClass() != o.getClass()) return false;
85105
FieldCapabilitiesIndexResponse that = (FieldCapabilitiesIndexResponse) o;
86-
return canMatch == that.canMatch && Objects.equals(indexName, that.indexName) && Objects.equals(responseMap, that.responseMap);
106+
return canMatch == that.canMatch && Objects.equals(indexName, that.indexName) && Objects.equals(fields, that.fields);
87107
}
88108

89109
@Override
90110
public int hashCode() {
91-
return Objects.hash(indexName, responseMap, canMatch);
111+
return Objects.hash(indexName, fields, canMatch);
92112
}
93113
}

server/src/main/java/org/elasticsearch/action/fieldcaps/TransportFieldCapabilitiesAction.java

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ protected void doExecute(Task task, FieldCapabilitiesRequest request, final Acti
147147
remoteClusterClient.fieldCaps(remoteRequest, ActionListener.wrap(response -> {
148148
for (FieldCapabilitiesIndexResponse resp : response.getIndexResponses()) {
149149
String indexName = RemoteClusterAware.buildRemoteIndexName(clusterAlias, resp.getIndexName());
150-
indexResponses.putIfAbsent(indexName, new FieldCapabilitiesIndexResponse(indexName, resp.get(), resp.canMatch()));
150+
indexResponses.putIfAbsent(indexName, new FieldCapabilitiesIndexResponse(indexName, resp.getFields(), resp.canMatch()));
151151
}
152152
for (FieldCapabilitiesFailure failure : response.getFailures()) {
153153
Exception ex = failure.getException();
@@ -265,17 +265,16 @@ private void innerMerge(
265265
Map<String, Map<String, FieldCapabilities.Builder>> responseMapBuilder,
266266
FieldCapabilitiesIndexResponse response
267267
) {
268-
for (Map.Entry<String, IndexFieldCapabilities> entry : response.get().entrySet()) {
269-
final String field = entry.getKey();
268+
for (IndexFieldCapabilities fieldCap : response.getFields()) {
269+
final String fieldName = fieldCap.getName();
270270
// best effort to detect metadata field coming from older nodes
271271
final boolean isMetadataField = response.getOriginVersion().onOrAfter(Version.V_7_13_0)
272-
? entry.getValue().isMetadatafield()
273-
: metadataFieldPred.test(field);
274-
final IndexFieldCapabilities fieldCap = entry.getValue();
275-
Map<String, FieldCapabilities.Builder> typeMap = responseMapBuilder.computeIfAbsent(field, f -> new HashMap<>());
272+
? fieldCap.isMetadatafield()
273+
: metadataFieldPred.test(fieldName);
274+
Map<String, FieldCapabilities.Builder> typeMap = responseMapBuilder.computeIfAbsent(fieldName, f -> new HashMap<>());
276275
FieldCapabilities.Builder builder = typeMap.computeIfAbsent(
277276
fieldCap.getType(),
278-
key -> new FieldCapabilities.Builder(field, key)
277+
key -> new FieldCapabilities.Builder(fieldName, key)
279278
);
280279
builder.add(response.getIndexName(), isMetadataField, fieldCap.isSearchable(), fieldCap.isAggregatable(), fieldCap.meta());
281280
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.action.fieldcaps;
10+
11+
import org.elasticsearch.common.io.stream.StreamInput;
12+
import org.elasticsearch.common.io.stream.Writeable;
13+
import org.elasticsearch.test.AbstractWireSerializingTestCase;
14+
15+
import java.util.ArrayList;
16+
import java.util.Base64;
17+
import java.util.Collections;
18+
import java.util.List;
19+
20+
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomAsciiLettersOfLength;
21+
import static org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponseTests.randomFieldCaps;
22+
import static org.hamcrest.Matchers.containsInAnyOrder;
23+
import static org.hamcrest.Matchers.equalTo;
24+
25+
public class FieldCapabilitiesIndexResponseTests extends AbstractWireSerializingTestCase<FieldCapabilitiesIndexResponse> {
26+
@Override
27+
protected Writeable.Reader<FieldCapabilitiesIndexResponse> instanceReader() {
28+
return FieldCapabilitiesIndexResponse::new;
29+
}
30+
31+
static FieldCapabilitiesIndexResponse randomIndexResponse() {
32+
return randomIndexResponse(randomAsciiLettersOfLength(10), randomBoolean());
33+
}
34+
35+
static FieldCapabilitiesIndexResponse randomIndexResponse(String index, boolean canMatch) {
36+
List<IndexFieldCapabilities> fields = new ArrayList<>();
37+
if (canMatch) {
38+
String[] fieldNames = generateRandomStringArray(5, 10, false, true);
39+
assertNotNull(fieldNames);
40+
for (String fieldName : fieldNames) {
41+
fields.add(randomFieldCaps(fieldName));
42+
}
43+
}
44+
return new FieldCapabilitiesIndexResponse(index, fields, canMatch);
45+
}
46+
47+
@Override
48+
protected FieldCapabilitiesIndexResponse createTestInstance() {
49+
return randomIndexResponse();
50+
}
51+
52+
public void testDeserializeFromBase64() throws Exception {
53+
String base64 = "CWxvZ3MtMTAwMQMGcGVyaW9kBnBlcmlvZARsb25nAQABAQR1bml0BnNlY29uZApAdGltZXN0"
54+
+ "YW1wCkB0aW1lc3RhbXAEZGF0ZQEBAAAHbWVzc2FnZQdtZXNzYWdlBHRleHQAAQAAAQAAAAAAAAAAAA==";
55+
StreamInput in = StreamInput.wrap(Base64.getDecoder().decode(base64));
56+
FieldCapabilitiesIndexResponse resp = new FieldCapabilitiesIndexResponse(in);
57+
assertTrue(resp.canMatch());
58+
assertThat(resp.getIndexName(), equalTo("logs-1001"));
59+
assertThat(
60+
resp.getFields(),
61+
containsInAnyOrder(
62+
new IndexFieldCapabilities("@timestamp", "date", true, true, false, Collections.emptyMap()),
63+
new IndexFieldCapabilities("message", "text", false, true, false, Collections.emptyMap()),
64+
new IndexFieldCapabilities("period", "long", true, false, true, Collections.singletonMap("unit", "second"))
65+
)
66+
);
67+
}
68+
}

server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesNodeResponseTests.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@
1818
import java.util.List;
1919
import java.util.Set;
2020

21+
import static org.elasticsearch.action.fieldcaps.FieldCapabilitiesIndexResponseTests.randomIndexResponse;
22+
2123
public class FieldCapabilitiesNodeResponseTests extends AbstractWireSerializingTestCase<FieldCapabilitiesNodeResponse> {
2224

2325
@Override
2426
protected FieldCapabilitiesNodeResponse createTestInstance() {
2527
List<FieldCapabilitiesIndexResponse> responses = new ArrayList<>();
2628
int numResponse = randomIntBetween(0, 10);
2729
for (int i = 0; i < numResponse; i++) {
28-
responses.add(FieldCapabilitiesResponseTests.createRandomIndexResponse());
30+
responses.add(randomIndexResponse());
2931
}
3032
int numUnmatched = randomIntBetween(0, 3);
3133
Set<ShardId> shardIds = new HashSet<>();
@@ -46,15 +48,15 @@ protected FieldCapabilitiesNodeResponse mutateInstance(FieldCapabilitiesNodeResp
4648
int mutation = response.getIndexResponses().isEmpty() ? 0 : randomIntBetween(0, 2);
4749
switch (mutation) {
4850
case 0:
49-
newResponses.add(FieldCapabilitiesResponseTests.createRandomIndexResponse());
51+
newResponses.add(randomIndexResponse());
5052
break;
5153
case 1:
5254
int toRemove = randomInt(newResponses.size() - 1);
5355
newResponses.remove(toRemove);
5456
break;
5557
case 2:
5658
int toReplace = randomInt(newResponses.size() - 1);
57-
newResponses.set(toReplace, FieldCapabilitiesResponseTests.createRandomIndexResponse());
59+
newResponses.set(toReplace, randomIndexResponse());
5860
break;
5961
}
6062
return new FieldCapabilitiesNodeResponse(newResponses, Collections.emptyMap(), response.getUnmatchedShardIds());

server/src/test/java/org/elasticsearch/action/fieldcaps/FieldCapabilitiesResponseTests.java

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@
2929
import java.util.List;
3030
import java.util.Map;
3131

32-
import static com.carrotsearch.randomizedtesting.RandomizedTest.randomAsciiLettersOfLength;
33-
3432
public class FieldCapabilitiesResponseTests extends AbstractWireSerializingTestCase<FieldCapabilitiesResponse> {
3533

3634
@Override
@@ -40,7 +38,7 @@ protected FieldCapabilitiesResponse createTestInstance() {
4038
int numResponse = randomIntBetween(0, 10);
4139

4240
for (int i = 0; i < numResponse; i++) {
43-
responses.add(createRandomIndexResponse());
41+
responses.add(FieldCapabilitiesIndexResponseTests.randomIndexResponse());
4442
}
4543
randomResponse = new FieldCapabilitiesResponse(responses, Collections.emptyList());
4644
return randomResponse;
@@ -51,22 +49,6 @@ protected Writeable.Reader<FieldCapabilitiesResponse> instanceReader() {
5149
return FieldCapabilitiesResponse::new;
5250
}
5351

54-
public static FieldCapabilitiesIndexResponse createRandomIndexResponse() {
55-
return randomIndexResponse(randomAsciiLettersOfLength(10), randomBoolean());
56-
}
57-
58-
public static FieldCapabilitiesIndexResponse randomIndexResponse(String index, boolean canMatch) {
59-
Map<String, IndexFieldCapabilities> responses = new HashMap<>();
60-
61-
String[] fields = generateRandomStringArray(5, 10, false, true);
62-
assertNotNull(fields);
63-
64-
for (String field : fields) {
65-
responses.put(field, randomFieldCaps(field));
66-
}
67-
return new FieldCapabilitiesIndexResponse(index, responses, canMatch);
68-
}
69-
7052
public static IndexFieldCapabilities randomFieldCaps(String fieldName) {
7153
Map<String, String> meta;
7254
switch (randomInt(2)) {

server/src/test/java/org/elasticsearch/action/fieldcaps/RequestDispatcherTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
import java.util.stream.Collectors;
8282
import java.util.stream.IntStream;
8383

84-
import static org.elasticsearch.action.fieldcaps.FieldCapabilitiesResponseTests.randomIndexResponse;
84+
import static org.elasticsearch.action.fieldcaps.FieldCapabilitiesIndexResponseTests.randomIndexResponse;
8585
import static org.elasticsearch.action.fieldcaps.RequestDispatcher.GROUP_REQUESTS_VERSION;
8686
import static org.hamcrest.Matchers.anEmptyMap;
8787
import static org.hamcrest.Matchers.equalTo;

0 commit comments

Comments
 (0)