Kafka
Murmur2
Go Language
Porting Code
Programming Implementation

Porting Kafka's murmur2 implementation to Go

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Apache Kafka uses the MurmurHash2 algorithm to decide which partition a particular message is sent to, based on the key that is associated with the message. This essential feature of message partitioning allows Kafka to achieve high levels of scalability by parallel processing. Porting this hashing function into Go can be beneficial for developers working with systems that integrate both Kafka and Go applications.

MurmurHash2 Algorithm

MurmurHash, designed by Austin Appleby, is a non-cryptographic hash function suitable for general hash-based lookup. The version Kafka uses is MurmurHash2, which balances good hash distribution and speed. In Kafka’s Java client, the hash function is tweaked* to always return a positive number by masking the high bit.

Porting to Go

A direct translation of the Java implementation into Go ensures that messages produced to Kafka from Go applications consistently map to the same partitions as their Java counterparts. Let us look at the following implementation step by step.

Step 1: Seed and Data Preparation

Kafka's implementation uses a specific seed (0x9747b28c) which must be consistent in the Go implementation. Go allows direct manipulation of bytes, so no special preparation of the message data is required as long as it’s in a byte slice format.

go
1const seed = 0x9747b28c
2
3func murmur2(data []byte) uint32 {
4    length := len(data)
5    h := uint32(seed)
6    const c1 = 0xcc9e2d51
7    const c2 = 0x1b873593
8    ...
9}

Step 2: Body

In the MurmurHash2 algorithm, the hash is accumulated in blocks of 4 bytes (32 bits). The data is treated as an array of uint32, processed in a loop.

go
1for i := 0; i < length/4; i++ {
2    k := *(*uint32)(unsafe.Pointer(&data[i*4]))
3    k *= c1
4    k = (k << 15) | (k >> (32 - 15)) // Rotate left 15
5    k *= c2
6
7    h ^= k
8    h = (h << 13) | (h >> (32 - 13)) // Rotate left 13
9    h = h*5 + 0xe6546b64
10}

Step 3: Tail

The remaining bytes (less than 4 bytes) need special handling:

go
1tail := data[length&^3:] // equivalent to `length % 4`
2
3switch len(tail) {
4    case 3:
5        h ^= uint32(tail[2]) << 16
6    case 2:
7        h ^= uint32(tail[1]) << 8
8    case 1:
9        h ^= uint32(tail[0])
10        h *= c1
11        h = (h << 13) | (h >> (32 - 13))
12        h *= c2
13}

Step 4: Finalization

Final mixing of bits to reduce the avalanche effect:

go
1h ^= uint32(length)
2h ^= (h >> 16)
3h *= 0x85ebca6b
4h ^= (h >> 13)
5h *= 0xc2b2ae35
6h ^= (h >> 16)

The result is masked to ensure positivity:

go
return h & 0x7fffffff

Porting Considerations

  • Handling Unsafe Code: Go's unsafe package is required for reading bytes as uint32. This operation must be handled with care especially to respect endianness.
  • Performance: Go's static compilation and optimization may not produce an identical output to Java's JIT-compiled code. It’s important to benchmark the Go implementation against Java, especially for performance-critical applications.
  • Endianness: Kafka assumes a little-endian format in its original implementation. Ensure that your Go implementation respects the byte order of your platform or enforces little-endian order.

Summary Table

plaintext
1| Component | Details | Considerations |
2| --------------------- | ----------------------------------------------- | ------------------------------------- |
3| Seed | Fixed seed of 0x9747b28c | Must be consistent with Java client |
4| Processing block | 4-byte block, processed with constants `c1` and `c2` | Uses unsafe for performance |
5| Tail Handling | Processes remaining bytes < 4 | Ensure correct byte handling |
6| Finalization | Mixes bits to obfuscate the input pattern | Critical for uniform distribution |
7| Output | Masks the high bit to ensure non-negative IDs | Compatibility with Java partitioner | ``` |
8
9By following the steps above, we can ensure that Go applications will appropriately distribute messages across Kafka partitions, maintaining consistency with Java counterparts. This enables a unified architecture for distributed systems leveraging both Go and Kafka.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.