├── .gitignore ├── LICENSE ├── README.md ├── consistent.go ├── consistent_test.go ├── example_test.go ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Khalid Lafi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Package consistent 2 | A Golang implementation of Consistent Hashing and Consistent Hashing With Bounded Loads. 3 | 4 | https://en.wikipedia.org/wiki/Consistent_hashing 5 | 6 | https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 7 | 8 | 9 | ### Consistent Hashing Example 10 | 11 | ```go 12 | 13 | package main 14 | 15 | import ( 16 | "log" 17 | "github.com/lafikl/consistent" 18 | ) 19 | 20 | func main() { 21 | c := consistent.New() 22 | 23 | // adds the hosts to the ring 24 | c.Add("127.0.0.1:8000") 25 | c.Add("92.0.0.1:8000") 26 | 27 | // Returns the host that owns `key`. 28 | // 29 | // As described in https://en.wikipedia.org/wiki/Consistent_hashing 30 | // 31 | // It returns ErrNoHosts if the ring has no hosts in it. 32 | host, err := c.Get("/app.html") 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | log.Println(host) 38 | } 39 | 40 | ``` 41 | 42 | 43 | ### Consistent Hashing With Bounded Loads Example 44 | 45 | ```go 46 | 47 | package main 48 | 49 | import ( 50 | "log" 51 | "github.com/lafikl/consistent" 52 | ) 53 | 54 | func main() { 55 | c := consistent.New() 56 | 57 | // adds the hosts to the ring 58 | c.Add("127.0.0.1:8000") 59 | c.Add("92.0.0.1:8000") 60 | 61 | // It uses Consistent Hashing With Bounded loads 62 | // https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 63 | // to pick the least loaded host that can serve the key 64 | // 65 | // It returns ErrNoHosts if the ring has no hosts in it. 66 | // 67 | host, err := c.GetLeast("/app.html") 68 | if err != nil { 69 | log.Fatal(err) 70 | } 71 | 72 | // increases the load of `host`, we have to call it before sending the request 73 | c.Inc(host) 74 | 75 | // send request or do whatever 76 | log.Println("send request to", host) 77 | 78 | // call it when the work is done, to update the load of `host`. 79 | c.Done(host) 80 | 81 | } 82 | 83 | ``` 84 | 85 | 86 | ## Docs 87 | 88 | https://godoc.org/github.com/lafikl/consistent 89 | 90 | 91 | 92 | # License 93 | 94 | ``` 95 | MIT License 96 | 97 | Copyright (c) 2017 Khalid Lafi 98 | 99 | Permission is hereby granted, free of charge, to any person obtaining a copy 100 | of this software and associated documentation files (the "Software"), to deal 101 | in the Software without restriction, including without limitation the rights 102 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 103 | copies of the Software, and to permit persons to whom the Software is 104 | furnished to do so, subject to the following conditions: 105 | 106 | The above copyright notice and this permission notice shall be included in all 107 | copies or substantial portions of the Software. 108 | 109 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 110 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 111 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 112 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 113 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 114 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 115 | SOFTWARE. 116 | 117 | ``` 118 | -------------------------------------------------------------------------------- /consistent.go: -------------------------------------------------------------------------------- 1 | // An implementation of Consistent Hashing and 2 | // Consistent Hashing With Bounded Loads. 3 | // 4 | // https://en.wikipedia.org/wiki/Consistent_hashing 5 | // 6 | // https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 7 | package consistent 8 | 9 | import ( 10 | "encoding/binary" 11 | "errors" 12 | "fmt" 13 | "math" 14 | "sort" 15 | "sync" 16 | "sync/atomic" 17 | 18 | blake2b "github.com/minio/blake2b-simd" 19 | ) 20 | 21 | const replicationFactor = 10 22 | 23 | var ErrNoHosts = errors.New("no hosts added") 24 | 25 | type Host struct { 26 | Name string 27 | Load int64 28 | } 29 | 30 | type Consistent struct { 31 | hosts map[uint64]string 32 | sortedSet []uint64 33 | loadMap map[string]*Host 34 | totalLoad int64 35 | 36 | sync.RWMutex 37 | } 38 | 39 | func New() *Consistent { 40 | return &Consistent{ 41 | hosts: map[uint64]string{}, 42 | sortedSet: []uint64{}, 43 | loadMap: map[string]*Host{}, 44 | } 45 | } 46 | 47 | func (c *Consistent) Add(host string) { 48 | c.Lock() 49 | defer c.Unlock() 50 | 51 | if _, ok := c.loadMap[host]; ok { 52 | return 53 | } 54 | 55 | c.loadMap[host] = &Host{Name: host, Load: 0} 56 | for i := 0; i < replicationFactor; i++ { 57 | h := c.hash(fmt.Sprintf("%s%d", host, i)) 58 | c.hosts[h] = host 59 | c.sortedSet = append(c.sortedSet, h) 60 | 61 | } 62 | // sort hashes ascendingly 63 | sort.Slice(c.sortedSet, func(i int, j int) bool { 64 | if c.sortedSet[i] < c.sortedSet[j] { 65 | return true 66 | } 67 | return false 68 | }) 69 | } 70 | 71 | // Returns the host that owns `key`. 72 | // 73 | // As described in https://en.wikipedia.org/wiki/Consistent_hashing 74 | // 75 | // It returns ErrNoHosts if the ring has no hosts in it. 76 | func (c *Consistent) Get(key string) (string, error) { 77 | c.RLock() 78 | defer c.RUnlock() 79 | 80 | if len(c.hosts) == 0 { 81 | return "", ErrNoHosts 82 | } 83 | 84 | h := c.hash(key) 85 | idx := c.search(h) 86 | return c.hosts[c.sortedSet[idx]], nil 87 | } 88 | 89 | // It uses Consistent Hashing With Bounded loads 90 | // 91 | // https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 92 | // 93 | // to pick the least loaded host that can serve the key 94 | // 95 | // It returns ErrNoHosts if the ring has no hosts in it. 96 | // 97 | func (c *Consistent) GetLeast(key string) (string, error) { 98 | c.RLock() 99 | defer c.RUnlock() 100 | 101 | if len(c.hosts) == 0 { 102 | return "", ErrNoHosts 103 | } 104 | 105 | h := c.hash(key) 106 | idx := c.search(h) 107 | 108 | i := idx 109 | for { 110 | host := c.hosts[c.sortedSet[i]] 111 | if c.loadOK(host) { 112 | return host, nil 113 | } 114 | i++ 115 | if i >= len(c.hosts) { 116 | i = 0 117 | } 118 | } 119 | } 120 | 121 | func (c *Consistent) search(key uint64) int { 122 | idx := sort.Search(len(c.sortedSet), func(i int) bool { 123 | return c.sortedSet[i] >= key 124 | }) 125 | 126 | if idx >= len(c.sortedSet) { 127 | idx = 0 128 | } 129 | return idx 130 | } 131 | 132 | // Sets the load of `host` to the given `load` 133 | func (c *Consistent) UpdateLoad(host string, load int64) { 134 | c.Lock() 135 | defer c.Unlock() 136 | 137 | if _, ok := c.loadMap[host]; !ok { 138 | return 139 | } 140 | c.totalLoad -= c.loadMap[host].Load 141 | c.loadMap[host].Load = load 142 | c.totalLoad += load 143 | } 144 | 145 | // Increments the load of host by 1 146 | // 147 | // should only be used with if you obtained a host with GetLeast 148 | func (c *Consistent) Inc(host string) { 149 | c.Lock() 150 | defer c.Unlock() 151 | 152 | if _, ok := c.loadMap[host]; !ok { 153 | return 154 | } 155 | atomic.AddInt64(&c.loadMap[host].Load, 1) 156 | atomic.AddInt64(&c.totalLoad, 1) 157 | } 158 | 159 | // Decrements the load of host by 1 160 | // 161 | // should only be used with if you obtained a host with GetLeast 162 | func (c *Consistent) Done(host string) { 163 | c.Lock() 164 | defer c.Unlock() 165 | 166 | if _, ok := c.loadMap[host]; !ok { 167 | return 168 | } 169 | atomic.AddInt64(&c.loadMap[host].Load, -1) 170 | atomic.AddInt64(&c.totalLoad, -1) 171 | } 172 | 173 | // Deletes host from the ring 174 | func (c *Consistent) Remove(host string) bool { 175 | c.Lock() 176 | defer c.Unlock() 177 | 178 | for i := 0; i < replicationFactor; i++ { 179 | h := c.hash(fmt.Sprintf("%s%d", host, i)) 180 | delete(c.hosts, h) 181 | c.delSlice(h) 182 | } 183 | delete(c.loadMap, host) 184 | return true 185 | } 186 | 187 | // Return the list of hosts in the ring 188 | func (c *Consistent) Hosts() (hosts []string) { 189 | c.RLock() 190 | defer c.RUnlock() 191 | for k, _ := range c.loadMap { 192 | hosts = append(hosts, k) 193 | } 194 | return hosts 195 | } 196 | 197 | // Returns the loads of all the hosts 198 | func (c *Consistent) GetLoads() map[string]int64 { 199 | loads := map[string]int64{} 200 | 201 | for k, v := range c.loadMap { 202 | loads[k] = v.Load 203 | } 204 | return loads 205 | } 206 | 207 | // Returns the maximum load of the single host 208 | // which is: 209 | // (total_load/number_of_hosts)*1.25 210 | // total_load = is the total number of active requests served by hosts 211 | // for more info: 212 | // https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 213 | func (c *Consistent) MaxLoad() int64 { 214 | if c.totalLoad == 0 { 215 | c.totalLoad = 1 216 | } 217 | var avgLoadPerNode float64 218 | avgLoadPerNode = float64(c.totalLoad / int64(len(c.loadMap))) 219 | if avgLoadPerNode == 0 { 220 | avgLoadPerNode = 1 221 | } 222 | avgLoadPerNode = math.Ceil(avgLoadPerNode * 1.25) 223 | return int64(avgLoadPerNode) 224 | } 225 | 226 | func (c *Consistent) loadOK(host string) bool { 227 | // a safety check if someone performed c.Done more than needed 228 | if c.totalLoad < 0 { 229 | c.totalLoad = 0 230 | } 231 | 232 | var avgLoadPerNode float64 233 | avgLoadPerNode = float64((c.totalLoad + 1) / int64(len(c.loadMap))) 234 | if avgLoadPerNode == 0 { 235 | avgLoadPerNode = 1 236 | } 237 | avgLoadPerNode = math.Ceil(avgLoadPerNode * 1.25) 238 | 239 | bhost, ok := c.loadMap[host] 240 | if !ok { 241 | panic(fmt.Sprintf("given host(%s) not in loadsMap", bhost.Name)) 242 | } 243 | 244 | if float64(bhost.Load)+1 <= avgLoadPerNode { 245 | return true 246 | } 247 | 248 | return false 249 | } 250 | 251 | func (c *Consistent) delSlice(val uint64) { 252 | idx := -1 253 | l := 0 254 | r := len(c.sortedSet) - 1 255 | for l <= r { 256 | m := (l + r) / 2 257 | if c.sortedSet[m] == val { 258 | idx = m 259 | break 260 | } else if c.sortedSet[m] < val { 261 | l = m + 1 262 | } else if c.sortedSet[m] > val { 263 | r = m - 1 264 | } 265 | } 266 | if idx != -1 { 267 | c.sortedSet = append(c.sortedSet[:idx], c.sortedSet[idx+1:]...) 268 | } 269 | } 270 | 271 | func (c *Consistent) hash(key string) uint64 { 272 | out := blake2b.Sum512([]byte(key)) 273 | return binary.LittleEndian.Uint64(out[:]) 274 | } 275 | -------------------------------------------------------------------------------- /consistent_test.go: -------------------------------------------------------------------------------- 1 | package consistent 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestAdd(t *testing.T) { 9 | c := New() 10 | 11 | c.Add("127.0.0.1:8000") 12 | if len(c.sortedSet) != replicationFactor { 13 | t.Fatal("vnodes number is incorrect") 14 | } 15 | } 16 | 17 | func TestGet(t *testing.T) { 18 | c := New() 19 | 20 | c.Add("127.0.0.1:8000") 21 | host, err := c.Get("127.0.0.1:8000") 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | if host != "127.0.0.1:8000" { 27 | t.Fatal("returned host is not what expected") 28 | } 29 | } 30 | 31 | func TestRemove(t *testing.T) { 32 | c := New() 33 | 34 | c.Add("127.0.0.1:8000") 35 | c.Remove("127.0.0.1:8000") 36 | 37 | if len(c.sortedSet) != 0 && len(c.hosts) != 0 { 38 | t.Fatal(("remove is not working")) 39 | } 40 | 41 | } 42 | 43 | func TestGetLeast(t *testing.T) { 44 | c := New() 45 | 46 | c.Add("127.0.0.1:8000") 47 | c.Add("92.0.0.1:8000") 48 | 49 | for i := 0; i < 100; i++ { 50 | host, err := c.GetLeast("92.0.0.1:80001") 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | c.Inc(host) 55 | } 56 | 57 | for k, v := range c.GetLoads() { 58 | if v > c.MaxLoad() { 59 | t.Fatalf("host %s is overloaded. %d > %d\n", k, v, c.MaxLoad()) 60 | } 61 | } 62 | fmt.Println("Max load per node", c.MaxLoad()) 63 | fmt.Println(c.GetLoads()) 64 | 65 | } 66 | 67 | func TestIncDone(t *testing.T) { 68 | c := New() 69 | 70 | c.Add("127.0.0.1:8000") 71 | c.Add("92.0.0.1:8000") 72 | 73 | host, err := c.GetLeast("92.0.0.1:80001") 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | c.Inc(host) 79 | if c.loadMap[host].Load != 1 { 80 | t.Fatalf("host %s load should be 1\n", host) 81 | } 82 | 83 | c.Done(host) 84 | if c.loadMap[host].Load != 0 { 85 | t.Fatalf("host %s load should be 0\n", host) 86 | } 87 | 88 | } 89 | 90 | func TestHosts(t *testing.T) { 91 | hosts := []string{ 92 | "127.0.0.1:8000", 93 | "92.0.0.1:8000", 94 | } 95 | 96 | c := New() 97 | for _, h := range hosts { 98 | c.Add(h) 99 | } 100 | fmt.Println("hosts in the ring", c.Hosts()) 101 | 102 | addedHosts := c.Hosts() 103 | for _, h := range hosts { 104 | found := false 105 | for _, ah := range addedHosts { 106 | if h == ah { 107 | found = true 108 | break 109 | } 110 | } 111 | if !found { 112 | t.Fatal("missing host", h) 113 | } 114 | } 115 | c.Remove("127.0.0.1:8000") 116 | fmt.Println("hosts in the ring", c.Hosts()) 117 | 118 | } 119 | 120 | func TestDelSlice(t *testing.T) { 121 | items := []uint64{0, 1, 2, 3, 5, 20, 22, 23, 25, 27, 28, 30, 35, 37, 1008, 1009} 122 | deletes := []uint64{25, 37, 1009, 3, 100000} 123 | 124 | c := &Consistent{} 125 | c.sortedSet = append(c.sortedSet, items...) 126 | 127 | fmt.Printf("before deletion%+v\n", c.sortedSet) 128 | 129 | for _, val := range deletes { 130 | c.delSlice(val) 131 | } 132 | 133 | for _, val := range deletes { 134 | for _, item := range c.sortedSet { 135 | if item == val { 136 | t.Fatalf("%d wasn't deleted\n", val) 137 | } 138 | } 139 | } 140 | 141 | fmt.Printf("after deletions: %+v\n", c.sortedSet) 142 | } -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package consistent_test 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/lafikl/consistent" 8 | ) 9 | 10 | func Example_consistent(t *testing.T) { 11 | c := consistent.New() 12 | 13 | // adds the hosts to the ring 14 | c.Add("127.0.0.1:8000") 15 | c.Add("92.0.0.1:8000") 16 | 17 | // Returns the host that owns `key`. 18 | // 19 | // As described in https://en.wikipedia.org/wiki/Consistent_hashing 20 | // 21 | // It returns ErrNoHosts if the ring has no hosts in it. 22 | host, err := c.Get("/app.html") 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | log.Println(host) 28 | } 29 | 30 | func Example_bounded() { 31 | c := consistent.New() 32 | 33 | // adds the hosts to the ring 34 | c.Add("127.0.0.1:8000") 35 | c.Add("92.0.0.1:8000") 36 | 37 | // It uses Consistent Hashing With Bounded loads 38 | // https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html 39 | // to pick the least loaded host that can serve the key 40 | // 41 | // It returns ErrNoHosts if the ring has no hosts in it. 42 | // 43 | host, err := c.GetLeast("/app.html") 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | // increases the load of `host`, we have to call it before sending the request 48 | c.Inc(host) 49 | // send request or do whatever 50 | log.Println("send request to", host) 51 | // call it when the work is done, to update the load of `host`. 52 | c.Done(host) 53 | } 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lafikl/consistent 2 | 3 | go 1.16 4 | 5 | require github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 h1:lYpkrQH5ajf0OXOcUbGjvZxxijuBwbbmlSxLiuofa+g= 2 | github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= 3 | --------------------------------------------------------------------------------