Skip to content
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

A76: Improvements to the Ring Hash LB Policy #412

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

atollena
Copy link
Contributor

@atollena atollena commented Jan 22, 2024

Based on discussion in grpc/grpc#33356.

A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
`hash_key` name resolver attribute changes.

The xDS resolver will be changed so that when converting EDS responses to
resolver endpoints, it will set the `hash_key` name resolver attribute to the
Copy link
Member

Choose a reason for hiding this comment

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

EDS responses are handled in a load balancing policy, so I'm not sure if "name resolver attribute" is the right terminology here.

Copy link
Contributor Author

@atollena atollena Jan 22, 2024

Choose a reason for hiding this comment

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

OK, I interpreted your comment as two things:

  1. It's not the xDS resolver, but the cluster resolver LB policy that does this, so I replaced the term.
  2. "name resolver attributes" are not really a thing. I replaced this with "Endpoint attributes", which is metadata attached to each endpoint, used for example to store locality IDs and weight, and which IIUC are language specific, but all languages have this feature. I don't know if this is clear enough for cross-language interpretations -- please let me know.

1 seems to be in motion with A74, which removes the cluster resolver LB policy in favor of the "xDS resolver".

uint64 min_ring_size = 1; // Optional, defaults to 1024.
uint64 max_ring_size = 2; // Optional, defaults to 4096, max is 8M.

string request_metadata_key = 3; // Optional, defaults to the empty string.
Copy link
Member

Choose a reason for hiding this comment

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

In the context of a Protobuf 3 message, an unset string field is the same as an empty string value, so stating that the field "defaults to the empty string" is redundant with the default Protobuf behavior. In addition, "defaults to the empty string" seems to imply that the empty string value will be used in some way, but earlier it says that "not set" and "empty" are treated the same. I think it would be clearer to say something like "Optional, unused if unset".

Copy link
Contributor Author

@atollena atollena Jan 22, 2024

Choose a reason for hiding this comment

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

Yes, replaced with "Optional, unused if unset.". I also removed mentions of "not set" in the implementation section, since there is no way to distinguish not set from empty.


After the addition of this field, the `ring_hash` LB policy config will be:

