-
Notifications
You must be signed in to change notification settings - Fork 25.6k
Make some Allocation Decider Code a Little More JIT Aware #62275
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
Make some Allocation Decider Code a Little More JIT Aware #62275
Conversation
|
Pinging @elastic/es-distributed (:Distributed/Allocation) |
|
Jenkins run elasticsearch-ci/bwc |
|
Jenkins run elasticsearch-ci/packaging-sample-windows |
henningandersen
left a comment
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.
Very interesting find @original-brownbear . I added a few initial comments, if nothing else to clarify my understanding of the issue.
| case INDICES_PRIMARIES_ACTIVE: | ||
| // check if there are unassigned primaries. | ||
| if (routingNodes.hasUnassignedPrimaries()) { | ||
| return debug ? NO_UNASSIGNED_PRIMARIES : Decision.NO; |
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.
Is there a reason we cannot just always return NO_UNASSIGNED_PRIMARIES? Looks like we can avoid the dependency on debug in this method.
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.
++ that works as far as I can tell
| if (routingNodes.hasInactiveShards()) { | ||
| return debug ? NO_INACTIVE_SHARDS : Decision.NO; | ||
| } | ||
| } |
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.
Add
// fall-through
to signal that fall through is intended.
| } | ||
| // type == Type.ALWAYS | ||
| return allocation.decision(Decision.YES, NAME, "all shards are active"); | ||
| // all shards active from above or type == Type.ALWAYS |
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 this fits better into the switch now in a default block?
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.
++
| unassignedInfo.getNumFailedAllocations(), maxRetry); | ||
| } | ||
| final Decision res = numFailedAllocations >= maxRetry ? Decision.NO : Decision.YES; | ||
| decision = debug ? debugDecision(res, unassignedInfo, numFailedAllocations, maxRetry) : res; |
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.
Rather than inline the debug flag switch, would it be possible to use a supplier-style (perhaps a function, depending on input) just like is done for logging? So that it would be either:
allocation.decision(Decision.NO, NAME, "......[%d]...[%s]", a -> a.args(maxRetry, unassignedInfo.toString()))
or
allocation.decision(Decision.NO, NAME, res -> debugDecision(res, unassignedInfo, numFailedAllcations, maxRetry))
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 guess technically yes, but it looks a lot more complicated and won't inline as well. I mean even for logging we often use
e.g. if (logger.isTraceEnabled()) {because the suppliers aren't free as well (especially when they capture a bunch of vars?).
| final boolean debug = allocation.debugDecision(); | ||
| final int numFailedAllocations = unassignedInfo == null ? 0 : unassignedInfo.getNumFailedAllocations(); | ||
| if (numFailedAllocations > 0) { | ||
| final IndexMetadata indexMetadata = allocation.metadata().getIndexSafe(shardRouting.index()); |
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 it was just as good (or better) to just extract the non-happy path here out into a method of its own?
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 why not, certainly fits in with the theme of this PR :)
| decision = debug ? debugDecision(res, unassignedInfo, numFailedAllocations, maxRetry) : res; | ||
| } else { | ||
| decision = allocation.decision(Decision.YES, NAME, "shard has no previous failures"); | ||
| decision = debug ? YES_NO_FAILURES : Decision.YES; |
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 am not sure we need to switch on debug for purely constant decisions?
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 looked into this and I think no we don't, we seem to only be using the full explanation in the explain allocation request -> will adjust accordingly
|
@henningandersen thanks for taking a look! All points addressed I think :) |
henningandersen
left a comment
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.
Left a few more comments.
| return canRebalance(allocation); | ||
| } | ||
|
|
||
| private static final Decision YES_ALL_PRIMARIES_ACTIVE = Decision.single(Decision.Type.YES, NAME, "all primary shards are active"); |
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 it would be nice to add Decision.constant that still uses Decision.Single but avoids the trap of being able to specify parameters (or eagerly resolves the string if anyone do specify them).
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.
Actually, I'm starting to wonder how much point there even is in making the String creation in the existing Decision.single lazy? The memory savings probably aren't that massive they only affect debug anyway?
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.
++, seems just resolving this early is not a big deal. It will resolve it anyway in both equals, hashCode and streaming write.
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.
Perfect :) Made it eager serialize now, also makes the Decision object immutable in general :)
| return decision; | ||
| } | ||
| if (node.node() != null) { | ||
| final boolean debug = allocation.debugDecision(); |
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.
Maybe remove this variable that it is only used once?
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.
++
| case INDICES_PRIMARIES_ACTIVE: | ||
| // check if there are unassigned primaries. | ||
| if (routingNodes.hasUnassignedPrimaries()) { | ||
| return NO_UNASSIGNED_PRIMARIES; |
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.
In AllocationDeciders we choose to early terminate no decisions, but only if the object is Decision.NO. I think we need to change that to check the underlying type if we return constants 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.
Fixed by checking decision type :) thanks for spotting this!
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 can add a test that the early termination works in AllocationDeciders? At least one specific test with one specific example if making something that randomly exercises all decider NO decisions is too complicated.
|
Thanks @henningandersen all points addressed I think :) |
|
@henningandersen ping here (low priority obviously) though I think it relates a to keeping making master nodes more responsive by burning less CPU + heap :) |
henningandersen
left a comment
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.
| private static Decision debugNoMissingAttribute(String awarenessAttribute, List<String> awarenessAttributes) { | ||
| return Decision.single(Decision.Type.NO, NAME, | ||
| "node does not contain the awareness attribute [%s]; required attributes cluster setting [" | ||
| + CLUSTER_ROUTING_ALLOCATION_AWARENESS_ATTRIBUTE_SETTING.getKey() + "=%s]", awarenessAttribute, |
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.
Is there a reason for not using [%s] for the settings key?
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 figured if we're already optimizing, why not just make this compile to one string constant instead of a constant format replacement
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, it just seems odd to have both forms (string concatenation and replacement) in the very same line?
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.
Hmm replacement with a constant seems odd as well to me as well :) But I just realized that this is the debug path anyway, so I'm happy to change this back if you want.
That said, for better or for worse, we do have that pattern of mixing concatenation + replacement in a bunch of places for logging or for file name formatting in BlobstoreRepository for example?
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.
OK, leave it as is, I can certainly gladly accept it as is, was a small nit only.
compile to one string constant
I think the getKey call prevents that? Though maybe the jit does something smart about this?
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 the getKey call prevents that? Though maybe the jit does something smart about this?
I would have thought the JIT can figure this out, but it took only a few minutes with JitWatch to learn that this is not the case. This initially compiles to:
private static org.elasticsearch.cluster.routing.allocation.decider.Decision debugNoMissingAttribute(java.lang.String, java.util.List<java.lang.String>);
Code:
0: getstatic #236 // Field org/elasticsearch/cluster/routing/allocation/decider/Decision$Type.NO:Lorg/elasticsearch/cluster/routing/allocation/decider/Decision$Type;
3: ldc #241 // String awareness
5: getstatic #7 // Field CLUSTER_ROUTING_ALLOCATION_AWARENESS_ATTRIBUTE_SETTING:Lorg/elasticsearch/common/settings/Setting;
8: invokevirtual #257 // Method org/elasticsearch/common/settings/Setting.getKey:()Ljava/lang/String;
11: invokedynamic #259, 0 // InvokeDynamic #2:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
16: iconst_2
17: anewarray #245 // class java/lang/Object
20: dup
21: iconst_0
22: aload_0
23: aastore
24: dup
25: iconst_1
26: aload_1
and all that happens is that the getKey call is eventually inlined but the string concatenation still happens every time.
-> I'll revert this before merging in a bit :)
| case INDICES_PRIMARIES_ACTIVE: | ||
| // check if there are unassigned primaries. | ||
| if (routingNodes.hasUnassignedPrimaries()) { | ||
| return NO_UNASSIGNED_PRIMARIES; |
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 can add a test that the early termination works in AllocationDeciders? At least one specific test with one specific example if making something that randomly exercises all decider NO decisions is too complicated.
Looking into that :) |
|
@henningandersen I pushed 7813daf for the test of the short-circuit logic Maybe take another look since I also slightly changed the returns of the methods in |
henningandersen
left a comment
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 Henning! |
) When investigating our code cache usage for another issue I ran into this. This PR just fixes a few spots and there's many more. The current way we compute the decisions often creates much larger than necessary methods because the compiler has no efficient way of optimizing away things like using CLUSTER_ROUTING_ALLOCATION_AWARENESS_ATTRIBUTE_SETTING.getKey() as an explain parameter that do allocations (but whose results are thrown away immediately if debug is off). As a result e.g. the max retry allocation decider's canAllocate compiles into an 18kb method before and into a 2kb method after this change (at C1 L3). I think if we're a little more mindful of the JIT here we can get some measurable speedups out of the allocation deciders logic. Plus, this kind of change saves quite a few in allocations in isolation as well which is always nice on a hot CS thread I suppose.
…64444) When investigating our code cache usage for another issue I ran into this. This PR just fixes a few spots and there's many more. The current way we compute the decisions often creates much larger than necessary methods because the compiler has no efficient way of optimizing away things like using CLUSTER_ROUTING_ALLOCATION_AWARENESS_ATTRIBUTE_SETTING.getKey() as an explain parameter that do allocations (but whose results are thrown away immediately if debug is off). As a result e.g. the max retry allocation decider's canAllocate compiles into an 18kb method before and into a 2kb method after this change (at C1 L3). I think if we're a little more mindful of the JIT here we can get some measurable speedups out of the allocation deciders logic. Plus, this kind of change saves quite a few in allocations in isolation as well which is always nice on a hot CS thread I suppose.
Some smaller improvements in the direction of elastic#62275 and removal of some dead code and duplication.
Some smaller improvements in the direction of #62275 and removal of some dead code and duplication.
Some smaller improvements in the direction of elastic#62275 and removal of some dead code and duplication.
In elastic#62275 we refactored this code a bit and inadvertently reversed the sense of this conditional when running in debug mode. This commit fixes the mistake.
In #62275 we refactored this code a bit and inadvertently reversed the sense of this conditional when running in debug mode. This commit fixes the mistake.
In elastic#62275 we refactored this code a bit and inadvertently reversed the sense of this conditional when running in debug mode. This commit fixes the mistake.
In #62275 we refactored this code a bit and inadvertently reversed the sense of this conditional when running in debug mode. This commit fixes the mistake.
In #62275 we refactored this code a bit and inadvertently reversed the sense of this conditional when running in debug mode. This commit fixes the mistake. Co-authored-by: Elastic Machine <[email protected]>
When investigating our code cache usage for another issue I ran into this. This PR just fixes a few spots and there's many more. The current way we compute the decisions often creates much larger than necessary methods because the compiler has no efficient way of optimizing away things like using
CLUSTER_ROUTING_ALLOCATION_AWARENESS_ATTRIBUTE_SETTING.getKey()as an explain parameter that do allocations (but whose results are thrown away immediately if debug is off).As a result e.g. the max retry allocation decider's
canAllocatecompiles into an 18kb method before and into a 2kb method after this change (at C1 L3). I think if we're a little more mindful of the JIT here we can get some measurable speedups out of the allocation deciders logic. Plus, this kind of change saves quite a few in allocations in isolation as well which is always nice on a hot CS thread I suppose.