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

Added Ring hash selection strategy #4831

Open
wants to merge 46 commits into
base: main
Choose a base branch
from

Conversation

Bue-von-hon
Copy link
Contributor

Motivation:

this Resolves #4698 issue

Added Ring hash strategy for For a broader selection.

Motification:

  • Added(RingHashEndpointSelectionStrategy): Implement a ring hash using the md5 hash algorithm and a ConcurrentSkipListMap

Result:

For now on, users will be able to use Ring hashes for load balancing.

Motivation:

this Resolves line#4698 issue

Added Ring hash strategy for For a broader selection.

Motification:

- Added(RingHashEndpointSelectionStrategy): Implement a ring hash using the md5 hash algorithm and a ConcurrentSkipListMap

Result:

For now on, users will be able to use Ring hashes for load balancing.
@Bue-von-hon
Copy link
Contributor Author

Looking at the wighted round robin,
it looks like we need to identify the keys via host and port, am I understanding this correctly?🤔

final int weight = endpoint.weight();
final String host = endpoint.host();
// If weight is 3, place 3 times in the ring, if weight is 1, place once in the ring
for (int i = 0; i < weight; i++) {
Copy link
Contributor

@ikhoon ikhoon Apr 30, 2023

Choose a reason for hiding this comment

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

The weight should be normalized to reduce the size of ring. The default weight of an Endpoint is 1000. If all endpoints have the default weight (1000), only one entry for Endpoint should be enough. 30k elements will be added to ring if we have 30 endpoints with 1000 weight which would be a waste of memory.

It is necessary to set the maximum ring size so that the size of the ring is not too large. If the GCM of weights is too small and exceeds the maximum ring size, it seems that you need to divide the value by setting an appropriately large number.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Respecting your feedback, �I have implemented two methods: binary search with normalized weights and GCD.
However, I think a binary search, as implemented in the weighted round robin strategy, is sufficient.
so how about not using GCD but using binary search only?

@@ -100,6 +100,9 @@ dependencies {
// Caffeine
implementation libs.caffeine

// https://mvnrepository.com/artifact/net.openhft/zero-allocation-hashing
implementation 'net.openhft:zero-allocation-hashing:0.16'
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think of forking https://github.com/OpenHFT/Zero-Allocation-Hashing/blob/ea/src/main/java/net/openhft/hashing/XxHash.java to our code base or shading and timing the library?

  • We only use XxHash but Zero-Allocation-Hashing has additional hash functions.
  • We don't expose a utility library as a transitive dependency in the POM file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Forked XxHash into our code base.
But due to the complex inheritance, several new files have been created.

Copy link
Member

@trustin trustin 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 your pull request, @Bue-von-hon! Could you add some test cases?

@@ -0,0 +1,86 @@
/*
* Copyright 2016 LINE Corporation
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Copyright 2016 LINE Corporation
* Copyright 2023 LINE Corporation

}

@Override
public @Nullable Endpoint selectNow(ClientRequestContext ctx) {
Copy link
Member

Choose a reason for hiding this comment

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

We probably need @Nullable here because its super class has @Nullable already on this method? (We may want to change this, but at least for now)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed @nullable annotation!

Comment on lines 51 to 52
final WeightedRingEndpoint weightedRingEndpoint = this.weightedRingEndpoint;
return weightedRingEndpoint.select(ctx.endpoint());
Copy link
Member

Choose a reason for hiding this comment

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

Q: Is there any chance of weightedRingEndpoint being null? For example, the listener you added in the construtor might not be notified early enough.

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 understand the possibility that it could be null.
I saw code in the WeightedRoundRobinStrategy class that prepared for the same possibility, and I borrowed it.


Endpoint select(Endpoint point) {
final int key = getXXHash(point.host());
final SortedMap<Integer, Endpoint> tailMap = ring.tailMap(key);
Copy link
Member

Choose a reason for hiding this comment

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

Could we use the Int2ObjectSortedMap instead?

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 it!

Bue-von-hon and others added 3 commits May 6, 2023 19:43
…shEndpointSelectionStrategy.java

Co-authored-by: Trustin Lee <[email protected]>
Motivation:

Based on Code Review we have problems like below.
There was an issue with too many endpoints being placed on the ring, and the WeightedRingEndpoint could be null in the selectNow method.
typo in the copyright.
unnecessary dependencies in the build.gradle. We only use XxHash but Zero-Allocation-Hashing has additional hash functions.
this commit resolves these problems.

Modification:

Fix(build.gradle): remove zero-allocation-hashing dependency
Add(Access.java, ByteBufferAccess.java, CharSequenceAccess.java, LongHashFunction.java, Primitives.java, UnsafeAccess.java, Util.java, XxHash.java): fork Xxhash Related Code to our code base
Fix(RingHashEndpointSelectionStrategy.java): fix typo and add a GCD and binary search to find an x that will evenly distribute the endpoints over the ring. Add filtering to use only weights > 0. Prevent weightedRingEndpoint being null. Remove @nullable based on code review.

Result:

Solved the problem of too many endpoints being placed in a ring with GCD and binary search.
Prepared for the possibility of a null WeightedRingEndpoint in the selectNow method. Fixed typo in copyright.
Removed unnecessary dependencies in build.gradle by forking only the necessary code.
@trustin
Copy link
Member

trustin commented May 8, 2023

Thanks for continuing updating this PR. Please feel free to let us know when it's ready for reviews. If you feel it's not yet, please consider switching it to a draft PR. 🙇

@Bue-von-hon Bue-von-hon marked this pull request as draft May 8, 2023 14:12
@Bue-von-hon
Copy link
Contributor Author

Thanks for continuing updating this PR. Please feel free to let us know when it's ready for reviews. If you feel it's not yet, please consider switching it to a draft PR. 🙇

Thanks for pointing out this great feature.
It turned out to be longer than I thought, so I fixed it to draft status

Copy link
Contributor Author

@Bue-von-hon Bue-von-hon left a comment

Choose a reason for hiding this comment

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

Reviews all comment! PTAL🙏 @trustin @ikhoon

p.s.
i have problem with sun.misc.Unsafe pakage.
This package should be imported, but it didn't exist in our tests.(so that, i impleamated only one test😭)
I'm worried about whether or not to fork this one because I've gotten reviews before saying to be careful about importing dependencies.(in addition I'm not sure how to import dependencies in this project.)

@@ -100,6 +100,9 @@ dependencies {
// Caffeine
implementation libs.caffeine

// https://mvnrepository.com/artifact/net.openhft/zero-allocation-hashing
implementation 'net.openhft:zero-allocation-hashing:0.16'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Forked XxHash into our code base.
But due to the complex inheritance, several new files have been created.

Comment on lines 51 to 52
final WeightedRingEndpoint weightedRingEndpoint = this.weightedRingEndpoint;
return weightedRingEndpoint.select(ctx.endpoint());
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 understand the possibility that it could be null.
I saw code in the WeightedRoundRobinStrategy class that prepared for the same possibility, and I borrowed it.


Endpoint select(Endpoint point) {
final int key = getXXHash(point.host());
final SortedMap<Integer, Endpoint> tailMap = ring.tailMap(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.

fixed it!

}

@Override
public @Nullable Endpoint selectNow(ClientRequestContext ctx) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed @nullable annotation!

final int weight = endpoint.weight();
final String host = endpoint.host();
// If weight is 3, place 3 times in the ring, if weight is 1, place once in the ring
for (int i = 0; i < weight; i++) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Respecting your feedback, �I have implemented two methods: binary search with normalized weights and GCD.
However, I think a binary search, as implemented in the weighted round robin strategy, is sufficient.
so how about not using GCD but using binary search only?

@Bue-von-hon Bue-von-hon marked this pull request as ready for review May 13, 2023 11:50
int getXXHash(String input) {
final byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8);
final XxHash xxHash = new XxHash();
final long hashBytes = xxHash.hash(inputBytes, inputBytes.length);
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think of using Hashing.murmur3_32_fixed().hashBytes(inputBytes).asInt() instead at the moment? Many files are forked than I expected.

Adopting XxHash could be considered a separate issue. If we can take advantage of it, we need to shade and trim the library.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the kind suggestion. i fixed it!

return gcd;
}

int gcd(int a, int b) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we replace this tail recursion with iteration? Modern IDE supports automatic conversion.

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 it!

Comment on lines 111 to 117
// When the size of the GCD is too small and exceeds the size of the ring,
// using binary search for find x where Σ (w[i] / x) ≤ ring_size, w[i] is endpoint's weight at index i
List<Integer> arr = new ArrayList<>();
for (Endpoint endpoint : this.endpoints) {
int weight = endpoint.weight();
arr.add(weight);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we need to start else block 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.

i added it!

}
}

// When the size of the GCD is too small and exceeds the size of the ring,
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see the size of the ring is a reasonable limitation for the maximum size of the ring. Some users might want to use more memory to get exact weighted balancing.

What do you think of taking the maximum size of the ring as an input parameter?

void select() {
assertThat(EndpointGroup.of(Endpoint.parse("localhost:1234"),
Endpoint.parse("localhost:2345"))
.selectNow(ctx)).isNotNull();
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this selection use RingHashEndpointSelectionStrategy? How do users use RingHashEndpointSelectionStrategy for their load-balancing strategy? 🤔

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 your suggestions would be very convenient for users if implemented.
However, in the existing interface, the size option is not taken into account, which makes the implementation a bit difficult for me.
I would appreciate any ideas on how to incorporate a size option.

Bue-von-hon and others added 4 commits May 30, 2023 16:54
…shEndpointSelectionStrategy.java

Co-authored-by: Ikhun Um <[email protected]>
…shEndpointSelectionStrategy.java

Co-authored-by: Ikhun Um <[email protected]>
…shEndpointSelectionStrategy.java

Co-authored-by: Ikhun Um <[email protected]>
Motivation:
Removed unnecessary files and added test case and fixed bugs inherent in the algorithm. Removed the use of the xxHash library.

Modification:
- Fix(EndpointSelectionStrategy): Add static factory methods
- Fix(RingHashEndpointSelectionStrategy): Fixed a bug where ring hash select resulted in the same key every time.
fixed invalid method names and missing tail recursion and added eles statements
- Fix(RingHashEndpointSelectionStrategyTest): Added test case
- Remove(Access, CharSequenceAccess, Primitives, UnsafeAccess, XxHash): delete unused file
@Bue-von-hon Bue-von-hon marked this pull request as draft June 10, 2023 10:00
@Bue-von-hon
Copy link
Contributor Author

gentle ping @trustin @ikhoon @jrhee17 @minwoox

@Bue-von-hon
Copy link
Contributor Author

gentle ping @trustin @ikhoon @jrhee17 @minwoox

@Bue-von-hon Bue-von-hon marked this pull request as draft February 28, 2024 01:08
@github-actions github-actions bot added the Stale label Apr 29, 2024
@github-actions github-actions bot removed the Stale label Aug 17, 2024
@Bue-von-hon Bue-von-hon marked this pull request as ready for review September 15, 2024 06:42
@Bue-von-hon
Copy link
Contributor Author

Bue-von-hon commented Sep 15, 2024

I fixed some minor mistakes, changed the hashing algorithm, and of course shaded the dependencies.
PTAL 🙏

I just realized a mistake in the shading implementation and am fixing it.

@Bue-von-hon Bue-von-hon marked this pull request as draft September 15, 2024 11:40
@Bue-von-hon
Copy link
Contributor Author

@ikhoon @trustin @minwoox @jrhee17
finally fixed the shading feature!
I actually unzipped it to check it out and it looks like this.
PTAL 🙏

unzip -l build/libs/armeria-shaded-1.30.2-SNAPSHOT.jar | grep openhft
        0  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/
      256  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/Access$1.class
     3183  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/Access$ReverseAccess.class
     3538  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/Access.class
     1539  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/LongHashFunction.class
      268  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/Primitives$1.class
      786  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/Primitives$ByteOrderHelper.class
      894  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/Primitives$ByteOrderHelperReverse.class
     1420  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/Primitives.class
      274  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/UnsafeAccess$1.class
     1156  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/UnsafeAccess$OldUnsafeAccessBigEndian.class
     1169  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/UnsafeAccess$OldUnsafeAccessLittleEndian.class
     4253  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/UnsafeAccess.class
     1717  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/XxHash$AsLongHashFunction.class
     2833  09-15-2024 21:15   com/linecorp/armeria/internal/shaded/openhft/XxHash.class

@Bue-von-hon Bue-von-hon marked this pull request as ready for review September 15, 2024 12:34
@Bue-von-hon
Copy link
Contributor Author

gentle ping @trustin @ikhoon @jrhee17 @minwoox

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add the Ring hash selection strategy
3 participants