mirror of
https://github.com/traefik/traefik.git
synced 2025-10-27 14:31:14 +01:00
Add least time load balancing strategy
This commit is contained in:
parent
067c7e7152
commit
a754236ce5
@ -515,3 +515,13 @@ For the experimental channel:
|
||||
```shell
|
||||
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/experimental-install.yaml
|
||||
```
|
||||
|
||||
### Kubernetes CRD Provider
|
||||
|
||||
To use the new `leastime` load-balancer algorithm with the Kubernetes CRD provider, you need to update your CRDs.
|
||||
|
||||
**Apply Updated CRDs:**
|
||||
|
||||
```shell
|
||||
kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.6/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
|
||||
```
|
||||
|
||||
@ -351,12 +351,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
@ -1313,12 +1314,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
@ -3031,12 +3033,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
@ -3359,12 +3362,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
@ -3506,12 +3510,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
@ -3740,12 +3745,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
|
||||
@ -351,12 +351,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
|
||||
@ -484,12 +484,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
|
||||
@ -262,12 +262,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
@ -590,12 +591,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
@ -737,12 +739,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
@ -971,12 +974,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
|
||||
@ -485,6 +485,48 @@ Power of two choices algorithm is a load balancing strategy that selects two ser
|
||||
url = "http://private-ip-server-3/"
|
||||
```
|
||||
|
||||
## Least-Time
|
||||
|
||||
The Least-Time load balancing algorithm selects the server with the lowest average response time (Time To First Byte - TTFB),
|
||||
combined with the fewest active connections, weighted by server capacity.
|
||||
This strategy is ideal for heterogeneous backend environments where servers have varying performance characteristics,
|
||||
different hardware capabilities, or varying network latency.
|
||||
|
||||
The algorithm continuously measures each backend's response time and tracks active connection counts.
|
||||
When routing a request,
|
||||
it calculates a score for each healthy server using the formula: `(avg_response_time × (1 + active_connections)) / weight`.
|
||||
The server with the lowest score receives the request.
|
||||
When multiple servers have identical scores,
|
||||
Weighted Round Robin (WRR) with Earliest Deadline First (EDF) scheduling is used as a tie-breaker to ensure fair distribution.
|
||||
|
||||
??? example "Basic Least-Time Load Balancing -- Using the [File Provider](../../../install-configuration/providers/others/file.md)"
|
||||
|
||||
```yaml tab="YAML"
|
||||
## Dynamic configuration
|
||||
http:
|
||||
services:
|
||||
my-service:
|
||||
loadBalancer:
|
||||
strategy: "leasttime"
|
||||
servers:
|
||||
- url: "http://private-ip-server-1/"
|
||||
- url: "http://private-ip-server-2/"
|
||||
- url: "http://private-ip-server-3/"
|
||||
```
|
||||
|
||||
```toml tab="TOML"
|
||||
## Dynamic configuration
|
||||
[http.services]
|
||||
[http.services.my-service.loadBalancer]
|
||||
strategy = "leasttime"
|
||||
[[http.services.my-service.loadBalancer.servers]]
|
||||
url = "http://private-ip-server-1/"
|
||||
[[http.services.my-service.loadBalancer.servers]]
|
||||
url = "http://private-ip-server-2/"
|
||||
[[http.services.my-service.loadBalancer.servers]]
|
||||
url = "http://private-ip-server-3/"
|
||||
```
|
||||
|
||||
## Mirroring
|
||||
|
||||
The mirroring is able to mirror requests sent to a service to other services. Please note that by default the whole request is buffered in memory while it is being mirrored. See the `maxBodySize` option in the example below for how to modify this behaviour. You can also omit the request body by setting the `mirrorBody` option to false.
|
||||
|
||||
@ -57,7 +57,7 @@ spec:
|
||||
httpOnly: true
|
||||
name: cookie
|
||||
secure: true
|
||||
strategy: RoundRobin
|
||||
strategy: wrr
|
||||
weight: 10
|
||||
tls:
|
||||
# Generate a TLS certificate using a certificate resolver
|
||||
|
||||
@ -47,7 +47,7 @@ spec:
|
||||
httpOnly: true
|
||||
name: cookie
|
||||
secure: true
|
||||
strategy: RoundRobin
|
||||
strategy: wrr
|
||||
```
|
||||
|
||||
```yaml tab="TraefikService"
|
||||
@ -75,13 +75,13 @@ spec:
|
||||
httpOnly: true
|
||||
name: cookie
|
||||
secure: true
|
||||
strategy: RoundRobin
|
||||
strategy: wrr
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
| Field | Description | Default | Required |
|
||||
|:---------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------|
|
||||
|:---------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------|
|
||||
| <a id="opt-kind" href="#opt-kind" title="#opt-kind">`kind`</a> | Kind of the service targeted.<br />Two values allowed:<br />- **Service**: Kubernetes Service<br /> **TraefikService**: Traefik Service.<br />More information [here](#externalname-service). | "Service" | No |
|
||||
| <a id="opt-name" href="#opt-name" title="#opt-name">`name`</a> | Service name.<br />The character `@` is not authorized. <br />More information [here](#middleware). | | Yes |
|
||||
| <a id="opt-namespace" href="#opt-namespace" title="#opt-namespace">`namespace`</a> | Service namespace.<br />Can be empty if the service belongs to the same namespace as the IngressRoute. <br />More information [here](#externalname-service). | | No |
|
||||
@ -107,7 +107,7 @@ spec:
|
||||
| <a id="opt-sticky-cookie-secure" href="#opt-sticky-cookie-secure" title="#opt-sticky-cookie-secure">`sticky.`<br />`cookie.secure`</a> | Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).<br />Evaluated only if the kind is **Service**. | false | No |
|
||||
| <a id="opt-sticky-cookie-sameSite" href="#opt-sticky-cookie-sameSite" title="#opt-sticky-cookie-sameSite">`sticky.`<br />`cookie.sameSite`</a> | [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy<br />Allowed values:<br />-`none`<br />-`lax`<br />`strict`<br />Evaluated only if the kind is **Service**. | "" | No |
|
||||
| <a id="opt-sticky-cookie-maxAge" href="#opt-sticky-cookie-maxAge" title="#opt-sticky-cookie-maxAge">`sticky.`<br />`cookie.maxAge`</a> | Number of seconds until the cookie expires.<br />Negative number, the cookie expires immediately.<br />0, the cookie never expires.<br />Evaluated only if the kind is **Service**. | 0 | No |
|
||||
| <a id="opt-strategy" href="#opt-strategy" title="#opt-strategy">`strategy`</a> | Load balancing strategy between the servers.<br />RoundRobin is the only supported value yet.<br />Evaluated only if the kind is **Service**. | "RoundRobin" | No |
|
||||
| <a id="opt-strategy" href="#opt-strategy" title="#opt-strategy">`strategy`</a> | Strategy defines the load balancing strategy between the servers.<br />Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).<br />Evaluated only if the kind is **Service**. | "RoundRobin" | No |
|
||||
| <a id="opt-nativeLB" href="#opt-nativeLB" title="#opt-nativeLB">`nativeLB`</a> | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.<br /> Evaluated only if the kind is **Service**. | false | No |
|
||||
| <a id="opt-nodePortLB" href="#opt-nodePortLB" title="#opt-nodePortLB">`nodePortLB`</a> | Use the nodePort IP address when the service type is NodePort.<br />It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.<br />Evaluated only if the kind is **Service**. | false | No |
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -56,56 +55,6 @@ 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 range 4 {
|
||||
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
|
||||
|
||||
@ -351,12 +351,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
@ -1313,12 +1314,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
@ -3031,12 +3033,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
@ -3359,12 +3362,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
@ -3506,12 +3510,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
@ -3740,12 +3745,13 @@ spec:
|
||||
strategy:
|
||||
description: |-
|
||||
Strategy defines the load balancing strategy between the servers.
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
RoundRobin value is deprecated and supported for backward compatibility.
|
||||
enum:
|
||||
- wrr
|
||||
- p2c
|
||||
- hrw
|
||||
- leasttime
|
||||
- RoundRobin
|
||||
type: string
|
||||
weight:
|
||||
|
||||
36
integration/fixtures/leasttime_server.toml
Normal file
36
integration/fixtures/leasttime_server.toml
Normal file
@ -0,0 +1,36 @@
|
||||
[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]
|
||||
strategy = "leasttime"
|
||||
[[http.services.service1.loadBalancer.servers]]
|
||||
url = "{{ .Server1 }}"
|
||||
weight = 1
|
||||
[[http.services.service1.loadBalancer.servers]]
|
||||
url = "{{ .Server2 }}"
|
||||
weight = 1
|
||||
@ -35,14 +35,3 @@ 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
|
||||
|
||||
@ -885,6 +885,109 @@ func (s *SimpleSuite) TestWRRServer() {
|
||||
assert.Equal(s.T(), 1, repartition[whoami2IP])
|
||||
}
|
||||
|
||||
func (s *SimpleSuite) TestLeastTimeServer() {
|
||||
s.createComposeProject("base")
|
||||
|
||||
s.composeUp()
|
||||
defer s.composeDown()
|
||||
|
||||
whoami1IP := s.getComposeServiceIP("whoami1")
|
||||
whoami2IP := s.getComposeServiceIP("whoami2")
|
||||
|
||||
file := s.adaptFile("fixtures/leasttime_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)
|
||||
|
||||
// Verify leasttime strategy is configured
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/http/services", 1000*time.Millisecond, try.BodyContains("leasttime"))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
// Make requests and verify both servers respond
|
||||
repartition := map[string]int{}
|
||||
for range 10 {
|
||||
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]++
|
||||
}
|
||||
}
|
||||
|
||||
// Both servers should have received requests
|
||||
assert.Positive(s.T(), repartition[whoami1IP])
|
||||
assert.Positive(s.T(), repartition[whoami2IP])
|
||||
}
|
||||
|
||||
func (s *SimpleSuite) TestLeastTimeHeterogeneousPerformance() {
|
||||
// Create test servers with different response times
|
||||
var fastServerCalls, slowServerCalls atomic.Int32
|
||||
|
||||
fastServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
fastServerCalls.Add(1)
|
||||
time.Sleep(10 * time.Millisecond) // Fast server
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, _ = rw.Write([]byte("fast-server"))
|
||||
}))
|
||||
defer fastServer.Close()
|
||||
|
||||
slowServer := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
slowServerCalls.Add(1)
|
||||
time.Sleep(100 * time.Millisecond) // Slow server
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
_, _ = rw.Write([]byte("slow-server"))
|
||||
}))
|
||||
defer slowServer.Close()
|
||||
|
||||
file := s.adaptFile("fixtures/leasttime_server.toml", struct {
|
||||
Server1 string
|
||||
Server2 string
|
||||
}{Server1: fastServer.URL, Server2: slowServer.URL})
|
||||
|
||||
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)
|
||||
|
||||
// Make 20 requests to build up response time statistics
|
||||
for range 20 {
|
||||
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)
|
||||
_, _ = io.ReadAll(response.Body)
|
||||
response.Body.Close()
|
||||
}
|
||||
|
||||
// Verify that the fast server received significantly more requests (>70%)
|
||||
fastCalls := fastServerCalls.Load()
|
||||
slowCalls := slowServerCalls.Load()
|
||||
totalCalls := fastCalls + slowCalls
|
||||
|
||||
assert.Equal(s.T(), int32(20), totalCalls)
|
||||
|
||||
// Fast server should get >70% of traffic due to lower response time
|
||||
fastPercentage := float64(fastCalls) / float64(totalCalls) * 100
|
||||
assert.Greater(s.T(), fastPercentage, 70.0)
|
||||
}
|
||||
|
||||
func (s *SimpleSuite) TestWRR() {
|
||||
s.createComposeProject("base")
|
||||
|
||||
|
||||
@ -190,6 +190,12 @@ type WRRService struct {
|
||||
GRPCStatus *GRPCStatus `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
|
||||
}
|
||||
|
||||
// SetDefaults Default values for a WRRService.
|
||||
func (w *WRRService) SetDefaults() {
|
||||
defaultWeight := 1
|
||||
w.Weight = &defaultWeight
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// HRWService is a reference to a service load-balanced with highest random weight.
|
||||
@ -204,12 +210,6 @@ func (w *HRWService) SetDefaults() {
|
||||
w.Weight = &defaultWeight
|
||||
}
|
||||
|
||||
// SetDefaults Default values for a WRRService.
|
||||
func (w *WRRService) SetDefaults() {
|
||||
defaultWeight := 1
|
||||
w.Weight = &defaultWeight
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
type GRPCStatus struct {
|
||||
@ -267,6 +267,8 @@ const (
|
||||
BalancerStrategyP2C BalancerStrategy = "p2c"
|
||||
// BalancerStrategyHRW is the highest random weight strategy.
|
||||
BalancerStrategyHRW BalancerStrategy = "hrw"
|
||||
// BalancerStrategyLeastTime is the least-time strategy.
|
||||
BalancerStrategyLeastTime BalancerStrategy = "leasttime"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: test.route
|
||||
namespace: default
|
||||
spec:
|
||||
entryPoints:
|
||||
- web
|
||||
routes:
|
||||
- match: Host(`foo.com`) && PathPrefix(`/leasttime`)
|
||||
kind: Rule
|
||||
priority: 12
|
||||
services:
|
||||
- name: whoami2
|
||||
port: 8080
|
||||
strategy: leasttime
|
||||
@ -384,7 +384,7 @@ func (c configBuilder) buildServersLB(namespace string, svc traefikv1alpha1.Load
|
||||
// TODO: remove this when the fake client apply default values.
|
||||
if svc.Strategy != "" {
|
||||
switch svc.Strategy {
|
||||
case dynamic.BalancerStrategyWRR, dynamic.BalancerStrategyP2C, dynamic.BalancerStrategyHRW:
|
||||
case dynamic.BalancerStrategyWRR, dynamic.BalancerStrategyP2C, dynamic.BalancerStrategyHRW, dynamic.BalancerStrategyLeastTime:
|
||||
lb.Strategy = svc.Strategy
|
||||
|
||||
// Here we are just logging a warning as the default value is already applied.
|
||||
|
||||
@ -5667,6 +5667,54 @@ func TestLoadIngressRoutes(t *testing.T) {
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Simple Ingress Route with leasttime strategy",
|
||||
paths: []string{"services.yml", "with_leasttime_strategy.yml"},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
Services: map[string]*dynamic.UDPService{},
|
||||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
ServersTransports: map[string]*dynamic.TCPServersTransport{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"default-test-route-55869f6407935ccfa805": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "default-test-route-55869f6407935ccfa805",
|
||||
Rule: "Host(`foo.com`) && PathPrefix(`/leasttime`)",
|
||||
Priority: 12,
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-test-route-55869f6407935ccfa805": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Strategy: dynamic.BalancerStrategyLeastTime,
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://10.10.0.3:8080",
|
||||
},
|
||||
{
|
||||
URL: "http://10.10.0.4:8080",
|
||||
},
|
||||
},
|
||||
PassHostHeader: pointer(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
|
||||
@ -118,10 +118,10 @@ type LoadBalancerSpec struct {
|
||||
// It defaults to https when Kubernetes Service port is 443, http otherwise.
|
||||
Scheme string `json:"scheme,omitempty"`
|
||||
// Strategy defines the load balancing strategy between the servers.
|
||||
// Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight).
|
||||
// Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), hrw (Highest Random Weight), and leasttime (Least-Time).
|
||||
// RoundRobin value is deprecated and supported for backward compatibility.
|
||||
// TODO: when the deprecated RoundRobin value will be removed, set the default value to wrr.
|
||||
// +kubebuilder:validation:Enum=wrr;p2c;hrw;RoundRobin
|
||||
// TODO: when the deprecated RoundRobin value will be removed, set the default kubebuilder value to wrr.
|
||||
// +kubebuilder:validation:Enum=wrr;p2c;hrw;leasttime;RoundRobin
|
||||
Strategy dynamic.BalancerStrategy `json:"strategy,omitempty"`
|
||||
// PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service.
|
||||
// By default, passHostHeader is true.
|
||||
|
||||
@ -13,6 +13,8 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/ip"
|
||||
)
|
||||
|
||||
var errNoAvailableServer = errors.New("no available server")
|
||||
|
||||
type namedHandler struct {
|
||||
http.Handler
|
||||
name string
|
||||
@ -114,15 +116,13 @@ func (b *Balancer) SetStatus(ctx context.Context, childName string, up bool) {
|
||||
// Not thread safe.
|
||||
func (b *Balancer) RegisterStatusUpdater(fn func(up bool)) error {
|
||||
if !b.wantsHealthCheck {
|
||||
return errors.New("healthCheck not enabled in config for this weighted service")
|
||||
return errors.New("healthCheck not enabled in config for this HRW service")
|
||||
}
|
||||
b.updaters = append(b.updaters, fn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var errNoAvailableServer = errors.New("no available server")
|
||||
|
||||
func (b *Balancer) nextServer(ip string) (*namedHandler, error) {
|
||||
b.handlersMu.RLock()
|
||||
var healthy []*namedHandler
|
||||
|
||||
373
pkg/server/service/loadbalancer/leasttime/leasttime.go
Normal file
373
pkg/server/service/loadbalancer/leasttime/leasttime.go
Normal file
@ -0,0 +1,373 @@
|
||||
package leasttime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
"github.com/traefik/traefik/v3/pkg/server/service/loadbalancer"
|
||||
)
|
||||
|
||||
const sampleSize = 100 // Number of response time samples to track.
|
||||
|
||||
var errNoAvailableServer = errors.New("no available server")
|
||||
|
||||
// namedHandler wraps an HTTP handler with metrics and server information.
|
||||
// Tracks response time (TTFB) and inflight request count for load balancing decisions.
|
||||
type namedHandler struct {
|
||||
http.Handler
|
||||
name string
|
||||
weight float64
|
||||
|
||||
deadlineMu sync.RWMutex
|
||||
deadline float64 // WRR tie-breaking (EDF scheduling).
|
||||
|
||||
inflightCount atomic.Int64 // Number of inflight requests.
|
||||
|
||||
responseTimeMu sync.RWMutex
|
||||
responseTimes [sampleSize]float64 // Fixed-size ring buffer (TTFB measurements in ms).
|
||||
responseTimeIdx int // Current position in ring buffer.
|
||||
responseTimeSum float64 // Sum of all values in buffer.
|
||||
sampleCount int // Number of samples collected so far.
|
||||
}
|
||||
|
||||
// updateResponseTime updates the average response time for this server using a ring buffer.
|
||||
func (s *namedHandler) updateResponseTime(elapsed time.Duration) {
|
||||
s.responseTimeMu.Lock()
|
||||
defer s.responseTimeMu.Unlock()
|
||||
|
||||
ms := float64(elapsed.Milliseconds())
|
||||
|
||||
if s.sampleCount < sampleSize {
|
||||
// Still filling the buffer.
|
||||
s.responseTimes[s.responseTimeIdx] = ms
|
||||
s.responseTimeSum += ms
|
||||
s.sampleCount++
|
||||
} else {
|
||||
// Buffer is full, replace oldest value.
|
||||
oldValue := s.responseTimes[s.responseTimeIdx]
|
||||
s.responseTimes[s.responseTimeIdx] = ms
|
||||
s.responseTimeSum = s.responseTimeSum - oldValue + ms
|
||||
}
|
||||
|
||||
s.responseTimeIdx = (s.responseTimeIdx + 1) % sampleSize
|
||||
}
|
||||
|
||||
// getAvgResponseTime returns the average response time in milliseconds.
|
||||
// Returns 0 if no samples have been collected yet (cold start).
|
||||
func (s *namedHandler) getAvgResponseTime() float64 {
|
||||
s.responseTimeMu.RLock()
|
||||
defer s.responseTimeMu.RUnlock()
|
||||
|
||||
if s.sampleCount == 0 {
|
||||
return 0
|
||||
}
|
||||
return s.responseTimeSum / float64(s.sampleCount)
|
||||
}
|
||||
|
||||
func (s *namedHandler) getDeadline() float64 {
|
||||
s.deadlineMu.RLock()
|
||||
defer s.deadlineMu.RUnlock()
|
||||
return s.deadline
|
||||
}
|
||||
|
||||
func (s *namedHandler) setDeadline(deadline float64) {
|
||||
s.deadlineMu.Lock()
|
||||
defer s.deadlineMu.Unlock()
|
||||
s.deadline = deadline
|
||||
}
|
||||
|
||||
// Balancer implements the least-time load balancing algorithm.
|
||||
// It selects the server with the lowest average response time (TTFB) and fewest active connections.
|
||||
type Balancer struct {
|
||||
wantsHealthCheck bool
|
||||
|
||||
// handlersMu protects the handlers slice, the status and the fenced maps.
|
||||
handlersMu sync.RWMutex
|
||||
handlers []*namedHandler
|
||||
// status is a record of which child services of the Balancer are healthy, keyed
|
||||
// by name of child service. A service is initially added to the map when it is
|
||||
// created via Add, and it is later removed or added to the map as needed,
|
||||
// through the SetStatus method.
|
||||
status map[string]struct{}
|
||||
// fenced is the list of terminating yet still serving child services.
|
||||
fenced map[string]struct{}
|
||||
|
||||
// updaters is the list of hooks that are run (to update the Balancer
|
||||
// parent(s)), whenever the Balancer status changes.
|
||||
// No mutex is needed, as it is modified only during the configuration build.
|
||||
updaters []func(bool)
|
||||
|
||||
sticky *loadbalancer.Sticky
|
||||
|
||||
// deadlineMu protects EDF scheduling state (curDeadline and all handler deadline fields).
|
||||
// Separate from handlersMu to reduce lock contention during tie-breaking.
|
||||
curDeadlineMu sync.RWMutex
|
||||
// curDeadline is used for WRR tie-breaking (EDF scheduling).
|
||||
curDeadline float64
|
||||
}
|
||||
|
||||
// New creates a new least-time load balancer.
|
||||
func New(stickyConfig *dynamic.Sticky, wantsHealthCheck bool) *Balancer {
|
||||
balancer := &Balancer{
|
||||
status: make(map[string]struct{}),
|
||||
fenced: make(map[string]struct{}),
|
||||
wantsHealthCheck: wantsHealthCheck,
|
||||
}
|
||||
if stickyConfig != nil && stickyConfig.Cookie != nil {
|
||||
balancer.sticky = loadbalancer.NewSticky(*stickyConfig.Cookie)
|
||||
}
|
||||
|
||||
return balancer
|
||||
}
|
||||
|
||||
// SetStatus sets on the balancer that its given child is now of the given
|
||||
// status. childName is only needed for logging purposes.
|
||||
func (b *Balancer) SetStatus(ctx context.Context, childName string, up bool) {
|
||||
b.handlersMu.Lock()
|
||||
defer b.handlersMu.Unlock()
|
||||
|
||||
upBefore := len(b.status) > 0
|
||||
|
||||
status := "DOWN"
|
||||
if up {
|
||||
status = "UP"
|
||||
}
|
||||
|
||||
log.Ctx(ctx).Debug().Msgf("Setting status of %s to %v", childName, status)
|
||||
|
||||
if up {
|
||||
b.status[childName] = struct{}{}
|
||||
} else {
|
||||
delete(b.status, childName)
|
||||
}
|
||||
|
||||
upAfter := len(b.status) > 0
|
||||
status = "DOWN"
|
||||
if upAfter {
|
||||
status = "UP"
|
||||
}
|
||||
|
||||
// No Status Change.
|
||||
if upBefore == upAfter {
|
||||
// We're still with the same status, no need to propagate.
|
||||
log.Ctx(ctx).Debug().Msgf("Still %s, no need to propagate", status)
|
||||
return
|
||||
}
|
||||
|
||||
// Status Change.
|
||||
log.Ctx(ctx).Debug().Msgf("Propagating new %s status", status)
|
||||
for _, fn := range b.updaters {
|
||||
fn(upAfter)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterStatusUpdater adds fn to the list of hooks that are run when the
|
||||
// status of the Balancer changes.
|
||||
// Not thread safe.
|
||||
func (b *Balancer) RegisterStatusUpdater(fn func(up bool)) error {
|
||||
if !b.wantsHealthCheck {
|
||||
return errors.New("healthCheck not enabled in config for this LeastTime service")
|
||||
}
|
||||
b.updaters = append(b.updaters, fn)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getHealthyServers returns the list of healthy, non-fenced servers.
|
||||
func (b *Balancer) getHealthyServers() []*namedHandler {
|
||||
b.handlersMu.RLock()
|
||||
defer b.handlersMu.RUnlock()
|
||||
|
||||
var healthy []*namedHandler
|
||||
for _, h := range b.handlers {
|
||||
if _, ok := b.status[h.name]; ok {
|
||||
if _, fenced := b.fenced[h.name]; !fenced {
|
||||
healthy = append(healthy, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
return healthy
|
||||
}
|
||||
|
||||
// selectWRR selects a server from candidates using Weighted Round Robin (EDF scheduling).
|
||||
// This is used for tie-breaking when multiple servers have identical scores.
|
||||
func (b *Balancer) selectWRR(candidates []*namedHandler) *namedHandler {
|
||||
if len(candidates) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
selected := candidates[0]
|
||||
minDeadline := math.MaxFloat64
|
||||
|
||||
// Find handler with earliest deadline.
|
||||
for _, h := range candidates {
|
||||
handlerDeadline := h.getDeadline()
|
||||
if handlerDeadline < minDeadline {
|
||||
minDeadline = handlerDeadline
|
||||
selected = h
|
||||
}
|
||||
}
|
||||
|
||||
// Update deadline based on when this server was selected (minDeadline),
|
||||
// not the global curDeadline. This ensures proper weighted distribution.
|
||||
newDeadline := minDeadline + 1/selected.weight
|
||||
selected.setDeadline(newDeadline)
|
||||
|
||||
// Track the maximum deadline assigned for initializing new servers.
|
||||
b.curDeadlineMu.Lock()
|
||||
if newDeadline > b.curDeadline {
|
||||
b.curDeadline = newDeadline
|
||||
}
|
||||
b.curDeadlineMu.Unlock()
|
||||
|
||||
return selected
|
||||
}
|
||||
|
||||
// Score = (avgResponseTime × (1 + inflightCount)) / weight.
|
||||
func (b *Balancer) nextServer() (*namedHandler, error) {
|
||||
healthy := b.getHealthyServers()
|
||||
|
||||
if len(healthy) == 0 {
|
||||
return nil, errNoAvailableServer
|
||||
}
|
||||
|
||||
if len(healthy) == 1 {
|
||||
return healthy[0], nil
|
||||
}
|
||||
|
||||
// Calculate scores and find minimum.
|
||||
minScore := math.MaxFloat64
|
||||
var candidates []*namedHandler
|
||||
|
||||
for _, h := range healthy {
|
||||
avgRT := h.getAvgResponseTime()
|
||||
inflight := float64(h.inflightCount.Load())
|
||||
score := (avgRT * (1 + inflight)) / h.weight
|
||||
|
||||
if score < minScore {
|
||||
minScore = score
|
||||
candidates = []*namedHandler{h}
|
||||
} else if score == minScore {
|
||||
candidates = append(candidates, h)
|
||||
}
|
||||
}
|
||||
|
||||
if len(candidates) == 1 {
|
||||
return candidates[0], nil
|
||||
}
|
||||
|
||||
// Multiple servers with same score: use WRR (EDF) tie-breaking.
|
||||
selected := b.selectWRR(candidates)
|
||||
if selected == nil {
|
||||
return nil, errNoAvailableServer
|
||||
}
|
||||
|
||||
return selected, nil
|
||||
}
|
||||
|
||||
func (b *Balancer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
// Handle sticky sessions first.
|
||||
if b.sticky != nil {
|
||||
h, rewrite, err := b.sticky.StickyHandler(req)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error while getting sticky handler")
|
||||
} else if h != nil {
|
||||
b.handlersMu.RLock()
|
||||
_, ok := b.status[h.Name]
|
||||
b.handlersMu.RUnlock()
|
||||
if ok {
|
||||
if rewrite {
|
||||
if err := b.sticky.WriteStickyCookie(rw, h.Name); err != nil {
|
||||
log.Error().Err(err).Msg("Writing sticky cookie")
|
||||
}
|
||||
}
|
||||
|
||||
h.ServeHTTP(rw, req)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server, err := b.nextServer()
|
||||
if err != nil {
|
||||
if errors.Is(err, errNoAvailableServer) {
|
||||
http.Error(rw, errNoAvailableServer.Error(), http.StatusServiceUnavailable)
|
||||
} else {
|
||||
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if b.sticky != nil {
|
||||
if err := b.sticky.WriteStickyCookie(rw, server.name); err != nil {
|
||||
log.Error().Err(err).Msg("Error while writing sticky cookie")
|
||||
}
|
||||
}
|
||||
|
||||
// Track inflight requests.
|
||||
server.inflightCount.Add(1)
|
||||
defer server.inflightCount.Add(-1)
|
||||
|
||||
startTime := time.Now()
|
||||
trace := &httptrace.ClientTrace{
|
||||
GotFirstResponseByte: func() {
|
||||
// Update average response time (TTFB).
|
||||
server.updateResponseTime(time.Since(startTime))
|
||||
},
|
||||
}
|
||||
traceCtx := httptrace.WithClientTrace(req.Context(), trace)
|
||||
server.ServeHTTP(rw, req.WithContext(traceCtx))
|
||||
}
|
||||
|
||||
// AddServer adds a handler with a server.
|
||||
func (b *Balancer) AddServer(name string, handler http.Handler, server dynamic.Server) {
|
||||
b.Add(name, handler, server.Weight, server.Fenced)
|
||||
}
|
||||
|
||||
// Add adds a handler.
|
||||
// A handler with a non-positive weight is ignored.
|
||||
func (b *Balancer) Add(name string, handler http.Handler, weight *int, fenced bool) {
|
||||
w := 1
|
||||
if weight != nil {
|
||||
w = *weight
|
||||
}
|
||||
|
||||
if w <= 0 { // non-positive weight is meaningless.
|
||||
return
|
||||
}
|
||||
|
||||
h := &namedHandler{Handler: handler, name: name, weight: float64(w)}
|
||||
|
||||
// Initialize deadline by adding 1/weight to current deadline.
|
||||
// This staggers servers to prevent all starting at the same time.
|
||||
var deadline float64
|
||||
b.curDeadlineMu.RLock()
|
||||
deadline = b.curDeadline + 1/h.weight
|
||||
b.curDeadlineMu.RUnlock()
|
||||
|
||||
h.setDeadline(deadline)
|
||||
|
||||
// Update balancer's current deadline with the new server's deadline.
|
||||
b.curDeadlineMu.Lock()
|
||||
b.curDeadline = deadline
|
||||
b.curDeadlineMu.Unlock()
|
||||
|
||||
b.handlersMu.Lock()
|
||||
b.handlers = append(b.handlers, h)
|
||||
b.status[name] = struct{}{}
|
||||
if fenced {
|
||||
b.fenced[name] = struct{}{}
|
||||
}
|
||||
b.handlersMu.Unlock()
|
||||
|
||||
if b.sticky != nil {
|
||||
b.sticky.AddHandler(name, handler)
|
||||
}
|
||||
}
|
||||
1093
pkg/server/service/loadbalancer/leasttime/leasttime_test.go
Normal file
1093
pkg/server/service/loadbalancer/leasttime/leasttime_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,8 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/server/service/loadbalancer"
|
||||
)
|
||||
|
||||
var errNoAvailableServer = errors.New("no available server")
|
||||
|
||||
type namedHandler struct {
|
||||
http.Handler
|
||||
|
||||
@ -81,7 +83,7 @@ func New(stickyConfig *dynamic.Sticky, wantsHealthCheck bool) *Balancer {
|
||||
}
|
||||
|
||||
// SetStatus sets on the balancer that its given child is now of the given
|
||||
// status. balancerName is only needed for logging purposes.
|
||||
// status. childName is only needed for logging purposes.
|
||||
func (b *Balancer) SetStatus(ctx context.Context, childName string, up bool) {
|
||||
b.handlersMu.Lock()
|
||||
defer b.handlersMu.Unlock()
|
||||
@ -126,14 +128,12 @@ func (b *Balancer) SetStatus(ctx context.Context, childName string, up bool) {
|
||||
// Not thread safe.
|
||||
func (b *Balancer) RegisterStatusUpdater(fn func(up bool)) error {
|
||||
if !b.wantsHealthCheck {
|
||||
return errors.New("healthCheck not enabled in config for this weighted service")
|
||||
return errors.New("healthCheck not enabled in config for this P2C service")
|
||||
}
|
||||
b.updaters = append(b.updaters, fn)
|
||||
return nil
|
||||
}
|
||||
|
||||
var errNoAvailableServer = errors.New("no available server")
|
||||
|
||||
func (b *Balancer) nextServer() (*namedHandler, error) {
|
||||
// We kept the same representation (map) as in the WRR strategy to improve maintainability.
|
||||
// However, with the P2C strategy, we only need a slice of healthy servers.
|
||||
|
||||
@ -12,6 +12,8 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/server/service/loadbalancer"
|
||||
)
|
||||
|
||||
var errNoAvailableServer = errors.New("no available server")
|
||||
|
||||
type namedHandler struct {
|
||||
http.Handler
|
||||
name string
|
||||
@ -94,7 +96,7 @@ func (b *Balancer) Pop() interface{} {
|
||||
}
|
||||
|
||||
// SetStatus sets on the balancer that its given child is now of the given
|
||||
// status. balancerName is only needed for logging purposes.
|
||||
// status. childName is only needed for logging purposes.
|
||||
func (b *Balancer) SetStatus(ctx context.Context, childName string, up bool) {
|
||||
b.handlersMu.Lock()
|
||||
defer b.handlersMu.Unlock()
|
||||
@ -139,14 +141,12 @@ func (b *Balancer) SetStatus(ctx context.Context, childName string, up bool) {
|
||||
// Not thread safe.
|
||||
func (b *Balancer) RegisterStatusUpdater(fn func(up bool)) error {
|
||||
if !b.wantsHealthCheck {
|
||||
return errors.New("healthCheck not enabled in config for this weighted service")
|
||||
return errors.New("healthCheck not enabled in config for this WRR service")
|
||||
}
|
||||
b.updaters = append(b.updaters, fn)
|
||||
return nil
|
||||
}
|
||||
|
||||
var errNoAvailableServer = errors.New("no available server")
|
||||
|
||||
func (b *Balancer) nextServer() (*namedHandler, error) {
|
||||
b.handlersMu.Lock()
|
||||
defer b.handlersMu.Unlock()
|
||||
|
||||
@ -29,6 +29,7 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/server/provider"
|
||||
"github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/failover"
|
||||
"github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/hrw"
|
||||
"github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/leasttime"
|
||||
"github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/mirror"
|
||||
"github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/p2c"
|
||||
"github.com/traefik/traefik/v3/pkg/server/service/loadbalancer/wrr"
|
||||
@ -402,6 +403,8 @@ func (m *Manager) getLoadBalancerServiceHandler(ctx context.Context, serviceName
|
||||
lb = p2c.New(service.Sticky, service.HealthCheck != nil)
|
||||
case dynamic.BalancerStrategyHRW:
|
||||
lb = hrw.New(service.HealthCheck != nil)
|
||||
case dynamic.BalancerStrategyLeastTime:
|
||||
lb = leasttime.New(service.Sticky, service.HealthCheck != nil)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported load-balancer strategy %q", service.Strategy)
|
||||
}
|
||||
|
||||
@ -82,6 +82,20 @@ func TestGetLoadBalancer(t *testing.T) {
|
||||
fwd: &forwarderMock{},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
desc: "Fails when unsupported strategy is set",
|
||||
serviceName: "test",
|
||||
service: &dynamic.ServersLoadBalancer{
|
||||
Strategy: "invalid",
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://localhost:8080",
|
||||
},
|
||||
},
|
||||
},
|
||||
fwd: &forwarderMock{},
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user