-
Notifications
You must be signed in to change notification settings - Fork 24.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Optimize usage calculation in ILM policies retrieval API #106953
base: main
Are you sure you want to change the base?
Conversation
Pinging @elastic/es-data-management (Team:Data Management) |
Hi @nielsbauman, I've created a changelog YAML for you. |
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyUtils.java
Show resolved
Hide resolved
I really like the idea of reducing the number of templates we are going through to speed up things. However, I am afraid this optimisation is not correct, I created a test that demonstrates the bug:
In this test we see a data stream whose We can explore more ideas like this, for example:
|
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
Outdated
Show resolved
Hide resolved
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
Show resolved
Hide resolved
/** A map from policy name to list of data streams that use that policy. */ | ||
private final Map<String, List<String>> policyToDataStream; | ||
/** A map from composable template name to the policy name it uses (or null) */ | ||
private final Map<String, String> templateToPolicy; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The templateToPolicy
map is basically only there to save some MetadataIndexTemplateService.resolveSettings
calls (as we have to loop over all templates in calculateUsage
anyway), but resolving the settings seemed to be relatively expensive, so templateToPolicy
basically serves as a cache for that.
List<String> indices = new ArrayList<>(); | ||
for (IndexMetadata indexMetadata : state.metadata().indices().values()) { | ||
if (policyName.equals(indexMetadata.getLifecyclePolicyName())) { | ||
indices.add(indexMetadata.getIndex().getName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could pre-compute a map of policy to indices as well, but I'm not sure the memory vs. speed trade-off is worth it there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thought, since we have to build this list for every policy anyway, pre-computing these lists and putting them in a map shouldn't be too much additional memory overhead. I'll wait till someone has done a first review before making more changes (in case my whole approach is off).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And the same goes for the composable templates of course.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's think about this. You only need to keep a cache of what is necessary. Right?
For example:
Composable templates:
Cache the resolved templates that match our policies and their index patterns: template -> policy
, probably you can also store policy -> templates
.
Data streams
Go over the data streams like you already do, find the template for each data stream and check the cache template -> policy
. You do not need to keep track of nulls anymore, since you have collected the relevant templates, if the template is not in your cache then the data stream shouldn't be either. So you create the policy -> data stream
.
Indices
I do not see an issue with going over the indices during initialisation because from what I saw the information is pre-calculated and then serialised. So, here you create policy -> indices
.
Then retrieving the data is just picking them up from the cache. Thoughts?
...core/src/test/java/org/elasticsearch/xpack/core/ilm/LifecyclePolicyUsageCalculatorTests.java
Show resolved
Hide resolved
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
Outdated
Show resolved
Hide resolved
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
Show resolved
Hide resolved
@@ -1050,29 +1051,24 @@ static Set<String> dataStreamsExclusivelyUsingTemplates(final ClusterState state | |||
.reduce(Sets::union) | |||
.orElse(Set.of()); | |||
|
|||
// Filter and sort all composable templates in advance, to speed up template retrieval later on. | |||
var templates = state.metadata() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this confused me a little bit, both the name and the comment. It wasn't clear to me what is filtered and when we use the word templates, which templates do we mean. So, what if we rename:
templateNames
torequestedTemplateNames
, signalling that these was provided by the caller.templates
torestTemplateCandidates
orotherTemplateCandidates
, signalling that these are the rest, so not the ones provided by the user and candidates becauseisGlobalAndHasIndexHiddenSetting
is filtering out the non candidates.
Names are up for debate but I hope I made clear the direction I am considering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I've renamed the templates
variable and updated the comments. Let me know if this is more clear.
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
Outdated
Show resolved
Hide resolved
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
Outdated
Show resolved
Hide resolved
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
Outdated
Show resolved
Hide resolved
final List<Tuple<String, ComposableIndexTemplate>> candidates = new ArrayList<>(); | ||
for (Map.Entry<String, ComposableIndexTemplate> entry : metadata.templatesV2().entrySet()) { | ||
outerLoop: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm..... I am not going to lie, seeing this does not give me a good feeling. Is this really the best alternative to structure the code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I've found a much better alternative, see 293ff9d. Let me know whether you agree that this approach is better.
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
Outdated
Show resolved
Hide resolved
server/src/main/java/org/elasticsearch/cluster/metadata/MetadataIndexTemplateService.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apologies for the late review! Really happy to see this work @nielsbauman, this is awesome! Let me know what you think on the comments.
I like the direction that you have taken :)
public static String findV2Template( | ||
Metadata metadata, | ||
Collection<Map.Entry<String, ComposableIndexTemplate>> templates, | ||
String indexName, | ||
boolean isHidden, | ||
boolean exitOnFirstMatch | ||
) { | ||
final List<Tuple<String, ComposableIndexTemplate>> candidates = findV2CandidateTemplates( | ||
templates, | ||
indexName, | ||
isHidden, | ||
exitOnFirstMatch | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you like the idea of more explicit method names like I suggested in the previous comment. We could change the visibility of this method to private or package private depending on the testing requirements. This way this method won't be misused by setting exitOnFirstMatch
with an unordered template list.
What do you think?
if (matched) { | ||
candidates.add(Tuple.tuple(name, template)); | ||
if (isHidden) { | ||
final boolean hasMatchAllTemplate = template.indexPatterns().stream().anyMatch(Regex::isMatchAllPattern); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: hasMatchAllTemplate
I think this reads better as isMatchAllTemplate
because it talks about the template itself. Otherwise it should be hasMatchAllPattern
.
.reduce(Sets::union) | ||
.orElse(Set.of()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I know this is not your code but since it's in my visual I wanted to add a small note.
I think this code is a bit more complex than it has to be.
.map(Set::copyOf)
.reduce(Sets::union)
.orElse(Set.of())
Effectively we want to add all patterns into a set, right? I think the following code is a bit more readable.
.flatMap(List::stream)
.collect(Collectors.toSet())
What do you think?
* Return an ordered list of the name (id) and composable index templates that would apply to an index. The first | ||
* one is the winner template that is applied to this index. In the event that no templates are matched, | ||
* an empty list is returned. | ||
* an empty list is returned. If <code>exitOnFirstMatch</code> is true, we return immediately after finding a match. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we need to elaborate a bit more on how to use this method. What are the trade-offs of exitOnFirstMatch
. How it should be used etc.
if (indexTemplate == null) { | ||
continue; | ||
} | ||
Settings settings = MetadataIndexTemplateService.resolveSettings(state.metadata(), indexTemplate); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before doing this, shouldn't we check if the template is already in templateToPolicy
, then we do not have to resolve the settings again?
.build(); | ||
assertThat( | ||
new LifecyclePolicyUsageCalculator(iner, state, List.of("mypolicy")).calculateUsage("mypolicy"), | ||
equalTo(new ItemUsage(Collections.emptyList(), Collections.emptyList(), Collections.emptyList())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
equalTo(new ItemUsage(Collections.emptyList(), Collections.emptyList(), Collections.emptyList())) | |
equalTo(new ItemUsage(List.of(), List.of(), List.of())) |
.build(); | ||
assertThat( | ||
new LifecyclePolicyUsageCalculator(iner, state, List.of("mypolicy")).calculateUsage("mypolicy"), | ||
equalTo(new ItemUsage(Collections.singleton("myindex"), Collections.emptyList(), Collections.emptyList())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
equalTo(new ItemUsage(Collections.singleton("myindex"), Collections.emptyList(), Collections.emptyList())) | |
equalTo(new ItemUsage(List.of("myindex"), List.of(), List.of())) |
public LifecyclePolicyUsageCalculator( | ||
final IndexNameExpressionResolver indexNameExpressionResolver, | ||
final ClusterState state, | ||
List<String> names |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because we have a lot of elements involved here, I think it would be easier to specify what are these names
. I think policyNames
or policies
is a bit more informative.
What do you think?
public static String findV2Template(Metadata metadata, String indexName, boolean isHidden) { | ||
final List<Tuple<String, ComposableIndexTemplate>> candidates = findV2CandidateTemplates(metadata, indexName, isHidden); | ||
return findV2Template(metadata, metadata.templatesV2().entrySet(), indexName, isHidden, false); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if we create one more method called: findV2TemplateFromSortedList
which will call findV2Template(metadata, metadata.templatesV2().entrySet(), indexName, isHidden, true)
and we can document that if you want to retrieve the templates of multiple targets you can provide a sorted list of templates which will speed things up.
I think this will clarify a bit more which method to use and it will make explicit that the templates need to be ordered based on priority.
List<String> indices = new ArrayList<>(); | ||
for (IndexMetadata indexMetadata : state.metadata().indices().values()) { | ||
if (policyName.equals(indexMetadata.getLifecyclePolicyName())) { | ||
indices.add(indexMetadata.getIndex().getName()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's think about this. You only need to keep a cache of what is necessary. Right?
For example:
Composable templates:
Cache the resolved templates that match our policies and their index patterns: template -> policy
, probably you can also store policy -> templates
.
Data streams
Go over the data streams like you already do, find the template for each data stream and check the cache template -> policy
. You do not need to keep track of nulls anymore, since you have collected the relevant templates, if the template is not in your cache then the data stream shouldn't be either. So you create the policy -> data stream
.
Indices
I do not see an issue with going over the indices during initialisation because from what I saw the information is pre-calculated and then serialised. So, here you create policy -> indices
.
Then retrieving the data is just picking them up from the cache. Thoughts?
Optimize calculating the usage of ILM policies in the
GET _ilm/policy
andGET _ilm/policy/<policy_id>
endpoints. I loaded some dummy data into my single-node locally-running ES cluster using the following script (numbers are inspired by a customer who was facing timeouts in Kibana due to this endpoint being slow):Bash snippet
I also played around with some aspects of the script such as making the templates use policies or not, as that represents different scenarios that users might face.
I generated a flamegraph on
main
, which showed that theMetadataIndexTemplateService.findV2Template(...)
call was by far the most expensive.Flamegraph
After some experimentation, I ended up extracting a separate class that pre-computes some parts on initialization (i.e. only once per request) and then uses those pre-computed parts when calculating the usage for an individual policy. This is of course a trade-off between memory usage and run-time performance. But, I think the added memory usage (which shouldn't be crazy much) is worth the significant speed improvement here.
Fixes #105773