mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
40b193fb79
* 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
401 lines
11 KiB
Go
401 lines
11 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package reverseproxy
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func testPool() UpstreamPool {
|
|
return UpstreamPool{
|
|
{Host: new(Host), Dial: "0.0.0.1"},
|
|
{Host: new(Host), Dial: "0.0.0.2"},
|
|
{Host: new(Host), Dial: "0.0.0.3"},
|
|
}
|
|
}
|
|
|
|
func TestRoundRobinPolicy(t *testing.T) {
|
|
pool := testPool()
|
|
rrPolicy := new(RoundRobinSelection)
|
|
req, _ := http.NewRequest("GET", "/", nil)
|
|
|
|
h := rrPolicy.Select(pool, req, nil)
|
|
// First selected host is 1, because counter starts at 0
|
|
// and increments before host is selected
|
|
if h != pool[1] {
|
|
t.Error("Expected first round robin host to be second host in the pool.")
|
|
}
|
|
h = rrPolicy.Select(pool, req, nil)
|
|
if h != pool[2] {
|
|
t.Error("Expected second round robin host to be third host in the pool.")
|
|
}
|
|
h = rrPolicy.Select(pool, req, nil)
|
|
if h != pool[0] {
|
|
t.Error("Expected third round robin host to be first host in the pool.")
|
|
}
|
|
// mark host as down
|
|
pool[1].setHealthy(false)
|
|
h = rrPolicy.Select(pool, req, nil)
|
|
if h != pool[2] {
|
|
t.Error("Expected to skip down host.")
|
|
}
|
|
// mark host as up
|
|
pool[1].setHealthy(true)
|
|
|
|
h = rrPolicy.Select(pool, req, nil)
|
|
if h == pool[2] {
|
|
t.Error("Expected to balance evenly among healthy hosts")
|
|
}
|
|
// mark host as full
|
|
pool[1].countRequest(1)
|
|
pool[1].MaxRequests = 1
|
|
h = rrPolicy.Select(pool, req, nil)
|
|
if h != pool[2] {
|
|
t.Error("Expected to skip full host.")
|
|
}
|
|
}
|
|
|
|
func TestLeastConnPolicy(t *testing.T) {
|
|
pool := testPool()
|
|
lcPolicy := new(LeastConnSelection)
|
|
req, _ := http.NewRequest("GET", "/", nil)
|
|
|
|
pool[0].countRequest(10)
|
|
pool[1].countRequest(10)
|
|
h := lcPolicy.Select(pool, req, nil)
|
|
if h != pool[2] {
|
|
t.Error("Expected least connection host to be third host.")
|
|
}
|
|
pool[2].countRequest(100)
|
|
h = lcPolicy.Select(pool, req, nil)
|
|
if h != pool[0] && h != pool[1] {
|
|
t.Error("Expected least connection host to be first or second host.")
|
|
}
|
|
}
|
|
|
|
func TestIPHashPolicy(t *testing.T) {
|
|
pool := testPool()
|
|
ipHash := new(IPHashSelection)
|
|
req, _ := http.NewRequest("GET", "/", nil)
|
|
|
|
// We should be able to predict where every request is routed.
|
|
req.RemoteAddr = "172.0.0.1:80"
|
|
h := ipHash.Select(pool, req, nil)
|
|
if h != pool[0] {
|
|
t.Error("Expected ip hash policy host to be the first host.")
|
|
}
|
|
req.RemoteAddr = "172.0.0.2:80"
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[0] {
|
|
t.Error("Expected ip hash policy host to be the first host.")
|
|
}
|
|
req.RemoteAddr = "172.0.0.3:80"
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[2] {
|
|
t.Error("Expected ip hash policy host to be the third host.")
|
|
}
|
|
req.RemoteAddr = "172.0.0.4:80"
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[1] {
|
|
t.Error("Expected ip hash policy host to be the second host.")
|
|
}
|
|
|
|
// we should get the same results without a port
|
|
req.RemoteAddr = "172.0.0.1"
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[0] {
|
|
t.Error("Expected ip hash policy host to be the first host.")
|
|
}
|
|
req.RemoteAddr = "172.0.0.2"
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[0] {
|
|
t.Error("Expected ip hash policy host to be the first host.")
|
|
}
|
|
req.RemoteAddr = "172.0.0.3"
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[2] {
|
|
t.Error("Expected ip hash policy host to be the third host.")
|
|
}
|
|
req.RemoteAddr = "172.0.0.4"
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[1] {
|
|
t.Error("Expected ip hash policy host to be the second host.")
|
|
}
|
|
|
|
// we should get a healthy host if the original host is unhealthy and a
|
|
// healthy host is available
|
|
req.RemoteAddr = "172.0.0.4"
|
|
pool[1].setHealthy(false)
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[2] {
|
|
t.Error("Expected ip hash policy host to be the third host.")
|
|
}
|
|
|
|
req.RemoteAddr = "172.0.0.2"
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[0] {
|
|
t.Error("Expected ip hash policy host to be the first host.")
|
|
}
|
|
pool[1].setHealthy(true)
|
|
|
|
req.RemoteAddr = "172.0.0.3"
|
|
pool[2].setHealthy(false)
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[1] {
|
|
t.Error("Expected ip hash policy host to be the second host.")
|
|
}
|
|
req.RemoteAddr = "172.0.0.4"
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[1] {
|
|
t.Error("Expected ip hash policy host to be the second host.")
|
|
}
|
|
|
|
// We should be able to resize the host pool and still be able to predict
|
|
// where a req will be routed with the same IP's used above
|
|
pool = UpstreamPool{
|
|
{Host: new(Host), Dial: "0.0.0.2"},
|
|
{Host: new(Host), Dial: "0.0.0.3"},
|
|
}
|
|
req.RemoteAddr = "172.0.0.1:80"
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[1] {
|
|
t.Error("Expected ip hash policy host to be the second host.")
|
|
}
|
|
req.RemoteAddr = "172.0.0.2:80"
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[0] {
|
|
t.Error("Expected ip hash policy host to be the first host.")
|
|
}
|
|
req.RemoteAddr = "172.0.0.3:80"
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[1] {
|
|
t.Error("Expected ip hash policy host to be the second host.")
|
|
}
|
|
req.RemoteAddr = "172.0.0.4:80"
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != pool[0] {
|
|
t.Error("Expected ip hash policy host to be the first host.")
|
|
}
|
|
|
|
// We should get nil when there are no healthy hosts
|
|
pool[0].setHealthy(false)
|
|
pool[1].setHealthy(false)
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h != nil {
|
|
t.Error("Expected ip hash policy host to be nil.")
|
|
}
|
|
|
|
// Reproduce #4135
|
|
pool = UpstreamPool{
|
|
{Host: new(Host)},
|
|
{Host: new(Host)},
|
|
{Host: new(Host)},
|
|
{Host: new(Host)},
|
|
{Host: new(Host)},
|
|
{Host: new(Host)},
|
|
{Host: new(Host)},
|
|
{Host: new(Host)},
|
|
{Host: new(Host)},
|
|
}
|
|
pool[0].setHealthy(false)
|
|
pool[1].setHealthy(false)
|
|
pool[2].setHealthy(false)
|
|
pool[3].setHealthy(false)
|
|
pool[4].setHealthy(false)
|
|
pool[5].setHealthy(false)
|
|
pool[6].setHealthy(false)
|
|
pool[7].setHealthy(false)
|
|
pool[8].setHealthy(true)
|
|
|
|
// We should get a result back when there is one healthy host left.
|
|
h = ipHash.Select(pool, req, nil)
|
|
if h == nil {
|
|
// If it is nil, it means we missed a host even though one is available
|
|
t.Error("Expected ip hash policy host to not be nil, but it is nil.")
|
|
}
|
|
}
|
|
|
|
func TestFirstPolicy(t *testing.T) {
|
|
pool := testPool()
|
|
firstPolicy := new(FirstSelection)
|
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
|
|
h := firstPolicy.Select(pool, req, nil)
|
|
if h != pool[0] {
|
|
t.Error("Expected first policy host to be the first host.")
|
|
}
|
|
|
|
pool[0].setHealthy(false)
|
|
h = firstPolicy.Select(pool, req, nil)
|
|
if h != pool[1] {
|
|
t.Error("Expected first policy host to be the second host.")
|
|
}
|
|
}
|
|
|
|
func TestURIHashPolicy(t *testing.T) {
|
|
pool := testPool()
|
|
uriPolicy := new(URIHashSelection)
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
h := uriPolicy.Select(pool, request, nil)
|
|
if h != pool[2] {
|
|
t.Error("Expected uri policy host to be the third host.")
|
|
}
|
|
|
|
pool[2].setHealthy(false)
|
|
h = uriPolicy.Select(pool, request, nil)
|
|
if h != pool[1] {
|
|
t.Error("Expected uri policy host to be the second host.")
|
|
}
|
|
|
|
request = httptest.NewRequest(http.MethodGet, "/test_2", nil)
|
|
h = uriPolicy.Select(pool, request, nil)
|
|
if h != pool[1] {
|
|
t.Error("Expected uri policy host to be the second host.")
|
|
}
|
|
|
|
// We should be able to resize the host pool and still be able to predict
|
|
// where a request will be routed with the same URI's used above
|
|
pool = UpstreamPool{
|
|
{Host: new(Host)},
|
|
{Host: new(Host)},
|
|
}
|
|
|
|
request = httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
h = uriPolicy.Select(pool, request, nil)
|
|
if h != pool[0] {
|
|
t.Error("Expected uri policy host to be the first host.")
|
|
}
|
|
|
|
pool[0].setHealthy(false)
|
|
h = uriPolicy.Select(pool, request, nil)
|
|
if h != pool[1] {
|
|
t.Error("Expected uri policy host to be the first host.")
|
|
}
|
|
|
|
request = httptest.NewRequest(http.MethodGet, "/test_2", nil)
|
|
h = uriPolicy.Select(pool, request, nil)
|
|
if h != pool[1] {
|
|
t.Error("Expected uri policy host to be the second host.")
|
|
}
|
|
|
|
pool[0].setHealthy(false)
|
|
pool[1].setHealthy(false)
|
|
h = uriPolicy.Select(pool, request, nil)
|
|
if h != nil {
|
|
t.Error("Expected uri policy policy host to be nil.")
|
|
}
|
|
}
|
|
|
|
func TestLeastRequests(t *testing.T) {
|
|
pool := testPool()
|
|
pool[0].Dial = "localhost:8080"
|
|
pool[1].Dial = "localhost:8081"
|
|
pool[2].Dial = "localhost:8082"
|
|
pool[0].setHealthy(true)
|
|
pool[1].setHealthy(true)
|
|
pool[2].setHealthy(true)
|
|
pool[0].countRequest(10)
|
|
pool[1].countRequest(20)
|
|
pool[2].countRequest(30)
|
|
|
|
result := leastRequests(pool)
|
|
|
|
if result == nil {
|
|
t.Error("Least request should not return nil")
|
|
}
|
|
|
|
if result != pool[0] {
|
|
t.Error("Least request should return pool[0]")
|
|
}
|
|
}
|
|
|
|
func TestRandomChoicePolicy(t *testing.T) {
|
|
pool := testPool()
|
|
pool[0].Dial = "localhost:8080"
|
|
pool[1].Dial = "localhost:8081"
|
|
pool[2].Dial = "localhost:8082"
|
|
pool[0].setHealthy(false)
|
|
pool[1].setHealthy(true)
|
|
pool[2].setHealthy(true)
|
|
pool[0].countRequest(10)
|
|
pool[1].countRequest(20)
|
|
pool[2].countRequest(30)
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
randomChoicePolicy := new(RandomChoiceSelection)
|
|
randomChoicePolicy.Choose = 2
|
|
|
|
h := randomChoicePolicy.Select(pool, request, nil)
|
|
|
|
if h == nil {
|
|
t.Error("RandomChoicePolicy should not return nil")
|
|
}
|
|
|
|
if h == pool[0] {
|
|
t.Error("RandomChoicePolicy should not choose pool[0]")
|
|
}
|
|
|
|
}
|
|
|
|
func TestCookieHashPolicy(t *testing.T) {
|
|
pool := testPool()
|
|
pool[0].Dial = "localhost:8080"
|
|
pool[1].Dial = "localhost:8081"
|
|
pool[2].Dial = "localhost:8082"
|
|
pool[0].setHealthy(true)
|
|
pool[1].setHealthy(false)
|
|
pool[2].setHealthy(false)
|
|
request := httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
w := httptest.NewRecorder()
|
|
cookieHashPolicy := new(CookieHashSelection)
|
|
h := cookieHashPolicy.Select(pool, request, w)
|
|
cookieServer1 := w.Result().Cookies()[0]
|
|
if cookieServer1 == nil {
|
|
t.Fatal("cookieHashPolicy should set a cookie")
|
|
}
|
|
if cookieServer1.Name != "lb" {
|
|
t.Error("cookieHashPolicy should set a cookie with name lb")
|
|
}
|
|
if h != pool[0] {
|
|
t.Error("Expected cookieHashPolicy host to be the first only available host.")
|
|
}
|
|
pool[1].setHealthy(true)
|
|
pool[2].setHealthy(true)
|
|
request = httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
w = httptest.NewRecorder()
|
|
request.AddCookie(cookieServer1)
|
|
h = cookieHashPolicy.Select(pool, request, w)
|
|
if h != pool[0] {
|
|
t.Error("Expected cookieHashPolicy host to stick to the first host (matching cookie).")
|
|
}
|
|
s := w.Result().Cookies()
|
|
if len(s) != 0 {
|
|
t.Error("Expected cookieHashPolicy to not set a new cookie.")
|
|
}
|
|
pool[0].setHealthy(false)
|
|
request = httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
w = httptest.NewRecorder()
|
|
request.AddCookie(cookieServer1)
|
|
h = cookieHashPolicy.Select(pool, request, w)
|
|
if h == pool[0] {
|
|
t.Error("Expected cookieHashPolicy to select a new host.")
|
|
}
|
|
if w.Result().Cookies() == nil {
|
|
t.Error("Expected cookieHashPolicy to set a new cookie.")
|
|
}
|
|
}
|