Skip to content

Conversation

@original-brownbear
Copy link
Contributor

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.

@original-brownbear original-brownbear added >non-issue :Distributed Coordination/Allocation All issues relating to the decision making around placing a shard (both master logic & on the nodes) v8.0.0 v7.10.0 labels Sep 11, 2020
@elasticmachine
Copy link
Collaborator

Pinging @elastic/es-distributed (:Distributed/Allocation)

@elasticmachine elasticmachine added the Team:Distributed (Obsolete) Meta label for distributed team (obsolete). Replaced by Distributed Indexing/Coordination. label Sep 11, 2020
@original-brownbear original-brownbear changed the title Make Allocation Decider Code a Little more JIT Aware Make Some Allocation Decider Code a Little More JIT Aware Sep 11, 2020
@original-brownbear original-brownbear changed the title Make Some Allocation Decider Code a Little More JIT Aware Make some Allocation Decider Code a Little More JIT Aware Sep 11, 2020
@original-brownbear
Copy link
Contributor Author

Jenkins run elasticsearch-ci/bwc
Jenkins run elasticsearch-ci/default-distro

@original-brownbear
Copy link
Contributor Author

Jenkins run elasticsearch-ci/packaging-sample-windows

Copy link
Contributor

@henningandersen henningandersen left a 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;
Copy link
Contributor

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.

Copy link
Contributor Author

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;
}
}
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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))

Copy link
Contributor Author

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());
Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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?

Copy link
Contributor Author

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

@original-brownbear
Copy link
Contributor Author

@henningandersen thanks for taking a look! All points addressed I think :)

Copy link
Contributor

@henningandersen henningandersen left a 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");
Copy link
Contributor

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).

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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();
Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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!

Copy link
Contributor

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.

@original-brownbear
Copy link
Contributor Author

Thanks @henningandersen all points addressed I think :)

@andreidan andreidan added v7.11.0 and removed v7.10.0 labels Oct 7, 2020
@original-brownbear
Copy link
Contributor Author

@henningandersen ping here (low priority obviously) though I think it relates a to keeping making master nodes more responsive by burning less CPU + heap :)

Copy link
Contributor

@henningandersen henningandersen left a 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,
Copy link
Contributor

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?

Copy link
Contributor Author

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

Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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?

Copy link
Contributor Author

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;
Copy link
Contributor

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.

@original-brownbear
Copy link
Contributor Author

I wonder if we can add a test that the early termination works in AllocationDeciders?

Looking into that :)

@original-brownbear
Copy link
Contributor Author

@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 AllocationDeciders so that we still always return a plain Decision.NO from those in the non-debug case?
Thanks!

Copy link
Contributor

@henningandersen henningandersen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@original-brownbear
Copy link
Contributor Author

Thanks Henning!

@original-brownbear original-brownbear merged commit 0ed5174 into elastic:master Oct 30, 2020
@original-brownbear original-brownbear deleted the fix-some-expensive-stuff branch October 30, 2020 17:30
original-brownbear added a commit to original-brownbear/elasticsearch that referenced this pull request Oct 30, 2020
)

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.
original-brownbear added a commit that referenced this pull request Oct 30, 2020
…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.
original-brownbear added a commit to original-brownbear/elasticsearch that referenced this pull request Nov 6, 2020
Some smaller improvements in the direction of elastic#62275 and removal of some
dead code and duplication.
original-brownbear added a commit that referenced this pull request Nov 6, 2020
Some smaller improvements in the direction of #62275 and removal of some
dead code and duplication.
original-brownbear added a commit to original-brownbear/elasticsearch that referenced this pull request Nov 6, 2020
Some smaller improvements in the direction of elastic#62275 and removal of some
dead code and duplication.
original-brownbear added a commit that referenced this pull request Nov 6, 2020
Some smaller improvements in the direction of #62275 and removal of some
dead code and duplication.
@original-brownbear original-brownbear restored the fix-some-expensive-stuff branch December 6, 2020 19:01
DaveCTurner added a commit to DaveCTurner/elasticsearch that referenced this pull request Sep 9, 2022
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.
elasticsearchmachine pushed a commit that referenced this pull request Sep 9, 2022
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.
DaveCTurner added a commit to DaveCTurner/elasticsearch that referenced this pull request Sep 9, 2022
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.
DaveCTurner added a commit that referenced this pull request Sep 9, 2022
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.
elasticsearchmachine pushed a commit that referenced this pull request Sep 12, 2022
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

:Distributed Coordination/Allocation All issues relating to the decision making around placing a shard (both master logic & on the nodes) >non-issue Team:Distributed (Obsolete) Meta label for distributed team (obsolete). Replaced by Distributed Indexing/Coordination. v7.11.0 v8.0.0-alpha1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants