-
Notifications
You must be signed in to change notification settings - Fork 922
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
base: main
Are you sure you want to change the base?
Conversation
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.
Looking at the wighted round robin, |
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++) { |
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.
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.
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.
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?
core/build.gradle
Outdated
@@ -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' |
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.
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.
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.
Forked XxHash into our code base.
But due to the complex inheritance, several new files have been created.
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.
Thanks for your pull request, @Bue-von-hon! Could you add some test cases?
@@ -0,0 +1,86 @@ | |||
/* | |||
* Copyright 2016 LINE Corporation |
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.
* Copyright 2016 LINE Corporation | |
* Copyright 2023 LINE Corporation |
core/src/main/java/com/linecorp/armeria/client/endpoint/RingHashEndpointSelectionStrategy.java
Outdated
Show resolved
Hide resolved
} | ||
|
||
@Override | ||
public @Nullable Endpoint selectNow(ClientRequestContext ctx) { |
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.
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)
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.
removed @nullable annotation!
final WeightedRingEndpoint weightedRingEndpoint = this.weightedRingEndpoint; | ||
return weightedRingEndpoint.select(ctx.endpoint()); |
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.
Q: Is there any chance of weightedRingEndpoint
being null
? For example, the listener you added in the construtor might not be notified early enough.
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 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); |
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.
Could we use the Int2ObjectSortedMap
instead?
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 it!
…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.
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. |
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.
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.)
core/build.gradle
Outdated
@@ -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' |
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.
Forked XxHash into our code base.
But due to the complex inheritance, several new files have been created.
final WeightedRingEndpoint weightedRingEndpoint = this.weightedRingEndpoint; | ||
return weightedRingEndpoint.select(ctx.endpoint()); |
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 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); |
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 it!
} | ||
|
||
@Override | ||
public @Nullable Endpoint selectNow(ClientRequestContext ctx) { |
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.
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++) { |
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.
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?
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); |
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.
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.
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.
Thanks for the kind suggestion. i fixed it!
return gcd; | ||
} | ||
|
||
int gcd(int a, int b) { |
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.
Should we replace this tail recursion with iteration? Modern IDE supports automatic conversion.
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 it!
core/src/main/java/com/linecorp/armeria/client/endpoint/RingHashEndpointSelectionStrategy.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/client/endpoint/RingHashEndpointSelectionStrategy.java
Outdated
Show resolved
Hide resolved
// 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); | ||
} |
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.
Don't we need to start else
block 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.
i added it!
core/src/main/java/com/linecorp/armeria/client/endpoint/RingHashEndpointSelectionStrategy.java
Outdated
Show resolved
Hide resolved
} | ||
} | ||
|
||
// When the size of the GCD is too small and exceeds the size of the ring, |
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 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(); |
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 this selection use RingHashEndpointSelectionStrategy
? How do users use RingHashEndpointSelectionStrategy
for their load-balancing strategy? 🤔
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 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.
…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
I just realized a mistake in the shading implementation and am fixing it. |
@ikhoon @trustin @minwoox @jrhee17 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 |
Motivation:
this Resolves #4698 issue
Added Ring hash strategy for For a broader selection.
Motification:
Result:
For now on, users will be able to use Ring hashes for load balancing.