message RingHashLoadBalancingConfig {
Copy link
Member

Choose a reason for hiding this comment

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

To get protobuf syntax highlighting, instead of indenting the code, use a fenced code block, with the language name proto.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

`GRPC_EXPERIMENTAL_XDS_RING_HASH_ENDPOINT_HASH_KEY` environment variable. This
will protect from the case where an xDS control plane is already setting the
`LbEndpoint.Metadata` `envoy.lb` `hash_key` field, in which case deploying this
new behavior would churn all endpoint hash keys. This environment variable will
Copy link
Member

Choose a reason for hiding this comment

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

Deploying a new version of gRPC will clear all ring_hash state anyway, so I don't understand how this environment variable would do anything to prevent churn.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Deploying with the environment variable unset will keep the current behavior of hashing the IP address instead of the new behavior of using the value from LbEndpoint.Metadata. As a result, the locations of existing endpoints on the ring will be guaranteed to be the same as before, and requests will continue being routed to the same endpoint. It will churn connections, if that's what you mean, but not endpoint locations on the ring. Suddenly changing all locations on the ring may have unintended consequences, removing the locality that ring hash may have been used for in the first place.

I'm thinking of a better phrasing, and the best I could think of was to replace "churn" with "change". Let me know if this is still unclear.

Copy link
Member

@markdroth markdroth left a comment

Choose a reason for hiding this comment

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

Thanks for writing this!

Overall, the design looks good, but I think there is one significant issue related to where we compute the request hash, since I don't think it will work to do it in the picker. That will probably need a bit more discussion to resolve.

Please let me know if you have any questions. Thanks!

A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
Copy link
Member

@markdroth markdroth left a comment

Choose a reason for hiding this comment

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

Thanks for making those updates! We'll get back to you on the open question once Eric gets back.

A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
@atollena
Copy link
Contributor Author

@markdroth just checking in on this, I've made the updates regarding handling the empty header. I'll start testing an implementation for this internally, but in the meantime would appreciate another review. Thanks!

Copy link
Member

@markdroth markdroth left a comment

Choose a reason for hiding this comment

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

Sorry for the delay! This looks really good overall. Comments are mostly things to improve clarity of the doc.

Please let me know if you have any questions. Thanks!

A76-ring-hash-improvements.md Outdated Show resolved Hide resolved

This proposal extends the following existing gRFC:

* [gRFC A42: xDS Ring Hash LB Policy][A42]
Copy link
Member

Choose a reason for hiding this comment

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

It's probably also worth referencing A61, which significantly simplified the ring_hash picker logic.

Copy link
Contributor Author

@atollena atollena Apr 2, 2024

Choose a reason for hiding this comment

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

OK, thanks for pointing this out, I wasn't aware of A61's impact on ring hash. I spent some time to understand how that affects the implementation I started in Go. Go implements A62 (pick first sticky transient failure) but not A61, so it cannot take advantage of this without further refactoring the ring hash policy. I'll sync up with the Go team to see what the best path forward is, but I imagine it'd be best to start with implementing at least part of A61 (delegating to pick_first).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After a quick discussion with @dfawley I decided to try to implement this before A61. I think the proposal could be more clear in this case, at least for Go, because there is a difference between immediately attempting to connect (which happens only for subchannels in "idle" state), and queuing a connect for subchannels in transient failure, where a connection attempt will be triggered when the subchannel returns to idle state after the connection backoff.

I implemented the new behavior of scanning forward for a ready subchannel, either when:

  1. the subchannel picked randomly is idle (immediate connection attempt triggered) or,
  2. the subchannel picked randomly is in transient failure (connection attempt queued)

I think it has the desired behaviour of making sure we don't trigger more than one connection attempt on each pick if there is a ready subchannel, not adding latency if there is a ready subchannel, and converge to random. But I also considered only implementing the scanning forward for ready subchannel when we triggered an immediate connection attempt, which happens when either:

  1. the picked subchannel is idle
  2. the picked subchannel is in transient failure, and the second is idle
  3. the picked suchannel is in transient failure, the second is also in transient failure, and we found an idle connection before a ready one when scanning the ring forward.

This ambiguity will disappear when all implementations have A61 implemented. This is planned for this quarter for Go, IIUC, but I imagine there may be reasons for some implementations to want to implement A76 before A61. My question is whether you think it's worth adding a note about it in this gRFC to lift this ambiguity.

Copy link
Member

Choose a reason for hiding this comment

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

My inclination is that this logic is already complicated enough to understand, so I'd prefer to avoid muddying the waters by trying to do this without having first implemented the relevant parts of A61. From first glance, what you describe here seems reasonable, but I think I'd have to stare at it for a lot longer to convince myself it didn't have any potential pitfalls.

@dfawley, is it not feasible to implement just the ring_hash part of A61 before implementing this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We discussed with the go team and we're going to implement delegating to pick first before this change.

A76-ring-hash-improvements.md Show resolved Hide resolved
A76-ring-hash-improvements.md Show resolved Hide resolved
Copy link
Member

@markdroth markdroth left a comment

Choose a reason for hiding this comment

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

This looks great to me!

@ejona86 @dfawley Would you two please do a review pass as well? Thanks!

A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved

Explicitly setting the request hash key cannot possibly lead to problem with
existing deployment because the new behavior requires setting a load balancing
policy configuration field that did not exist before. Therefore, it is not gated
Copy link
Member

Choose a reason for hiding this comment

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

That's not the condition. It's "whether remote I/O can trigger it" and it can. The reason to have an environment variable here is to handle bugs in implementations while they are being implemented. If there's a crasher bug, for example, you don't want to prevent using the new feature because you might trigger old clients to crash.

Copy link
Contributor Author

@atollena atollena Jul 22, 2024

Choose a reason for hiding this comment

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

OK, so I guess for this one it also makes sense to have it be opt-in to start with. I added an environment variable condition called GRPC_EXPERIMENTAL_RING_HASH_SET_REQUEST_HASH_KEY. That would let users disable the feature should it cause problems if the source of the service config is causing an undesirable behavior. I am not familiar with the process of when we decide to remove this kind of feature gating environment variable. Please let me know if that seems right.

Copy link
Member

Choose a reason for hiding this comment

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

The normal process is, "when it is adequately tested, enable it by default." C++ core tends to remove the env var at that point. In Java/Go we tend to wait a release with the env var there so it can be disabled if something goes awry. When the env var is removed doesn't matter; we care about when the default changes here.

But the main point is "when tested." In most other gRFC's that's our interop test framework; here, I guess it'll just be unit tests? For Go in practice it might be "when someone tests it with a service" where "someone" is "Antoine and friends".

Again, the main thing we want to avoid is a bug during the initial implementation that causes really bad client behavior such that you can't enable the feature in the future, lest you trigger broken clients.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, that makes sense, and it also applies to the other environment variable then.

I'm happy to remove the environment variables when we have usage (we have internal usage of a fork of the existing ring hash that we'll be able to replace when this is implemented in Java and Go).

