Switch to xxhash64

This commit is contained in:
Vladimir Mihailenco 2020-04-19 08:52:58 +03:00
parent dbd3e7ca18
commit 039194a5d3
5 changed files with 79 additions and 36 deletions

View File

@ -18,29 +18,28 @@ limitations under the License.
package consistenthash package consistenthash
import ( import (
"hash/crc32"
"sort" "sort"
"strconv" "strconv"
"github.com/cespare/xxhash"
"github.com/go-redis/redis/v7/internal" "github.com/go-redis/redis/v7/internal"
) )
type Map struct { type Map struct {
hash internal.Hash hash internal.Hash
replicas int replicas int
keys []int // Sorted keys []uint64 // Sorted
hashMap map[int]string hashMap map[uint64]string
} }
func New(replicas int, fn internal.Hash) *Map { func New(replicas int, fn internal.Hash) *Map {
m := &Map{ m := &Map{
replicas: replicas, replicas: replicas,
hash: fn, hash: fn,
hashMap: make(map[int]string), hashMap: make(map[uint64]string),
} }
if m.hash == nil { if m.hash == nil {
m.hash = crc32.ChecksumIEEE m.hash = xxhash.Sum64
} }
return m return m
} }
@ -54,12 +53,14 @@ func (m *Map) IsEmpty() bool {
func (m *Map) Add(keys ...string) { func (m *Map) Add(keys ...string) {
for _, key := range keys { for _, key := range keys {
for i := 0; i < m.replicas; i++ { for i := 0; i < m.replicas; i++ {
hash := int(m.hash([]byte(strconv.Itoa(i) + key))) hash := m.hash([]byte(strconv.Itoa(i) + key))
m.keys = append(m.keys, hash) m.keys = append(m.keys, hash)
m.hashMap[hash] = key m.hashMap[hash] = key
} }
} }
sort.Ints(m.keys) sort.Slice(m.keys, func(i, j int) bool {
return m.keys[i] < m.keys[j]
})
} }
// Gets the closest item in the hash to the provided key. // Gets the closest item in the hash to the provided key.
@ -68,7 +69,7 @@ func (m *Map) Get(key string) string {
return "" return ""
} }
hash := int(m.hash([]byte(key))) hash := m.hash([]byte(key))
// Binary search for appropriate replica. // Binary search for appropriate replica.
idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash }) idx := sort.Search(len(m.keys), func(i int) bool { return m.keys[i] >= hash })

View File

@ -26,12 +26,12 @@ func TestHashing(t *testing.T) {
// Override the hash function to return easier to reason about values. Assumes // Override the hash function to return easier to reason about values. Assumes
// the keys can be converted to an integer. // the keys can be converted to an integer.
hash := New(3, func(key []byte) uint32 { hash := New(3, func(key []byte) uint64 {
i, err := strconv.Atoi(string(key)) i, err := strconv.ParseUint(string(key), 10, 64)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return uint32(i) return i
}) })
// Given the above hash function, this will give replicas with "hashes": // Given the above hash function, this will give replicas with "hashes":
@ -76,6 +76,7 @@ func TestConsistency(t *testing.T) {
t.Errorf("Fetching 'Ben' from both hashes should be the same") t.Errorf("Fetching 'Ben' from both hashes should be the same")
} }
hash1.Add("Ben", "Becky", "Bobby")
hash2.Add("Becky", "Ben", "Bobby") hash2.Add("Becky", "Ben", "Bobby")
if hash1.Get("Ben") != hash2.Get("Ben") || if hash1.Get("Ben") != hash2.Get("Ben") ||
@ -108,3 +109,23 @@ func benchmarkGet(b *testing.B, shards int) {
hash.Get(buckets[i&(shards-1)]) hash.Get(buckets[i&(shards-1)])
} }
} }
func TestDistribution(t *testing.T) {
hash := New(1000, nil)
hash.Add("1", "2", "3", "4", "5", "6")
results := make(map[string]int, 10)
for i := 0; i < 1000000; i++ {
key := strconv.Itoa(i)
site := hash.Get(key)
if val, ok := results[site]; ok {
results[site] = val + 1
} else {
results[site] = 1
}
}
fmt.Println(results)
}

View File

@ -1,3 +1,3 @@
package internal package internal
type Hash func(data []byte) uint32 type Hash func(data []byte) uint64

View File

@ -1,9 +1,7 @@
package rendezvoushash package rendezvoushash
import ( import (
"crypto/sha1" "github.com/cespare/xxhash"
"hash/crc32"
"github.com/go-redis/redis/v7/internal" "github.com/go-redis/redis/v7/internal"
) )
@ -17,7 +15,7 @@ func New(fn internal.Hash) *Map {
hash: fn, hash: fn,
} }
if m.hash == nil { if m.hash == nil {
m.hash = crc32.ChecksumIEEE m.hash = xxhash.Sum64
} }
return m return m
} }
@ -41,12 +39,15 @@ func (m *Map) Get(key string) string {
} }
// find the site that, when hashed with the key, yields the largest weight // find the site that, when hashed with the key, yields the largest weight
maxWeight := uint32(0) var targetSite string
targetSite := ""
var maxWeight uint64
buf := make([]byte, len(key), 2*len(key))
copy(buf, key)
for _, site := range m.sites { for _, site := range m.sites {
hasher := sha1.New() buf = buf[:len(key)]
hasher.Write([]byte(site + key)) buf = append(buf, site...)
siteWeight := m.hash(hasher.Sum(nil)) siteWeight := m.hash(buf)
if siteWeight > maxWeight { if siteWeight > maxWeight {
maxWeight = siteWeight maxWeight = siteWeight

View File

@ -2,12 +2,12 @@ package rendezvoushash
import ( import (
"fmt" "fmt"
"hash/crc32" "strconv"
"testing" "testing"
) )
func TestHashing(t *testing.T) { func TestHashing(t *testing.T) {
hash := New(crc32.ChecksumIEEE) hash := New(nil)
hash.Add("site1", "site2", "site3") hash.Add("site1", "site2", "site3")
verifyFn := func(cases map[string]string) { verifyFn := func(cases map[string]string) {
@ -84,3 +84,23 @@ func benchmarkGet(b *testing.B, shards int) {
hash.Get(buckets[i&(shards-1)]) hash.Get(buckets[i&(shards-1)])
} }
} }
func TestDistribution(t *testing.T) {
hash := New(nil)
hash.Add("1", "2", "3", "4", "5", "6")
results := make(map[string]int, 10)
for i := 0; i < 1000000; i++ {
key := strconv.Itoa(i)
site := hash.Get(key)
if val, ok := results[site]; ok {
results[site] = val + 1
} else {
results[site] = 1
}
}
fmt.Println(results)
}