diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index df6db2ff0..7044d13d0 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -178,6 +178,7 @@ - "traefik.http.services.service02.loadbalancer.sticky.cookie.secure=true" - "traefik.http.services.service02.loadbalancer.server.port=foobar" - "traefik.http.services.service02.loadbalancer.server.scheme=foobar" +- "traefik.http.services.service02.loadbalancer.server.weight=42" - "traefik.tcp.middlewares.tcpmiddleware01.ipallowlist.sourcerange=foobar, foobar" - "traefik.tcp.middlewares.tcpmiddleware02.ipwhitelist.sourcerange=foobar, foobar" - "traefik.tcp.middlewares.tcpmiddleware03.inflightconn.amount=42" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 541affb9c..7d771c6a2 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -58,9 +58,11 @@ [[http.services.Service02.loadBalancer.servers]] url = "foobar" + weight = 42 [[http.services.Service02.loadBalancer.servers]] url = "foobar" + weight = 42 [http.services.Service02.loadBalancer.healthCheck] scheme = "foobar" mode = "foobar" diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 4e0019e42..24df9aada 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -65,7 +65,9 @@ http: maxAge: 42 servers: - url: foobar + weight: 42 - url: foobar + weight: 42 healthCheck: scheme: foobar mode: foobar diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index 6d80b13b8..cc0334567 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -240,7 +240,9 @@ THIS FILE MUST NOT BE EDITED BY HAND | `traefik/http/services/Service02/loadBalancer/passHostHeader` | `true` | | `traefik/http/services/Service02/loadBalancer/responseForwarding/flushInterval` | `42s` | | `traefik/http/services/Service02/loadBalancer/servers/0/url` | `foobar` | +| `traefik/http/services/Service02/loadBalancer/servers/0/weight` | `42` | | `traefik/http/services/Service02/loadBalancer/servers/1/url` | `foobar` | +| `traefik/http/services/Service02/loadBalancer/servers/1/weight` | `42` | | `traefik/http/services/Service02/loadBalancer/serversTransport` | `foobar` | | `traefik/http/services/Service02/loadBalancer/sticky/cookie/httpOnly` | `true` | | `traefik/http/services/Service02/loadBalancer/sticky/cookie/maxAge` | `42` | diff --git a/docs/content/routing/services/index.md b/docs/content/routing/services/index.md index 7658195e1..7ac5ce78c 100644 --- a/docs/content/routing/services/index.md +++ b/docs/content/routing/services/index.md @@ -143,6 +143,36 @@ The `url` option point to a specific instance. url = "http://private-ip-server-1/" ``` +The `weight` option allows for weighted load balancing on the servers. + +??? example "A Service with Two Servers with Weight -- Using the [File Provider](../../providers/file.md)" + + ```yaml tab="YAML" + ## Dynamic configuration + http: + services: + my-service: + loadBalancer: + servers: + - url: "http://private-ip-server-1/" + weight: 2 + - url: "http://private-ip-server-2/" + weight: 1 + + ``` + + ```toml tab="TOML" + ## Dynamic configuration + [http.services] + [http.services.my-service.loadBalancer] + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-1/" + weight = 2 + [[http.services.my-service.loadBalancer.servers]] + url = "http://private-ip-server-2/" + weight = 1 + ``` + #### Load-balancing For now, only round robin load balancing is supported: diff --git a/integration/docker_test.go b/integration/docker_test.go index 1a767ff6f..daa6fa06b 100644 --- a/integration/docker_test.go +++ b/integration/docker_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "io" "net/http" + "strings" "testing" "time" @@ -55,6 +56,56 @@ func (s *DockerSuite) TestSimpleConfiguration() { require.NoError(s.T(), err) } +func (s *DockerSuite) TestWRRServer() { + tempObjects := struct { + DockerHost string + DefaultRule string + }{ + DockerHost: s.getDockerHost(), + DefaultRule: "Host(`{{ normalize .Name }}.docker.localhost`)", + } + + file := s.adaptFile("fixtures/docker/simple.toml", tempObjects) + + s.composeUp() + + s.traefikCmd(withConfigFile(file)) + + whoami1IP := s.getComposeServiceIP("wrr-server") + whoami2IP := s.getComposeServiceIP("wrr-server2") + + // Expected a 404 as we did not configure anything + err := try.GetRequest("http://127.0.0.1:8000/", 500*time.Millisecond, try.StatusCodeIs(http.StatusNotFound)) + require.NoError(s.T(), err) + + err = try.GetRequest("http://127.0.0.1:8080/api/http/services", 1000*time.Millisecond, try.BodyContains("wrr-server")) + require.NoError(s.T(), err) + + repartition := map[string]int{} + for i := 0; i < 4; i++ { + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil) + req.Host = "my.wrr.host" + require.NoError(s.T(), err) + + response, err := http.DefaultClient.Do(req) + require.NoError(s.T(), err) + assert.Equal(s.T(), http.StatusOK, response.StatusCode) + + body, err := io.ReadAll(response.Body) + require.NoError(s.T(), err) + + if strings.Contains(string(body), whoami1IP) { + repartition[whoami1IP]++ + } + if strings.Contains(string(body), whoami2IP) { + repartition[whoami2IP]++ + } + } + + assert.Equal(s.T(), 3, repartition[whoami1IP]) + assert.Equal(s.T(), 1, repartition[whoami2IP]) +} + func (s *DockerSuite) TestDefaultDockerContainers() { tempObjects := struct { DockerHost string diff --git a/integration/fixtures/wrr_server.toml b/integration/fixtures/wrr_server.toml new file mode 100644 index 000000000..d1735986d --- /dev/null +++ b/integration/fixtures/wrr_server.toml @@ -0,0 +1,35 @@ +[global] + checkNewVersion = false + sendAnonymousUsage = false + +[api] + insecure = true + +[log] + level = "DEBUG" + noColor = true + +[entryPoints] + + [entryPoints.web] + address = ":8000" + +[providers.file] + filename = "{{ .SelfFilename }}" + +## dynamic configuration ## + +[http.routers] + [http.routers.router] + service = "service1" + rule = "Path(`/whoami`)" + +[http.services] + + [http.services.service1.loadBalancer] + [[http.services.service1.loadBalancer.servers]] + url = "{{ .Server1 }}" + weight = 3 + [[http.services.service1.loadBalancer.servers]] + url = "{{ .Server2 }}" + diff --git a/integration/resources/compose/docker.yml b/integration/resources/compose/docker.yml index e594ec87c..b16571a4b 100644 --- a/integration/resources/compose/docker.yml +++ b/integration/resources/compose/docker.yml @@ -36,3 +36,14 @@ services: labels: traefik.http.Routers.Super.Rule: Host(`my.super.host`) traefik.http.Services.powpow.LoadBalancer.server.Port: 2375 + + wrr-server: + image: traefik/whoami + labels: + traefik.http.Routers.wrr-server.Rule: Host(`my.wrr.host`) + traefik.http.Services.wrr-server.LoadBalancer.server.Weight: 4 + wrr-server2: + image: traefik/whoami + labels: + traefik.http.Routers.wrr-server.Rule: Host(`my.wrr.host`) + traefik.http.Services.wrr-server.LoadBalancer.server.Weight: 1 diff --git a/integration/simple_test.go b/integration/simple_test.go index 94b4ec45e..d2c3be077 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -809,6 +809,49 @@ func (s *SimpleSuite) TestUDPServiceConfigErrors() { require.NoError(s.T(), err) } +func (s *SimpleSuite) TestWRRServer() { + s.createComposeProject("base") + + s.composeUp() + defer s.composeDown() + + whoami1IP := s.getComposeServiceIP("whoami1") + whoami2IP := s.getComposeServiceIP("whoami2") + + file := s.adaptFile("fixtures/wrr_server.toml", struct { + Server1 string + Server2 string + }{Server1: "http://" + whoami1IP, Server2: "http://" + whoami2IP}) + + s.traefikCmd(withConfigFile(file)) + + err := try.GetRequest("http://127.0.0.1:8080/api/http/services", 1000*time.Millisecond, try.BodyContains("service1")) + require.NoError(s.T(), err) + + repartition := map[string]int{} + for i := 0; i < 4; i++ { + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil) + require.NoError(s.T(), err) + + response, err := http.DefaultClient.Do(req) + require.NoError(s.T(), err) + assert.Equal(s.T(), http.StatusOK, response.StatusCode) + + body, err := io.ReadAll(response.Body) + require.NoError(s.T(), err) + + if strings.Contains(string(body), whoami1IP) { + repartition[whoami1IP]++ + } + if strings.Contains(string(body), whoami2IP) { + repartition[whoami2IP]++ + } + } + + assert.Equal(s.T(), 3, repartition[whoami1IP]) + assert.Equal(s.T(), 1, repartition[whoami2IP]) +} + func (s *SimpleSuite) TestWRR() { s.createComposeProject("base") diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index 0783d5592..541f61b79 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -227,6 +227,7 @@ func (r *ResponseForwarding) SetDefaults() { // Server holds the server configuration. type Server struct { URL string `json:"url,omitempty" toml:"url,omitempty" yaml:"url,omitempty" label:"-"` + Weight *int `json:"weight,omitempty" toml:"weight,omitempty" yaml:"weight,omitempty" label:"weight"` Scheme string `json:"-" toml:"-" yaml:"-" file:"-"` Port string `json:"-" toml:"-" yaml:"-" file:"-"` } diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 3969d144d..85b513b2a 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -1128,6 +1128,11 @@ func (in *RouterTLSConfig) DeepCopy() *RouterTLSConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Server) DeepCopyInto(out *Server) { *out = *in + if in.Weight != nil { + in, out := &in.Weight, &out.Weight + *out = new(int) + **out = **in + } return } @@ -1180,7 +1185,9 @@ func (in *ServersLoadBalancer) DeepCopyInto(out *ServersLoadBalancer) { if in.Servers != nil { in, out := &in.Servers, &out.Servers *out = make([]Server, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.HealthCheck != nil { in, out := &in.HealthCheck, &out.HealthCheck diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 55fcb9fa2..2d71791a8 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -319,7 +319,7 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName proxy = tracingMiddle.NewService(ctx, serviceName, proxy) - lb.Add(proxyName, proxy, nil) + lb.Add(proxyName, proxy, server.Weight) // servers are considered UP by default. info.UpdateServerStatus(target.String(), runtime.StatusUp)