Skip to content

Implement downstream IP hashing for HTTP ketama routing#1727

Merged
mattklein123 merged 11 commits intoenvoyproxy:masterfrom
akonradi:stable-routing-ip-hashing
Sep 27, 2017
Merged

Implement downstream IP hashing for HTTP ketama routing#1727
mattklein123 merged 11 commits intoenvoyproxy:masterfrom
akonradi:stable-routing-ip-hashing

Conversation

@akonradi
Copy link
Copy Markdown
Contributor

The v2 API supports using the downstream (source) IP address to route HTTP requests to upstream hosts. This PR implements that functionality, as specified in #1296.

The downstream address information is necessary for source-IP-based
hashing.

Signed-off-by: Alex Konradi <akonradi@google.com>
Add support for hashing the downstream IP address for use in HTTP ketama
routing. This complements the existing scheme of using one of the HTTP
headers as a hash value.

Signed-off-by: Alex Konradi <akonradi@google.com>
@akonradi
Copy link
Copy Markdown
Contributor Author

This code implements the desired functionality, and includes unit tests. An integration test might also be a good idea but I don't know how to implement it without depending on the choice of hashing scheme. For instance, it would be easy to check that requests from a downstream client C get routed to the same upstream host X, but if another upstream host Y joins, depending on where it ends up being placed in the hash ring, it might start receiving requests from C. Thoughts?

@mattklein123
Copy link
Copy Markdown
Member

@akonradi I think unit test only is fine for this change. (Haven't reviewed yet).

Copy link
Copy Markdown
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

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

Looks good. I agree that we don't need an integration test for this (and adding one will be difficult, since we don't have a naive way of having multiple source addresses in IPv6-only setups).

* exist.
*/
virtual Optional<uint64_t> generateHash(const Http::HeaderMap& headers) const PURE;
virtual Optional<uint64_t> generateHash(const Network::Address::Instance* downstream_address,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you add Doxygen comments for @param?

if (header) {
hash.value(HashUtil::xxHash64(header->value().c_str()));
}
} else if (hash_ip_ && downstream_addr) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about the idea of these being cumulative? I.e. I can make the hash a function of both header and downstream IP rather than either/or? I think this was the idea of making the policy a repeated.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah this is the one part of this change that I find a little strange.

  • Cumulative also makes sense to me.
  • It seems odd to trigger header matching based on the empty string. When we translate the repeated is there a way to translate in such a way that we don't need to check on empty string and perhaps just translate to generic interface with an evaluate() function like we do in a few other places? (Not sure of details here but just a thought).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's #1422, though it wouldn't be too bad to implement as part of this PR.

