-
Notifications
You must be signed in to change notification settings - Fork 25.3k
Implement setting deduplication via String interning #80493
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
Implement setting deduplication via String interning #80493
Conversation
This is a somewhat crude solution to elastic#78892 that addresses 95%+ of duplicate setting entry memory consumption in large clusters. The remaining duplicate structures (lists of all the same strings) are comparatively cheap in their heap consumption. In heavy benchmarking for elastic#77466 no runtime impact of adding this extra step to setting creation has been found despite pushing setting creation harder than is expected in real-world usage (part of the low relative impact here is the fact that populating a tree-map is quite expensive to begin with so adding the string interning which is fast via the CHM cache doesn't add much overhead). On the other hand, the heap use impact for use-cases that come with a large number of duplicate settings (many similar indices) is significant. As an example, 10k AuditBeat indices consume about 500M of heap for duplicate settings data structures without this change. This cahnge brings the heap consumption from duplicate settings down to O(1M) on every node in the cluster. Relates and addresses most of elastic#78892 Relates elastic#77466
Pinging @elastic/es-core-infra (Team:Core/Infra) |
@@ -20,8 +18,6 @@ | |||
*/ | |||
public final class StringLiteralDeduplicator { | |||
|
|||
private static final Logger logger = LogManager.getLogger(StringLiteralDeduplicator.class); |
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.
We use this thing before the logging is fully initialized now in some cases leading to the same issue we resolve by indirection in org.elasticsearch.common.settings.Settings.DeprecationLoggerHolder
. I don't think the logging here is valuable enough to go for any kind of sophisticated solution to keep it around so I just dropped it.
Also relates #80348 |
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.
LGTM!
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 would like to have a test added that verifies (and ensures we preserve) the sharing of strings. Perhaps an integ-test, with two nodes and two indices, verifying that a few keys/values from the index-settings are shared between the indices on both nodes. Otherwise this looks good to me.
@@ -99,7 +100,43 @@ private static Settings of(Map<String, Object> settings, SecureSettings secureSe | |||
|
|||
private Settings(Map<String, Object> settings, SecureSettings secureSettings) { | |||
// we use a sorted map for consistent serialization when using getAsMap() | |||
this.settings = Collections.unmodifiableNavigableMap(new TreeMap<>(settings)); |
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 would prefer this in Builder.build()
. Seems more logical and also, AFAICS, that is the only path we need to cover.
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 found it a little easier to reason about to keep it here, so we are always guaranteed to get a fully immutable structure now (also for lists etc.).
Also technically org.elasticsearch.common.settings.Settings#getByPrefix
which uses Settings.of(
profits from this change as well so maybe just keep it here in the constructor to keep things simple and have this apply in all cases? We could look into some optimizations in a follow-up maybe, but generally as discussed elsewhere setting setup isn't a hot path issue.
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 mainly found it illogical that we do not fully build the parameters for the constructor in the build method like we normally do. I would still prefer it the other way around, but can accept it residing here for now.
I am not sure I found a place where getByPrefix
needs this, they all look like "get and throw away" style lookups, perhaps you have a specific case. Even if there are such cases, it seems unnecessary to go through the lists once more when all we need is interned keys in that case.
} | ||
} | ||
if (notInternedOffset >= 0) { | ||
final String[] internedArr = new String[listSize]; |
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 wonder if we should not just copy the list every time, interning it, rather than this optimization? The only thing we are saving is this array, but we still copy the list anyway so hardly seems worth the complexity added here.
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 reason I made it this way actually wasn't to save work in the constructor. I was going for re-using the full list object if all its contents are interned already. I had some concrete and short ideas for exploiting that for another round of savings on the settings and I suppose it already does do some saving today.
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 would prefer to omit this then. As mentioned above, this is not on the hot path. And if we want to optimize it further, it seems more intuitive to do so in the builder, since we could intern the strings in the list in putList
and assume all lists are interned (and immutable) always (and likewise for keys).
If follow-up work requires this we can re-add it, but I find this too much of half an optimization in the current state.
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.
Sounds good, always copying for now :)
Thanks @henningandersen I added a test that checks that we actually intern all these strings but had some reservations on the code changes. Let me know what you think :) |
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.
LGTM, thanks for adding the test.
assertEqualsAndStringsInterned(queryFieldsSetting, updatedIndex1SettingsMaster); | ||
assertEqualsAndStringsInterned(queryFieldsSetting, updatedIndex1SettingsData); | ||
assertEqualsAndStringsInterned(queryFieldsSetting, clusterServiceMaster.state().metadata().index(index2).getSettings()); | ||
assertEqualsAndStringsInterned(queryFieldsSetting, clusterServiceData.state().metadata().index(index2).getSettings()); |
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.
Can we also assert that a key is interned? Could just check that the key on index1 is the same as on index2.
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.
Sure, I actually just made it assert that all of the keyset is interned strings on these settings now. That was easier (hard to get a reference to a setting key directly) and stricter :)
} | ||
} | ||
if (notInternedOffset >= 0) { | ||
final String[] internedArr = new String[listSize]; |
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 would prefer to omit this then. As mentioned above, this is not on the hot path. And if we want to optimize it further, it seems more intuitive to do so in the builder, since we could intern the strings in the list in putList
and assume all lists are interned (and immutable) always (and likewise for keys).
If follow-up work requires this we can re-add it, but I find this too much of half an optimization in the current state.
@@ -99,7 +100,43 @@ private static Settings of(Map<String, Object> settings, SecureSettings secureSe | |||
|
|||
private Settings(Map<String, Object> settings, SecureSettings secureSettings) { | |||
// we use a sorted map for consistent serialization when using getAsMap() | |||
this.settings = Collections.unmodifiableNavigableMap(new TreeMap<>(settings)); |
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 mainly found it illogical that we do not fully build the parameters for the constructor in the build method like we normally do. I would still prefer it the other way around, but can accept it residing here for now.
I am not sure I found a place where getByPrefix
needs this, they all look like "get and throw away" style lookups, perhaps you have a specific case. Even if there are such cases, it seems unnecessary to go through the lists once more when all we need is interned keys in that case.
Thanks Nikolai + Henning! |
This is a somewhat crude solution to elastic#78892 that addresses 95%+ of duplicate setting entry memory consumption in large clusters. The remaining duplicate structures (lists of all the same strings) are comparatively cheap in their heap consumption. In heavy benchmarking for elastic#77466 no runtime impact of adding this extra step to setting creation has been found despite pushing setting creation harder than is expected in real-world usage (part of the low relative impact here is the fact that populating a tree-map is quite expensive to begin with so adding the string interning which is fast via the CHM cache doesn't add much overhead). On the other hand, the heap use impact for use-cases that come with a large number of duplicate settings (many similar indices) is significant. As an example, 10k AuditBeat indices consume about 500M of heap for duplicate settings data structures without this change. This cahnge brings the heap consumption from duplicate settings down to O(1M) on every node in the cluster. Relates and addresses most of elastic#78892 Relates elastic#77466
This is a somewhat crude solution to #78892 that addresses 95%+ of duplicate setting entry memory consumption in large clusters. The remaining duplicate structures (lists of all the same strings) are comparatively cheap in their heap consumption. In heavy benchmarking for #77466 no runtime impact of adding this extra step to setting creation has been found despite pushing setting creation harder than is expected in real-world usage (part of the low relative impact here is the fact that populating a tree-map is quite expensive to begin with so adding the string interning which is fast via the CHM cache doesn't add much overhead). On the other hand, the heap use impact for use-cases that come with a large number of duplicate settings (many similar indices) is significant. As an example, 10k AuditBeat indices consume about 500M of heap for duplicate settings data structures without this change. This cahnge brings the heap consumption from duplicate settings down to O(1M) on every node in the cluster. Relates and addresses most of #78892 Relates #77466
This is a somewhat crude solution to elastic#78892 that addresses 95%+ of duplicate setting entry memory consumption in large clusters. The remaining duplicate structures (lists of all the same strings) are comparatively cheap in their heap consumption. In heavy benchmarking for elastic#77466 no runtime impact of adding this extra step to setting creation has been found despite pushing setting creation harder than is expected in real-world usage (part of the low relative impact here is the fact that populating a tree-map is quite expensive to begin with so adding the string interning which is fast via the CHM cache doesn't add much overhead). On the other hand, the heap use impact for use-cases that come with a large number of duplicate settings (many similar indices) is significant. As an example, 10k AuditBeat indices consume about 500M of heap for duplicate settings data structures without this change. This cahnge brings the heap consumption from duplicate settings down to O(1M) on every node in the cluster. Relates and addresses most of elastic#78892 Relates elastic#77466
This is a somewhat crude solution to #78892 that addresses 95%+ of duplicate setting entry memory consumption in large clusters. The remaining duplicate structures (lists of all the same strings) are comparatively cheap in their heap consumption. In heavy benchmarking for #77466 no runtime impact of adding this extra step to setting creation has been found despite pushing setting creation harder than is expected in real-world usage (part of the low relative impact here is the fact that populating a tree-map is quite expensive to begin with so adding the string interning which is fast via the CHM cache doesn't add much overhead). On the other hand, the heap use impact for use-cases that come with a large number of duplicate settings (many similar indices) is significant. As an example, 10k AuditBeat indices consume about 500M of heap for duplicate settings data structures without this change. This cahnge brings the heap consumption from duplicate settings down to O(1M) on every node in the cluster. Relates and addresses most of #78892 Relates #77466
Preamble:
This is a somewhat/admittedly crude solution to #78892 that nevertheless addresses
95%+ of duplicate setting entry memory consumption in large clusters. This has been heavily tested/benchmarked
and no issues have been found. It reduces the heap consumption on every node in the cluster to the same degree
since all of them hold the deserialized settings on heap. In practice this can mean hundreds to GBs of heap savings
on every node in larger clusters. While a more sophisticated solution should eventually be implemented to also
do away with the remaining duplicate data structures the marginal gain of that over this solution is relatively limited
as things work today. Also, this solution is a safe and cheap win that we could potentially even backport to
7.16.1
to bring down memory consumption of common index types there.
The remaining duplicate structures (lists of all the same strings) are
comparatively cheap in their heap consumption.
In heavy benchmarking for #77466 no runtime impact of adding this extra step
to setting creation has been found despite pushing setting creation harder
than is expected in real-world usage (part of the low relative impact here is
the fact that populating a tree-map is quite expensive to begin with so adding
the string interning which is fast via the CHM cache doesn't add much overhead).
On the other hand, the heap use impact for use-cases that come with a large number
of duplicate settings (many similar indices) is significant. As an example,
10k Audit Beat indices consume about 500M of heap for duplicate settings data structures
without this change. This change brings the heap consumption from duplicate settings down to
O(1M) on every node in the cluster.
Relates and addresses most of #78892
Relates #77466