Skip to content

Commit 74949fb

Browse files
reverseproxy: Use xxhash instead of fnv32 for LB (#6203)
* Added Faster Non-cryptographic Hash Function for Load Balancing * Ran golangci-lint * Updated hash version and hash return type
1 parent ddb1d2c commit 74949fb

File tree

2 files changed

+88
-87
lines changed

2 files changed

+88
-87
lines changed

modules/caddyhttp/reverseproxy/selectionpolicies.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,15 @@ import (
2020
"encoding/hex"
2121
"encoding/json"
2222
"fmt"
23-
"hash/fnv"
2423
weakrand "math/rand"
2524
"net"
2625
"net/http"
2726
"strconv"
2827
"strings"
2928
"sync/atomic"
3029

30+
"github.com/cespare/xxhash/v2"
31+
3132
"github.com/caddyserver/caddy/v2"
3233
"github.com/caddyserver/caddy/v2/caddyconfig"
3334
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
@@ -807,7 +808,7 @@ func hostByHashing(pool []*Upstream, s string) *Upstream {
807808
// see https://medium.com/i0exception/rendezvous-hashing-8c00e2fb58b0,
808809
// https://randorithms.com/2020/12/26/rendezvous-hashing.html,
809810
// and https://en.wikipedia.org/wiki/Rendezvous_hashing.
810-
var highestHash uint32
811+
var highestHash uint64
811812
var upstream *Upstream
812813
for _, up := range pool {
813814
if !up.Available() {
@@ -823,10 +824,10 @@ func hostByHashing(pool []*Upstream, s string) *Upstream {
823824
}
824825

825826
// hash calculates a fast hash based on s.
826-
func hash(s string) uint32 {
827-
h := fnv.New32a()
827+
func hash(s string) uint64 {
828+
h := xxhash.New()
828829
_, _ = h.Write([]byte(s))
829-
return h.Sum32()
830+
return h.Sum64()
830831
}
831832

832833
func loadFallbackPolicy(d *caddyfile.Dispenser) (json.RawMessage, error) {

modules/caddyhttp/reverseproxy/selectionpolicies_test.go

Lines changed: 82 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ func TestIPHashPolicy(t *testing.T) {
157157
// We should be able to predict where every request is routed.
158158
req.RemoteAddr = "172.0.0.1:80"
159159
h := ipHash.Select(pool, req, nil)
160-
if h != pool[1] {
161-
t.Error("Expected ip hash policy host to be the second host.")
160+
if h != pool[0] {
161+
t.Error("Expected ip hash policy host to be the first host.")
162162
}
163163
req.RemoteAddr = "172.0.0.2:80"
164164
h = ipHash.Select(pool, req, nil)
@@ -167,8 +167,8 @@ func TestIPHashPolicy(t *testing.T) {
167167
}
168168
req.RemoteAddr = "172.0.0.3:80"
169169
h = ipHash.Select(pool, req, nil)
170-
if h != pool[1] {
171-
t.Error("Expected ip hash policy host to be the second host.")
170+
if h != pool[0] {
171+
t.Error("Expected ip hash policy host to be the first host.")
172172
}
173173
req.RemoteAddr = "172.0.0.4:80"
174174
h = ipHash.Select(pool, req, nil)
@@ -179,8 +179,8 @@ func TestIPHashPolicy(t *testing.T) {
179179
// we should get the same results without a port
180180
req.RemoteAddr = "172.0.0.1"
181181
h = ipHash.Select(pool, req, nil)
182-
if h != pool[1] {
183-
t.Error("Expected ip hash policy host to be the second host.")
182+
if h != pool[0] {
183+
t.Error("Expected ip hash policy host to be the first host.")
184184
}
185185
req.RemoteAddr = "172.0.0.2"
186186
h = ipHash.Select(pool, req, nil)
@@ -189,8 +189,8 @@ func TestIPHashPolicy(t *testing.T) {
189189
}
190190
req.RemoteAddr = "172.0.0.3"
191191
h = ipHash.Select(pool, req, nil)
192-
if h != pool[1] {
193-
t.Error("Expected ip hash policy host to be the second host.")
192+
if h != pool[0] {
193+
t.Error("Expected ip hash policy host to be the first host.")
194194
}
195195
req.RemoteAddr = "172.0.0.4"
196196
h = ipHash.Select(pool, req, nil)
@@ -203,8 +203,8 @@ func TestIPHashPolicy(t *testing.T) {
203203
req.RemoteAddr = "172.0.0.4"
204204
pool[1].setHealthy(false)
205205
h = ipHash.Select(pool, req, nil)
206-
if h != pool[0] {
207-
t.Error("Expected ip hash policy host to be the first host.")
206+
if h != pool[2] {
207+
t.Error("Expected ip hash policy host to be the third host.")
208208
}
209209

210210
req.RemoteAddr = "172.0.0.2"
@@ -217,8 +217,8 @@ func TestIPHashPolicy(t *testing.T) {
217217
req.RemoteAddr = "172.0.0.3"
218218
pool[2].setHealthy(false)
219219
h = ipHash.Select(pool, req, nil)
220-
if h != pool[1] {
221-
t.Error("Expected ip hash policy host to be the second host.")
220+
if h != pool[0] {
221+
t.Error("Expected ip hash policy host to be the first host.")
222222
}
223223
req.RemoteAddr = "172.0.0.4"
224224
h = ipHash.Select(pool, req, nil)
@@ -300,8 +300,8 @@ func TestClientIPHashPolicy(t *testing.T) {
300300
// We should be able to predict where every request is routed.
301301
caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.1:80")
302302
h := ipHash.Select(pool, req, nil)
303-
if h != pool[1] {
304-
t.Error("Expected ip hash policy host to be the second host.")
303+
if h != pool[0] {
304+
t.Error("Expected ip hash policy host to be the first host.")
305305
}
306306
caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.2:80")
307307
h = ipHash.Select(pool, req, nil)
@@ -310,8 +310,8 @@ func TestClientIPHashPolicy(t *testing.T) {
310310
}
311311
caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.3:80")
312312
h = ipHash.Select(pool, req, nil)
313-
if h != pool[1] {
314-
t.Error("Expected ip hash policy host to be the second host.")
313+
if h != pool[0] {
314+
t.Error("Expected ip hash policy host to be the first host.")
315315
}
316316
caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.4:80")
317317
h = ipHash.Select(pool, req, nil)
@@ -322,8 +322,8 @@ func TestClientIPHashPolicy(t *testing.T) {
322322
// we should get the same results without a port
323323
caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.1")
324324
h = ipHash.Select(pool, req, nil)
325-
if h != pool[1] {
326-
t.Error("Expected ip hash policy host to be the second host.")
325+
if h != pool[0] {
326+
t.Error("Expected ip hash policy host to be the first host.")
327327
}
328328
caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.2")
329329
h = ipHash.Select(pool, req, nil)
@@ -332,8 +332,8 @@ func TestClientIPHashPolicy(t *testing.T) {
332332
}
333333
caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.3")
334334
h = ipHash.Select(pool, req, nil)
335-
if h != pool[1] {
336-
t.Error("Expected ip hash policy host to be the second host.")
335+
if h != pool[0] {
336+
t.Error("Expected ip hash policy host to be the first host.")
337337
}
338338
caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.4")
339339
h = ipHash.Select(pool, req, nil)
@@ -346,8 +346,8 @@ func TestClientIPHashPolicy(t *testing.T) {
346346
caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.4")
347347
pool[1].setHealthy(false)
348348
h = ipHash.Select(pool, req, nil)
349-
if h != pool[0] {
350-
t.Error("Expected ip hash policy host to be the first host.")
349+
if h != pool[2] {
350+
t.Error("Expected ip hash policy host to be the third host.")
351351
}
352352

353353
caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.2")
@@ -360,8 +360,8 @@ func TestClientIPHashPolicy(t *testing.T) {
360360
caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.3")
361361
pool[2].setHealthy(false)
362362
h = ipHash.Select(pool, req, nil)
363-
if h != pool[1] {
364-
t.Error("Expected ip hash policy host to be the second host.")
363+
if h != pool[0] {
364+
t.Error("Expected ip hash policy host to be the first host.")
365365
}
366366
caddyhttp.SetVar(req.Context(), caddyhttp.ClientIPVarKey, "172.0.0.4")
367367
h = ipHash.Select(pool, req, nil)
@@ -470,21 +470,21 @@ func TestQueryHashPolicy(t *testing.T) {
470470

471471
request = httptest.NewRequest(http.MethodGet, "/?foo=100000", nil)
472472
h = queryPolicy.Select(pool, request, nil)
473-
if h != pool[0] {
474-
t.Error("Expected query policy host to be the first host.")
473+
if h != pool[1] {
474+
t.Error("Expected query policy host to be the second host.")
475475
}
476476

477477
request = httptest.NewRequest(http.MethodGet, "/?foo=1", nil)
478478
pool[0].setHealthy(false)
479479
h = queryPolicy.Select(pool, request, nil)
480-
if h != pool[1] {
481-
t.Error("Expected query policy host to be the second host.")
480+
if h != pool[2] {
481+
t.Error("Expected query policy host to be the third host.")
482482
}
483483

484484
request = httptest.NewRequest(http.MethodGet, "/?foo=100000", nil)
485485
h = queryPolicy.Select(pool, request, nil)
486-
if h != pool[2] {
487-
t.Error("Expected query policy host to be the third host.")
486+
if h != pool[1] {
487+
t.Error("Expected query policy host to be the second host.")
488488
}
489489

490490
// We should be able to resize the host pool and still be able to predict
@@ -533,14 +533,14 @@ func TestURIHashPolicy(t *testing.T) {
533533

534534
request := httptest.NewRequest(http.MethodGet, "/test", nil)
535535
h := uriPolicy.Select(pool, request, nil)
536-
if h != pool[1] {
537-
t.Error("Expected uri policy host to be the second host.")
536+
if h != pool[2] {
537+
t.Error("Expected uri policy host to be the third host.")
538538
}
539539

540540
pool[2].setHealthy(false)
541541
h = uriPolicy.Select(pool, request, nil)
542-
if h != pool[1] {
543-
t.Error("Expected uri policy host to be the second host.")
542+
if h != pool[0] {
543+
t.Error("Expected uri policy host to be the first host.")
544544
}
545545

546546
request = httptest.NewRequest(http.MethodGet, "/test_2", nil)
@@ -691,54 +691,54 @@ func TestCookieHashPolicy(t *testing.T) {
691691
}
692692

693693
func TestCookieHashPolicyWithSecureRequest(t *testing.T) {
694-
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
695-
defer cancel()
696-
cookieHashPolicy := CookieHashSelection{}
697-
if err := cookieHashPolicy.Provision(ctx); err != nil {
698-
t.Errorf("Provision error: %v", err)
699-
t.FailNow()
700-
}
701-
702-
pool := testPool()
703-
pool[0].Dial = "localhost:8080"
704-
pool[1].Dial = "localhost:8081"
705-
pool[2].Dial = "localhost:8082"
706-
pool[0].setHealthy(true)
707-
pool[1].setHealthy(false)
708-
pool[2].setHealthy(false)
709-
710-
// Create a test server that serves HTTPS requests
711-
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
712-
h := cookieHashPolicy.Select(pool, r, w)
713-
if h != pool[0] {
714-
t.Error("Expected cookieHashPolicy host to be the first only available host.")
715-
}
716-
}))
717-
defer ts.Close()
718-
719-
// Make a new HTTPS request to the test server
720-
client := ts.Client()
721-
request, err := http.NewRequest(http.MethodGet, ts.URL+"/test", nil)
722-
if err != nil {
723-
t.Fatal(err)
724-
}
725-
response, err := client.Do(request)
726-
if err != nil {
727-
t.Fatal(err)
728-
}
729-
730-
// Check if the cookie set is Secure and has SameSiteNone mode
731-
cookies := response.Cookies()
732-
if len(cookies) == 0 {
733-
t.Fatal("Expected a cookie to be set")
734-
}
735-
cookie := cookies[0]
736-
if !cookie.Secure {
737-
t.Error("Expected cookie Secure attribute to be true when request is secure")
738-
}
739-
if cookie.SameSite != http.SameSiteNoneMode {
740-
t.Error("Expected cookie SameSite attribute to be None when request is secure")
741-
}
694+
ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
695+
defer cancel()
696+
cookieHashPolicy := CookieHashSelection{}
697+
if err := cookieHashPolicy.Provision(ctx); err != nil {
698+
t.Errorf("Provision error: %v", err)
699+
t.FailNow()
700+
}
701+
702+
pool := testPool()
703+
pool[0].Dial = "localhost:8080"
704+
pool[1].Dial = "localhost:8081"
705+
pool[2].Dial = "localhost:8082"
706+
pool[0].setHealthy(true)
707+
pool[1].setHealthy(false)
708+
pool[2].setHealthy(false)
709+
710+
// Create a test server that serves HTTPS requests
711+
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
712+
h := cookieHashPolicy.Select(pool, r, w)
713+
if h != pool[0] {
714+
t.Error("Expected cookieHashPolicy host to be the first only available host.")
715+
}
716+
}))
717+
defer ts.Close()
718+
719+
// Make a new HTTPS request to the test server
720+
client := ts.Client()
721+
request, err := http.NewRequest(http.MethodGet, ts.URL+"/test", nil)
722+
if err != nil {
723+
t.Fatal(err)
724+
}
725+
response, err := client.Do(request)
726+
if err != nil {
727+
t.Fatal(err)
728+
}
729+
730+
// Check if the cookie set is Secure and has SameSiteNone mode
731+
cookies := response.Cookies()
732+
if len(cookies) == 0 {
733+
t.Fatal("Expected a cookie to be set")
734+
}
735+
cookie := cookies[0]
736+
if !cookie.Secure {
737+
t.Error("Expected cookie Secure attribute to be true when request is secure")
738+
}
739+
if cookie.SameSite != http.SameSiteNoneMode {
740+
t.Error("Expected cookie SameSite attribute to be None when request is secure")
741+
}
742742
}
743743

744744
func TestCookieHashPolicyWithFirstFallback(t *testing.T) {

0 commit comments

Comments
 (0)