Repository: traefik/traefik (language: go)

Issue:
Weighted Round Robin for all providers
<!-- PLEASE FOLLOW THE ISSUE TEMPLATE TO HELP TRIAGE AND SUPPORT! -->

### Do you want to request a *feature* or report a *bug*?

<!--
DO NOT FILE ISSUES FOR GENERAL SUPPORT QUESTIONS.

The issue tracker is for reporting bugs and feature requests only.
For end-user related support questions, please refer to one of the following:

- the Traefik community forum: https://community.containo.us/

-->

Feature

### What did you expect to see?
Currently with Traefik 1.7 we used the weighted round robin with docker swarm providers. But with the 2.0 version the WRR is only possible with file providers.

We use the WRR for a mechanism of zero downtime deployments for statefull application : deploy a new version without disconnect the users on old version (with deploying multiple stack).

This feature is required for our migration to traefik 2.0. 

Exemple of our mechanism : 

1. We have a swarm stack with `myApp` at version _1_ named `myAppV1` with one service `back`.
This service have this labels: 
```
- "traefik.enable=true"
- "traefik.backend.loadbalancer.stickiness=true"
- "traefik.backend.loadbalancer.stickiness.cookieName=myapp-back-traefik-sticky"
- "traefik.app.port=8080"
- "traefik.app.backend=myapp-back"
- "traefik.app.frontend.rule=Host:myapp.example.com"
- "traefik.app.frontend.entryPoints=http,https"
- "traefik.app.weight=1"
```
2. We deploying a new version of this app in stack `myAppV2` with same label of first version for the service `back`
3. We set the `traefik.app.weight` to `0` on the service `myAppV1_back`

So with this the current session continue to access to the V1 and the new ones access to the V2.

<!--

HOW TO WRITE A GOOD ISSUE?

- Respect the issue template as much as possible.
- The title should be short and descriptive.
- Explain the conditions which led you to report this issue: the context.
- The context should lead to something, an idea or a problem that you’re facing.
- Remain clear and concise.
- Format your messages to help the reader focus on what matters and understand the structure of your message, use Markdown syntax https://help.github.com/articles/github-flavored-markdown

-->



Discussion / hints:
I propose to implement this request which is necessary for our migration to traefik 2.x. 

I would like to know if this feature will be adopted or not?
Hello,

We put the label `proposal` and not `enhancement`, because there are some major problems with this feature.

The goal of the proposal is to discuss about the feature.

Hello, 

@ldez what are those major problems you refer to?
Is there another method to do blue-green or zero downtime with docker or docker swarm and Traefik 2.x?
Hello,

> Is there another method to do blue-green or zero downtime with docker or docker swarm and Traefik 2.x?

you have to use the docker provider and the file provider to handle that.

Thanks, 

> you have to use the docker provider and the file provider to handle that

can you provide an full configuration example? I don't know how to do that.
https://docs.traefik.io/v2.1/routing/services/#weighted-round-robin-service

I would need to use WRR for a service in a docker swarm cluster with different underlying server capabilities.
@ldez Can you please elaborate on how to combine docker provider and file provider to do this ? Or point me to relevant resources ?

The current (swarm) deploy configuration of my service looks like that:
```
deploy:
    mode: replicated
    replicas: 3
    labels:
      traefik.docker.network: traefik
      traefik.enable: 'true'

      traefik.http.services.mysvc.loadbalancer.server.port: '8069'
      traefik.http.services.mysvc.loadbalancer.sticky: 'true'
      traefik.http.services.mysvc.loadbalancer.sticky.cookie.name: sticky-mysvc
      traefik.http.services.mysvc.loadbalancer.sticky.cookie.secure: 'true'
      traefik.http.routers.mysvc.rule: (Host(`mysvc.mydomain.net`))  
      traefik.http.routers.mysvc.entrypoints: https
      traefik.http.routers.mysvc.middlewares: https_oauth@file
      traefik.http.routers.mysvc.service: mysvc
      traefik.http.routers.mysvc.tls: 'true'
      traefik.http.routers.mysvc.tls.certresolver: letsencrypt

      traefik.http.services.mysvcgraphql.loadbalancer.server.port: '8069'
      traefik.http.services.mysvcgraphql.loadbalancer.sticky: 'true'
      traefik.http.services.mysvcgraphql.loadbalancer.sticky.cookie.name: sticky-mysvc
      traefik.http.services.mysvcgraphql.loadbalancer.sticky.cookie.secure: 'true'
      traefik.http.routers.mysvcgraphql.rule: (Host(`mysvc.mydomain.net`) && ( PathPrefix(`/graphql`) || PathPrefix(`/web/image`) ))  
      traefik.http.routers.mysvcgraphql.entrypoints: https
      traefik.http.routers.mysvcgraphql.middlewares: https_default@file
      traefik.http.routers.mysvcgraphql.service: mysvc
      traefik.http.routers.mysvcgraphql.tls: 'true'
      traefik.http.routers.mysvcgraphql.tls.certresolver: letsencrypt

      traefik.http.services.mysvcchat.loadbalancer.server.port: '8072'
      traefik.http.services.mysvcchat.loadbalancer.sticky: 'true'
      traefik.http.services.mysvcchat.loadbalancer.sticky.cookie.name: sticky-mysvc
      traefik.http.services.mysvcchat.loadbalancer.sticky.cookie.secure: 'true'
      traefik.http.routers.mysvcchat.rule: (Host(`mysvc.mydomain.net`) && PathPrefix(`/longpolling`))  
      traefik.http.routers.mysvcchat.service: mysvcchat
      traefik.http.routers.mysvcchat.entrypoints: https
      traefik.http.routers.mysvcchat.tls: 'true'
      traefik.http.routers.mysvcchat.tls.certresolver: letsencrypt
```
It is deployed on worker 1 to 3, worker 1 being 4 times more powerfull than the 2 others
Hey folks,

