-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Hash Function for UUID Primary Keys #12538
Conversation
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
Outdated
Show resolved
Hide resolved
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
Outdated
Show resolved
Hide resolved
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #12538 +/- ##
============================================
+ Coverage 61.75% 62.14% +0.39%
+ Complexity 207 198 -9
============================================
Files 2436 2517 +81
Lines 133233 137911 +4678
Branches 20636 21342 +706
============================================
+ Hits 82274 85709 +3435
- Misses 44911 45814 +903
- Partials 6048 6388 +340
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
Outdated
Show resolved
Hide resolved
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
Outdated
Show resolved
Hide resolved
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
Outdated
Show resolved
Hide resolved
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
Outdated
Show resolved
Hide resolved
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
Outdated
Show resolved
Hide resolved
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 @ankitsultana for the PR, LGTM.
@chenboat @satishd : I was thinking about this today and I think appending a null-byte is not the right approach, since it can lead to collisions (albeit on sub-optimally defined primary-keys). I have instead resorted to prepending the length of the byte-array for each element in the |
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.
@ankitsultana
If any of the values is not of type UUID and hash function is used as UUID, it generates a string, and appending string bytes prefixed with the respective length. But that can also cause key collisions.
Other alternatives when any of the values is invalid UUID type:
- Use primaryKey.asBytes() as the intention is more like a no-op hash function as you mentioned in this PR description.
- Fallback to Murmur3 hash.
wdyt?
@satishd I guess you mean collision between two primary keys where one is UUID conformant and the other is not? Unless we prepend the length of the string in the UUID conformant serialization scheme, there will always be theoretical chances of collision. I think prepending the length of the string removes chances of such collision beyond any reasonable doubt. I got rid of the null terminated strings because those felt too easy to collide (e.g. |
Right.
@ankitsultana Other approach like falling back to Murmur3 may create collisions but reduce size. I understand that you are trying to make a tradeoff here for consistency and storage by trying to generate the unique keys even when the values are not conforming to the respective UUID format. But it can cause collisions in very rare scenarios. This is because it uses These are rare scenarios and the proposed implementation in this PR seems reasonable to me. |
Can you be more specific on the PR title and description section on what is the hash function used? It is not clear currently. |
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
Show resolved
Hide resolved
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
Outdated
Show resolved
Hide resolved
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
Outdated
Show resolved
Hide resolved
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 fallback logic is kind of customized behavior. I'm okay moving forward, but we should consider making the hash function pluggable so that you may plug your own customized hash (this is actually not hash, but compression)
* {@link UUID} object. The 16 bytes of each UUID are appended to a buffer which is then returned in the result. | ||
* If any of the values is not a valid UUID, then we return the result of {@link #concatenate}. | ||
*/ | ||
public static byte[] hashUUID(Object[] values) { |
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.
For clarification, we are not really hashing the values, but compress it instead right?
pinot-segment-local/src/main/java/org/apache/pinot/segment/local/utils/HashUtils.java
Outdated
Show resolved
Hide resolved
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.
LGTM
This can help reduce memory (disk for off-heap hashmap) usage for scenarios where all columns of the Primary Key are UUIDs (as defined in RFC 4122). This can also handle scenarios where some values are not valid UUIDs, by simply switching to being a no-op hash-function.
Using Murmur's 128 bit hash-function will either give the same or higher space savings (in case of composite keys), but it risks hash collision. Though chances of that are extremely low, we usually avoid any exposure for critical use-cases.
Test Plan: Added UTs. Also tested this in our cluster and some results are as follows.
Testing on Cluster (~35% Old Gen improvement)
Onboarded some test tables to one of our large servers. Each server ingested from the same Kafka topic, and both of them ended up with ~397M primary keys.
You can see the memory savings in the graph below. At 17:21 I triggered a full GC by running
jmap -histo:live
on both servers. I triggered a full GC again a few minutes later which corresponds to the second dip in old gen.Here's the output of
jmap -histo:live
in the two servers:Server with no hashing:
Server with UUID hashing: