Skip to content

Commit c9e7e8a

Browse files
committed
Added prefix match for consistent hash
1 parent 24b0969 commit c9e7e8a

File tree

3 files changed

+74
-20
lines changed

3 files changed

+74
-20
lines changed

consistenthash/consistenthash.go

+54-8
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,32 @@ package consistenthash
1919

2020
import (
2121
"hash/crc32"
22+
"math/bits"
2223
"sort"
2324
"strconv"
2425
)
2526

2627
type Hash func(data []byte) uint32
2728

2829
type Map struct {
29-
hash Hash
30-
replicas int
31-
keys []int // Sorted
32-
hashMap map[int]string
30+
hash Hash
31+
replicas int
32+
prefixTableExpansion int
33+
34+
keys []int // Sorted
35+
hashMap map[int]string
36+
37+
prefixBits uint32
38+
prefixShift uint32
39+
prefixTable []string
3340
}
3441

35-
func New(replicas int, fn Hash) *Map {
42+
func New(replicas int, tableExpansion int, fn Hash) *Map {
3643
m := &Map{
37-
replicas: replicas,
38-
hash: fn,
39-
hashMap: make(map[int]string),
44+
replicas: replicas,
45+
hash: fn,
46+
hashMap: make(map[int]string),
47+
prefixTableExpansion: tableExpansion,
4048
}
4149
if m.hash == nil {
4250
m.hash = crc32.ChecksumIEEE
@@ -59,6 +67,37 @@ func (m *Map) Add(keys ...string) {
5967
}
6068
}
6169
sort.Ints(m.keys)
70+
71+
// Find minimum number of bits to hold |keys| * prefixTableExpansion
72+
m.prefixBits = uint32(bits.Len32(uint32(len(m.keys) * m.prefixTableExpansion)))
73+
m.prefixShift = 32 - m.prefixBits
74+
75+
prefixTableSize := 1 << m.prefixBits
76+
m.prefixTable = make([]string, prefixTableSize)
77+
78+
previousKeyPrefix := -1 // Effectively -Inf
79+
currentKeyIdx := 0
80+
currentKeyPrefix := m.keys[currentKeyIdx] >> m.prefixShift
81+
82+
for i := range m.prefixTable {
83+
if previousKeyPrefix < i && currentKeyPrefix > i {
84+
// All keys with this prefix will map to a single value
85+
m.prefixTable[i] = m.hashMap[m.keys[currentKeyIdx]]
86+
} else {
87+
// Several keys might have the same prefix. Walk
88+
// over them until it changes
89+
previousKeyPrefix = currentKeyPrefix
90+
for currentKeyPrefix == previousKeyPrefix {
91+
currentKeyIdx++
92+
if currentKeyIdx < len(m.keys) {
93+
currentKeyPrefix = m.keys[currentKeyIdx] >> m.prefixShift
94+
} else {
95+
currentKeyIdx = 0
96+
currentKeyPrefix = prefixTableSize + 1 // Effectively +Inf
97+
}
98+
}
99+
}
100+
}
62101
}
63102

64103
// Gets the closest item in the hash to the provided key.
@@ -69,6 +108,13 @@ func (m *Map) Get(key string) string {
69108

70109
hash := int(m.hash([]byte(key)))
71110

111+
// Look for the hash prefix in the prefix table
112+
prefixSlot := hash >> m.prefixShift
113+
tableResult := m.prefixTable[prefixSlot]
114+
if len(tableResult) > 0 {
115+
return tableResult
116+
}
117+
72118
// Binary search for appropriate replica.
73119
idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash })
74120

consistenthash/consistenthash_test.go