@@ -545,6 +545,7 @@ for (i = 0; i < ring.size(); ++i) {
return PICK_QUEUE;
}
}
return PICK_FAIL;
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 took the liberty to add this clarification to A62 -- handle the case of transient failure. Although I presume that the aggregated state reported to the parent may prevent the case from ever happening?

Copy link
Member

Choose a reason for hiding this comment

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

There are definitely times we'll pick from failing policies, like if all alternatives are also failing.

Implementation note: in Java we do return ring[first_index].picker->Pick(...);, which seems appropriate for everyone to do. And we can do the same in the new flow; both loops seem to guarantee if we get to the end that all endpoints are TF.

Copy link
Contributor Author

@atollena atollena Jul 25, 2024

Choose a reason for hiding this comment

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

// ...
}

requested_connection = picker_has_a_child_connecting;
Copy link
Member

Choose a reason for hiding this comment

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

We should describe that picker_has_a_child_connecting is computed when the picker is created.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done.

// Determine request hash.
using_random_hash = false;
if (config.request_hash_header.empty()) {
request_hash = call_attributes.hash;
Copy link
Member

Choose a reason for hiding this comment

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

Can't this also be empty? In which case you'd use a random hash, too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I think the behavior is a bit confusing as it it, since if request_hash_header is empty, and the call attribute is not set because there is no xDS config selector to set it to a random value, then this would result in a fix hash that always routes to the same endpoint on the ring. I updated the logic and the text to pick a random hash in this case.

determining the locations of each endpoint on the ring will be extracted from a
pre-defined endpoint attribute called `hash_key`. If this attribute is set, then
the endpoint is placed on the ring by hashing its value. If this attribute is
not set or empty, then the endpoint's first address is hashed, matching the
Copy link
Member

Choose a reason for hiding this comment

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

"first address" -- So the A61 gRFC says the DNS resolver should do RFC-6724 sorting. But the endpoints aren't necessarily coming from the DNS resolver, right? So do we want to apply the same sorting here anyway, to ensure consistency?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But the endpoints aren't necessarily coming from the DNS resolver, right? So do we want to apply the same sorting here anyway, to ensure consistency?

The DNS resolver will create an endpoint per address. The fact that it sorts them seems irrelevant, since we do not take the order of endpoints into consideration when placing them on the ring. I would expect other resolvers that cannot group endpoints to operate like the DNS resolver, and resolvers that can group endpoints to sort addresses within an endpoint in a consistent way (such as according to RFC-6724). To me the wording seems sufficient here: users that care about this consistency should either sort addresses in the resolver, or use the endpoint metadata key.

You do raise an interesting point which is that if the DNS resolver returns multiple addresses for the same endpoint, there is no way for ring hash to associate them to the same endpoint. So each address, regardless of family, will get its own place on the ring. Some addresses may be multi-home of the same endpoint, etc. That seems like a fundamental limitation of using DNS A/AAAA records for service discovery: we can't do things that rely on knowing the list of endpoints rather than the list of addresses. And I think this concern also applies to other LB policies, such as RR and WRR, which when used with DNS, would cause multiple connections to the same endpoint, potentially resulting in load imbalance.

A76-ring-hash-improvements.md Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
A76-ring-hash-improvements.md Outdated Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants