@marcel91200 / caddy Mirror · read-only / commits / 40b193fb
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 13, 19:15 UTC

reverseproxy: Improve hashing LB policies with HRW (#4724)

* reverseproxy: Improve hashing LB policies with HRW Previously, if a list of upstreams changed, hash-based LB policies would be greatly affected because the hash relied on the position of upstreams in the pool. Highest Random Weight or "rendezvous" hashing is apparently robust to pool changes. It runs in O(n) instead of O(log n), but n is very small usually. * Fix bug and update tests

Matt Holt committed Apr 27, 2022 at 10:39 UTC 40b193fb791e241dec630a37167aac576375bc96
2 files changed +46 -41
modules/caddyhttp/reverseproxy/selectionpolicies.go
+17 -12
@@ -514,21 +514,26 @@ func leastRequests(upstreams []*Upstream) *Upstream {
514 return best[weakrand.Intn(len(best))]
515 }
516
517 -// hostByHashing returns an available host
518 -// from pool based on a hashable string s.
517 +// hostByHashing returns an available host from pool based on a hashable string s.
518 func hostByHashing(pool []*Upstream, s string) *Upstream {
520 - poolLen := uint32(len(pool))
521 - if poolLen == 0 {
522 - return nil
523 - }
524 - index := hash(s) % poolLen
525 - for i := uint32(0); i < poolLen; i++ {
526 - upstream := pool[(index+i)%poolLen]
527 - if upstream.Available() {
528 - return upstream
519 + // Highest Random Weight (HRW, or "Rendezvous") hashing,
520 + // guarantees stability when the list of upstreams changes;
521 + // see https://medium.com/i0exception/rendezvous-hashing-8c00e2fb58b0,
522 + // https://randorithms.com/2020/12/26/rendezvous-hashing.html,
523 + // and https://en.wikipedia.org/wiki/Rendezvous_hashing.
524 + var highestHash uint32
525 + var upstream *Upstream
526 + for _, up := range pool {
527 + if !up.Available() {
528 + continue
529 + }
530 + h := hash(s + up.String()) // important to hash key and server together
531 + if h > highestHash {
532 + highestHash = h
533 + upstream = up
534 }
535 }
531 - return nil
536 + return upstream
537 }
538
539 // hash calculates a fast hash based on s.
modules/caddyhttp/reverseproxy/selectionpolicies_test.go
+29 -29
@@ -22,9 +22,9 @@ import (
22
23 func testPool() UpstreamPool {
24 return UpstreamPool{
25 - {Host: new(Host)},
26 - {Host: new(Host)},
27 - {Host: new(Host)},
25 + {Host: new(Host), Dial: "0.0.0.1"},
26 + {Host: new(Host), Dial: "0.0.0.2"},
27 + {Host: new(Host), Dial: "0.0.0.3"},
28 }
29 }
30
@@ -95,13 +95,13 @@ func TestIPHashPolicy(t *testing.T) {
95 // We should be able to predict where every request is routed.
96 req.RemoteAddr = "172.0.0.1:80"
97 h := ipHash.Select(pool, req, nil)
98 - if h != pool[1] {
99 - t.Error("Expected ip hash policy host to be the second host.")
98 + if h != pool[0] {
99 + t.Error("Expected ip hash policy host to be the first host.")
100 }
101 req.RemoteAddr = "172.0.0.2:80"
102 h = ipHash.Select(pool, req, nil)
103 - if h != pool[1] {
104 - t.Error("Expected ip hash policy host to be the second host.")
103 + if h != pool[0] {
104 + t.Error("Expected ip hash policy host to be the first host.")
105 }
106 req.RemoteAddr = "172.0.0.3:80"
107 h = ipHash.Select(pool, req, nil)
@@ -117,13 +117,13 @@ func TestIPHashPolicy(t *testing.T) {
117 // we should get the same results without a port
118 req.RemoteAddr = "172.0.0.1"
119 h = ipHash.Select(pool, req, nil)
120 - if h != pool[1] {
121 - t.Error("Expected ip hash policy host to be the second host.")
120 + if h != pool[0] {
121 + t.Error("Expected ip hash policy host to be the first host.")
122 }
123 req.RemoteAddr = "172.0.0.2"
124 h = ipHash.Select(pool, req, nil)
125 - if h != pool[1] {
126 - t.Error("Expected ip hash policy host to be the second host.")
125 + if h != pool[0] {
126 + t.Error("Expected ip hash policy host to be the first host.")
127 }
128 req.RemoteAddr = "172.0.0.3"
129 h = ipHash.Select(pool, req, nil)
@@ -138,7 +138,7 @@ func TestIPHashPolicy(t *testing.T) {
138
139 // we should get a healthy host if the original host is unhealthy and a
140 // healthy host is available
141 - req.RemoteAddr = "172.0.0.1"
141 + req.RemoteAddr = "172.0.0.4"
142 pool[1].setHealthy(false)
143 h = ipHash.Select(pool, req, nil)
144 if h != pool[2] {
@@ -147,16 +147,16 @@ func TestIPHashPolicy(t *testing.T) {
147
148 req.RemoteAddr = "172.0.0.2"
149 h = ipHash.Select(pool, req, nil)
150 - if h != pool[2] {
151 - t.Error("Expected ip hash policy host to be the third host.")
150 + if h != pool[0] {
151 + t.Error("Expected ip hash policy host to be the first host.")
152 }
153 pool[1].setHealthy(true)
154
155 req.RemoteAddr = "172.0.0.3"
156 pool[2].setHealthy(false)
157 h = ipHash.Select(pool, req, nil)
158 - if h != pool[0] {
159 - t.Error("Expected ip hash policy host to be the first host.")
158 + if h != pool[1] {
159 + t.Error("Expected ip hash policy host to be the second host.")
160 }
161 req.RemoteAddr = "172.0.0.4"
162 h = ipHash.Select(pool, req, nil)
@@ -167,29 +167,29 @@ func TestIPHashPolicy(t *testing.T) {
167 // We should be able to resize the host pool and still be able to predict
168 // where a req will be routed with the same IP's used above
169 pool = UpstreamPool{
170 - {Host: new(Host)},
171 - {Host: new(Host)},
170 + {Host: new(Host), Dial: "0.0.0.2"},
171 + {Host: new(Host), Dial: "0.0.0.3"},
172 }
173 req.RemoteAddr = "172.0.0.1:80"
174 h = ipHash.Select(pool, req, nil)
175 - if h != pool[0] {
176 - t.Error("Expected ip hash policy host to be the first host.")
177 - }
178 - req.RemoteAddr = "172.0.0.2:80"
179 - h = ipHash.Select(pool, req, nil)
175 if h != pool[1] {
176 t.Error("Expected ip hash policy host to be the second host.")
177 }
183 - req.RemoteAddr = "172.0.0.3:80"
178 + req.RemoteAddr = "172.0.0.2:80"
179 h = ipHash.Select(pool, req, nil)
180 if h != pool[0] {
181 t.Error("Expected ip hash policy host to be the first host.")
182 }
188 - req.RemoteAddr = "172.0.0.4:80"
183 + req.RemoteAddr = "172.0.0.3:80"
184 h = ipHash.Select(pool, req, nil)
185 if h != pool[1] {
186 t.Error("Expected ip hash policy host to be the second host.")
187 }
188 + req.RemoteAddr = "172.0.0.4:80"
189 + h = ipHash.Select(pool, req, nil)
190 + if h != pool[0] {
191 + t.Error("Expected ip hash policy host to be the first host.")
192 + }
193
194 // We should get nil when there are no healthy hosts
195 pool[0].setHealthy(false)
@@ -252,14 +252,14 @@ func TestURIHashPolicy(t *testing.T) {
252
253 request := httptest.NewRequest(http.MethodGet, "/test", nil)
254 h := uriPolicy.Select(pool, request, nil)
255 - if h != pool[0] {
256 - t.Error("Expected uri policy host to be the first host.")
255 + if h != pool[2] {
256 + t.Error("Expected uri policy host to be the third host.")
257 }
258
259 - pool[0].setHealthy(false)
259 + pool[2].setHealthy(false)
260 h = uriPolicy.Select(pool, request, nil)
261 if h != pool[1] {
262 - t.Error("Expected uri policy host to be the first host.")
262 + t.Error("Expected uri policy host to be the second host.")
263 }
264
265 request = httptest.NewRequest(http.MethodGet, "/test_2", nil)