+16-10
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestHashing(t *testing.T) {
2626

2727
// Override the hash function to return easier to reason about values. Assumes
2828
// the keys can be converted to an integer.
29-
hash := New(3, func(key []byte) uint32 {
29+
hash := New(3, 6, func(key []byte) uint32 {
3030
i, err := strconv.Atoi(string(key))
3131
if err != nil {
3232
panic(err)
@@ -66,8 +66,8 @@ func TestHashing(t *testing.T) {
6666
}
6767

6868
func TestConsistency(t *testing.T) {
69-
hash1 := New(1, nil)
70-
hash2 := New(1, nil)
69+
hash1 := New(1, 6, nil)
70+
hash2 := New(1, 6, nil)
7171

7272
hash1.Add("Bill", "Bob", "Bonny")
7373
hash2.Add("Bob", "Bonny", "Bill")
@@ -86,25 +86,31 @@ func TestConsistency(t *testing.T) {
8686

8787
}
8888

89-
func BenchmarkGet8(b *testing.B) { benchmarkGet(b, 8) }
90-
func BenchmarkGet32(b *testing.B) { benchmarkGet(b, 32) }
91-
func BenchmarkGet128(b *testing.B) { benchmarkGet(b, 128) }
92-
func BenchmarkGet512(b *testing.B) { benchmarkGet(b, 512) }
89+
func BenchmarkGet8(b *testing.B) { benchmarkGet(b, 8, 6) }
90+
func BenchmarkGet32(b *testing.B) { benchmarkGet(b, 32, 6) }
91+
func BenchmarkGet128(b *testing.B) { benchmarkGet(b, 128, 6) }
92+
func BenchmarkGet512(b *testing.B) { benchmarkGet(b, 512, 6) }
9393

94-
func benchmarkGet(b *testing.B, shards int) {
94+
func benchmarkGet(b *testing.B, shards int, expansion int) {
9595

96-
hash := New(50, nil)
96+
hash := New(50, expansion, nil)
9797

9898
var buckets []string
9999
for i := 0; i < shards; i++ {
100100
buckets = append(buckets, fmt.Sprintf("shard-%d", i))
101101
}
102102

103+
testStringCount := shards
104+
var testStrings []string
105+
for i := 0; i < testStringCount; i++ {
106+
testStrings = append(testStrings, fmt.Sprintf("%d", i))
107+
}
108+
103109
hash.Add(buckets...)
104110

105111
b.ResetTimer()
106112

107113
for i := 0; i < b.N; i++ {
108-
hash.Get(buckets[i&(shards-1)])
114+
hash.Get(testStrings[i&(testStringCount-1)])
109115
}
110116
}

http.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ const defaultBasePath = "/_groupcache/"
3434

3535
const defaultReplicas = 50
3636

37+
const defaultHashExpansion = 6
38+
3739
// HTTPPool implements PeerPicker for a pool of HTTP peers.
3840
type HTTPPool struct {
3941
// Context optionally specifies a context for the server to use when it
@@ -106,7 +108,7 @@ func NewHTTPPoolOpts(self string, o *HTTPPoolOptions) *HTTPPool {
106108
if p.opts.Replicas == 0 {
107109
p.opts.Replicas = defaultReplicas
108110
}
109-
p.peers = consistenthash.New(p.opts.Replicas, p.opts.HashFn)
111+
p.peers = consistenthash.New(p.opts.Replicas, defaultHashExpansion, p.opts.HashFn)
110112

111113
RegisterPeerPicker(func() PeerPicker { return p })
112114
return p
@@ -118,7 +120,7 @@ func NewHTTPPoolOpts(self string, o *HTTPPoolOptions) *HTTPPool {
118120
func (p *HTTPPool) Set(peers ...string) {
119121
p.mu.Lock()
120122
defer p.mu.Unlock()
121-
p.peers = consistenthash.New(p.opts.Replicas, p.opts.HashFn)
123+
p.peers = consistenthash.New(p.opts.Replicas, defaultHashExpansion, p.opts.HashFn)
122124
p.peers.Add(peers...)
123125
p.httpGetters = make(map[string]*httpGetter, len(peers))
124126
for _, peer := range peers {

0 commit comments

Comments
 (0)