auto hash_policy = route_entry_->hashPolicy();
if (hash_policy) {
return hash_policy->generateHash(*downstream_headers_);
const Network::Connection* connection = this->downstreamConnection();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In what situations do we now have a downstreamConnection()? Should this a be an ASSERT and/or reference instead?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IIRC AsyncClient does not have a downstream connection. This actually confused me also and I had to think about it so I think worth a comment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also nit: in general we don't use this-> in this type of context.

TEST(RouterMatcherTest, HashPolicyIp) {
NiceMock<Runtime::MockLoader> runtime;
NiceMock<Upstream::MockClusterManager> cm;
auto routeConfig = baseHashPolicyRouteConfig();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd probably use a test fixture here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also nit: route_config

}
{
Http::TestHeaderMapImpl headers = genHeaders("www.lyft.com", "/foo", "GET");
Network::Address::Ipv4Instance addr("1.2.3.4");
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about a test to show that different IPs hash to different addresses?

Bugfix: the previous version of this code used the address of the
directly connected downstream host. This is not the same as the address
of the client making the request unless the client is directly connected
to the Envoy instance doing the routing. To fix this, use the downstream
address of the routed stream instead.

Signed-off-by: Alex Konradi <akonradi@google.com>
Signed-off-by: Alex Konradi <akonradi@google.com>
Signed-off-by: Alex Konradi <akonradi@google.com>
Signed-off-by: Alex Konradi <akonradi@google.com>
Copy link
Copy Markdown
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

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

LGTM, just some one more thing..

* A hash value might not be returned if for example the specified HTTP header does not
* exist. In the future we might add additional support for hashing on origin address,
* etc.
* @param const downstream_address the address of the connected client host
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@param should have the name of the parameter, not the type.

{
Http::TestHeaderMapImpl headers = genHeaders("www.lyft.com", "/foo", "GET");
const auto hash_policy = config.route(headers, 0)->routeEntry()->hashPolicy();
uint64_t hash_1 = hash_policy->generateHash("1.2.3.4", headers).value();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: const

}

Optional<uint64_t> HashPolicyImpl::generateHash(const Http::HeaderMap& headers) const {
Optional<uint64_t> HashPolicyImpl::generateHash(const std::string& downstream_addr,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

According to @mattklein123:

IIRC AsyncClient does not have a downstream connection. This actually confused me also and I had to think about it so I think worth a comment.

Should we at least add a check that a downstream IP based route config is not used alongside this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't understand your question, but a comment was added to the docs for generateHash that addresses @mattklein123's comment. downstream_addr is an empty string for requests originated by AsyncClient.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK, as long as it's empty it's fine.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK cool. I wasn't sure. If it's empty string that's fine.

Signed-off-by: Alex Konradi <akonradi@google.com>
Signed-off-by: Alex Konradi <akonradi@google.com>
Signed-off-by: Alex Konradi <akonradi@google.com>
htuch
htuch previously approved these changes Sep 26, 2017
* @param const headers the HTTP headers for the stream
* @return Optional<uint64_t> an optional hash value to route on. A hash value might not be
* returned if for example the specified HTTP header does not exist.
* @return Optional<uint64_t> an optional hash value to route on.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: reorder @return after @param.

* returned if for example the specified HTTP header does not exist.
* @return Optional<uint64_t> an optional hash value to route on.
* @param downstream_address contains the address of the connected client host, or an
* empty string if the request is initiated from within this host, and
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: no need for , and between @param

const Http::HeaderEntry* header = headers.get(header_name_);
if (header) {
hash.value(HashUtil::xxHash64(header->value().c_str()));
if (!header_name_.get().empty()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about the comment thread here re: pivoting on empty string and hashing on multiple things? Did we decide to defer that to a follow up?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yup, that'll be my next PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK sounds good.

…-hashing

Signed-off-by: Alex Konradi <akonradi@google.com>
Signed-off-by: Alex Konradi <akonradi@google.com>
@akonradi
Copy link
Copy Markdown
Contributor Author

The coverage test failed because the JVM bugged out, lovely.

@mattklein123
Copy link
Copy Markdown
Member

@akonradi I kicked it for you

Copy link
Copy Markdown
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

LGTM, thank you.

@mattklein123 mattklein123 merged commit 0408b98 into envoyproxy:master Sep 27, 2017
@akonradi akonradi deleted the stable-routing-ip-hashing branch September 27, 2017 13:49
costinm pushed a commit to costinm/envoy that referenced this pull request Oct 2, 2017
)

The v2 API supports using the downstream (source) IP address to route HTTP requests to
upstream hosts. This PR implements that functionality, as specified in envoyproxy#1296.

Signed-off-by: Alex Konradi <akonradi@google.com>
jpsim pushed a commit that referenced this pull request Nov 28, 2022
Signed-off-by: Jose Nino <jnino@lyft.com>
Signed-off-by: JP Simard <jp@jpsim.com>
jpsim pushed a commit that referenced this pull request Nov 29, 2022
Signed-off-by: Jose Nino <jnino@lyft.com>
Signed-off-by: JP Simard <jp@jpsim.com>
mathetake pushed a commit that referenced this pull request Mar 3, 2026
**Description**

The reason for the 401 response could also be token is missing in the
request.

Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com>
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.

3 participants