New to Traefik here so could be way off, but curious if there were updates around this. I'm looking to use Traefik with the Consul Catalog provider and being able to specify things like weighted servers/services is an important feature (compared to Fabio where it's trivially adding `weight=x` to your tags).

I had partial success with the file provider, but it's also easy to run into issues when a service no longer exists and the Traefik WRR service is no longer valid (ex: Canary gets promoted and there's only the live version remaining). It seems like keeping a file provider in sync with other services is error-prone and tricky to maintain, but that's currently the only option.

Ideally, being able to specify a weight per server instance would be great, but I understand if that's not how Traefik v2 is designed.

Would this be a reasonable proposal to have a WRR defined by tags rather than managing a file (would obviously need to change, but just the general idea):

```
traefik.wrr.enable=true
traefik.wrr.service=my-service-weighted
traefik.wrr.rule=PathPrefix(`/httpbin`)
traefik.wrr.middlewares=...
traefik.wrr.weight=10
```

Alternatively, I'm not sure if there could be a more elegant way to split traffic to either multiple servers or services through some new Router options, which intuitively makes a bit more sense to me at least.
Also interested in any workarounds (@ldez?) as similar to @evandam , we have a need for being able to add or scale down canary services dynamically in Docker Swarm.

If I'm reading things right, I suppose in the short-term we can apply a regular round robin without weighting between the canary and non-canary services, and just adjust number of services replicas to perform a crude weighting, but would be really nice to be able to fine-tune this in Traefik.
Just curious if any more thought has gone into this or if it's on any roadmap. Another thought I had for a workaround in the meantime would just be if the router/service for WRR wouldn't show an error if one of the upstreams does not exist or is unhealthy. Instead it would just route traffic to the other services available?

For example:

```yml
http:
  routers:
    httpbin-weighted:
      entryPoints: ["traefik"]
      service: httpbin-weighted
      rule: PathPrefix(`/`)
  services:
    httpbin-weighted:
      weighted:
        services:
          - name: httpbin-traefik@docker
            weight: 10
          - name: httpbin-canary-traefik@docker
            weight: 1
```

but if there is no canary running:

![Screen Shot 2021-08-23 at 11 42 09 AM](https://user-images.githubusercontent.com/3985383/130500555-5825fff8-27c9-4d2b-ba15-aeda0417e8f2.png)



Fix patch:
diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml
index df6db2ff0f..7044d13d0e 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 541affb9c4..7d771c6a20 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 4e0019e429..24df9aada5 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 6d80b13b80..cc03345672 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 7658195e18..7ac5ce78c9 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/fixtures/wrr_server.toml b/integration/fixtures/wrr_server.toml
new file mode 100644
index 0000000000..d1735986da
--- /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 e594ec87c5..b16571a4b7 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/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go
index 0783d55921..541f61b797 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 3969d144d3..85b513b2a3 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 55fcb9fa24..2d71791a83 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)


Test patch:
diff --git a/integration/docker_test.go b/integration/docker_test.go
index 1a767ff6f7..daa6fa06b9 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/simple_test.go b/integration/simple_test.go
index 94b4ec45e4..d2c3be0772 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")