diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index 46d70ef9f..afc32d9b9 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -331,11 +331,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: @@ -1292,11 +1293,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: @@ -2791,6 +2793,243 @@ spec: spec: description: TraefikServiceSpec defines the desired state of a TraefikService. properties: + highestRandomWeight: + description: HighestRandomWeight defines the highest random weight + service configuration. + properties: + services: + description: Services defines the list of Kubernetes Service and/or + TraefikService to load-balance, with weight. + items: + description: Service defines an upstream HTTP service to proxy + traffic to. + properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls for healthy targets. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + unhealthyInterval: + anyOf: + - type: integer + - type: string + description: |- + UnhealthyInterval defines the frequency of the health check calls for unhealthy targets. + When UnhealthyInterval is not defined, it defaults to the Interval value. + Default: 30s + x-kubernetes-int-or-string: true + type: object + kind: + description: Kind defines the kind of the Service. + enum: + - Service + - TraefikService + type: string + name: + description: |- + Name defines the name of the referenced Kubernetes Service or TraefikService. + The differentiation between the two is specified in the Kind field. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Kubernetes Service or TraefikService. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. + The Kubernetes Service itself does load-balance to the pods. + By default, NativeLB is false. + type: boolean + nodePortLB: + description: |- + NodePortLB controls, when creating the load-balancer, + whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. + It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. + By default, NodePortLB is false. + type: boolean + passHostHeader: + description: |- + PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service. + By default, passHostHeader is true. + type: boolean + passiveHealthCheck: + description: PassiveHealthCheck defines passive health checks + for ExternalName services. + properties: + failureWindow: + anyOf: + - type: integer + - type: string + description: FailureWindow defines the time window during + which the failed attempts must occur for the server + to be marked as unhealthy. It also defines for how + long the server will be considered unhealthy. + x-kubernetes-int-or-string: true + maxFailedAttempts: + description: MaxFailedAttempts is the number of consecutive + failed attempts allowed within the failure window + before marking the server as unhealthy. + type: integer + type: object + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + responseForwarding: + description: ResponseForwarding defines how Traefik forwards + the response from the upstream Kubernetes Service to the + client. + properties: + flushInterval: + description: |- + FlushInterval defines the interval, in milliseconds, in between flushes to the client while copying the response body. + A negative value means to flush immediately after each write to the client. + This configuration is ignored when ReverseProxy recognizes a response as a streaming response; + for such responses, writes are flushed to the client immediately. + Default: 100ms + type: string + type: object + scheme: + description: |- + Scheme defines the scheme to use for the request to the upstream Kubernetes Service. + It defaults to https when Kubernetes Service port is 443, http otherwise. + type: string + serversTransport: + description: |- + ServersTransport defines the name of ServersTransport resource to use. + It allows to configure the transport between Traefik and your servers. + Can only be used on a Kubernetes Service. + type: string + sticky: + description: |- + Sticky defines the sticky sessions configuration. + More info: https://doc.traefik.io/traefik/v3.5/routing/services/#sticky-sessions + properties: + cookie: + description: Cookie defines the sticky cookie configuration. + properties: + domain: + description: |- + Domain defines the host to which the cookie will be sent. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value + type: string + httpOnly: + description: HTTPOnly defines whether the cookie + can be accessed by client-side APIs, such as JavaScript. + type: boolean + maxAge: + description: |- + MaxAge defines the number of seconds until the cookie expires. + When set to a negative number, the cookie expires immediately. + When set to zero, the cookie never expires. + type: integer + name: + description: Name defines the Cookie name. + type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string + sameSite: + description: |- + SameSite defines the same site policy. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + enum: + - none + - lax + - strict + type: string + secure: + description: Secure defines whether the cookie can + only be transmitted over an encrypted connection + (i.e. HTTPS). + type: boolean + type: object + type: object + 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). + RoundRobin value is deprecated and supported for backward compatibility. + enum: + - wrr + - p2c + - hrw + - RoundRobin + type: string + weight: + description: |- + Weight defines the weight and should only be specified when Name references a TraefikService object + (and to be precise, one that embeds a Weighted Round Robin). + minimum: 0 + type: integer + required: + - name + type: object + type: array + type: object mirroring: description: Mirroring defines the Mirroring service configuration. properties: @@ -3100,11 +3339,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: @@ -3246,11 +3486,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: @@ -3479,11 +3720,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: diff --git a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml index 0ffc11dfb..99d510b4b 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_ingressroutes.yaml @@ -331,11 +331,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index 97135b70d..3c976793f 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -484,11 +484,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: diff --git a/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml b/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml index 1ed24d021..e7bf10673 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_traefikservices.yaml @@ -44,6 +44,243 @@ spec: spec: description: TraefikServiceSpec defines the desired state of a TraefikService. properties: + highestRandomWeight: + description: HighestRandomWeight defines the highest random weight + service configuration. + properties: + services: + description: Services defines the list of Kubernetes Service and/or + TraefikService to load-balance, with weight. + items: + description: Service defines an upstream HTTP service to proxy + traffic to. + properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls for healthy targets. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + unhealthyInterval: + anyOf: + - type: integer + - type: string + description: |- + UnhealthyInterval defines the frequency of the health check calls for unhealthy targets. + When UnhealthyInterval is not defined, it defaults to the Interval value. + Default: 30s + x-kubernetes-int-or-string: true + type: object + kind: + description: Kind defines the kind of the Service. + enum: + - Service + - TraefikService + type: string + name: + description: |- + Name defines the name of the referenced Kubernetes Service or TraefikService. + The differentiation between the two is specified in the Kind field. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Kubernetes Service or TraefikService. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. + The Kubernetes Service itself does load-balance to the pods. + By default, NativeLB is false. + type: boolean + nodePortLB: + description: |- + NodePortLB controls, when creating the load-balancer, + whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. + It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. + By default, NodePortLB is false. + type: boolean + passHostHeader: + description: |- + PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service. + By default, passHostHeader is true. + type: boolean + passiveHealthCheck: + description: PassiveHealthCheck defines passive health checks + for ExternalName services. + properties: + failureWindow: + anyOf: + - type: integer + - type: string + description: FailureWindow defines the time window during + which the failed attempts must occur for the server + to be marked as unhealthy. It also defines for how + long the server will be considered unhealthy. + x-kubernetes-int-or-string: true + maxFailedAttempts: + description: MaxFailedAttempts is the number of consecutive + failed attempts allowed within the failure window + before marking the server as unhealthy. + type: integer + type: object + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + responseForwarding: + description: ResponseForwarding defines how Traefik forwards + the response from the upstream Kubernetes Service to the + client. + properties: + flushInterval: + description: |- + FlushInterval defines the interval, in milliseconds, in between flushes to the client while copying the response body. + A negative value means to flush immediately after each write to the client. + This configuration is ignored when ReverseProxy recognizes a response as a streaming response; + for such responses, writes are flushed to the client immediately. + Default: 100ms + type: string + type: object + scheme: + description: |- + Scheme defines the scheme to use for the request to the upstream Kubernetes Service. + It defaults to https when Kubernetes Service port is 443, http otherwise. + type: string + serversTransport: + description: |- + ServersTransport defines the name of ServersTransport resource to use. + It allows to configure the transport between Traefik and your servers. + Can only be used on a Kubernetes Service. + type: string + sticky: + description: |- + Sticky defines the sticky sessions configuration. + More info: https://doc.traefik.io/traefik/v3.5/routing/services/#sticky-sessions + properties: + cookie: + description: Cookie defines the sticky cookie configuration. + properties: + domain: + description: |- + Domain defines the host to which the cookie will be sent. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value + type: string + httpOnly: + description: HTTPOnly defines whether the cookie + can be accessed by client-side APIs, such as JavaScript. + type: boolean + maxAge: + description: |- + MaxAge defines the number of seconds until the cookie expires. + When set to a negative number, the cookie expires immediately. + When set to zero, the cookie never expires. + type: integer + name: + description: Name defines the Cookie name. + type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string + sameSite: + description: |- + SameSite defines the same site policy. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + enum: + - none + - lax + - strict + type: string + secure: + description: Secure defines whether the cookie can + only be transmitted over an encrypted connection + (i.e. HTTPS). + type: boolean + type: object + type: object + 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). + RoundRobin value is deprecated and supported for backward compatibility. + enum: + - wrr + - p2c + - hrw + - RoundRobin + type: string + weight: + description: |- + Weight defines the weight and should only be specified when Name references a TraefikService object + (and to be precise, one that embeds a Weighted Round Robin). + minimum: 0 + type: integer + required: + - name + type: object + type: array + type: object mirroring: description: Mirroring defines the Mirroring service configuration. properties: @@ -353,11 +590,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: @@ -499,11 +737,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: @@ -732,11 +971,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: diff --git a/docs/content/reference/routing-configuration/kubernetes/crd/http/traefikservice.md b/docs/content/reference/routing-configuration/kubernetes/crd/http/traefikservice.md index 2cdf94c9c..8b3f18f3f 100644 --- a/docs/content/reference/routing-configuration/kubernetes/crd/http/traefikservice.md +++ b/docs/content/reference/routing-configuration/kubernetes/crd/http/traefikservice.md @@ -3,11 +3,12 @@ title: "Traefik Kubernetes Services Documentation" description: "Learn how to configure routing and load balancing in Traefik Proxy to reach Services, which handle incoming requests. Read the technical documentation." --- -A `TraefikService` is a custom resource that sits on top of the Kubernetes Services. It enables advanced load-balancing features such as a [Weighted Round Robin](#weighted-round-robin) load balancing or a [Mirroring](#mirroring) between your Kubernetes Services. +A `TraefikService` is a custom resource that sits on top of the Kubernetes Services. It enables advanced load-balancing features such as a [Weighted Round Robin](#weighted-round-robin) load balancing, a [Highest Random Weight](#highest-random-weight) load balancing, or a [Mirroring](#mirroring) between your Kubernetes Services. Services configure how to reach the actual endpoints that will eventually handle incoming requests. In Traefik, the target service can be either a standard [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/)—which exposes a pod—or a TraefikService. The latter allows you to combine advanced load-balancing options like: - [Weighted Round Robin load balancing](#weighted-round-robin). +- [Highest Random Weight load balancing](#highest-random-weight). - [Mirroring](#mirroring). ## Weighted Round Robin @@ -255,6 +256,143 @@ In the example above, to keep a session open with the same server, the client wo curl -H Host:example.com -b "lvl1=default-whoami1-80; lvl2=http://10.42.0.6:80" http://localhost:8000/foo ``` +## Highest Random Weight + +The HRW (Highest Random Weight) load balancer uses consistent hashing to ensure that requests from the same client IP are always routed to the same service. Unlike weighted round-robin which distributes requests based on weights, HRW provides consistent routing based on the client's remote address. + +This is particularly useful for maintaining session affinity without requiring sticky cookies, as clients will consistently reach the same backend service based on their IP address. + +### Configuration Example + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: test-hrw + namespace: apps + +spec: + entryPoints: + - websecure + routes: + - match: Host(`example.com`) && PathPrefix(`/app`) + kind: Rule + services: + # Set an HRW TraefikService + - name: hrw1 + namespace: apps + kind: TraefikService + tls: + secretName: supersecret +``` + +```yaml tab="TraefikService HRW" +apiVersion: traefik.io/v1alpha1 +kind: TraefikService +metadata: + name: hrw1 + namespace: apps + +spec: + highestRandomWeight: + services: + # Kubernetes Service with weight 10 + - name: svc1 + namespace: apps + port: 80 + weight: 10 + # Kubernetes Service with weight 20 + - name: svc2 + namespace: apps + port: 80 + weight: 20 + # Another TraefikService + - name: wrr1 + namespace: apps + kind: TraefikService + weight: 15 +``` + +```yaml tab="Kubernetes Services" +apiVersion: v1 +kind: Service +metadata: + name: svc1 + namespace: apps + +spec: + ports: + - name: http + port: 80 + selector: + app: traefiklabs + task: app1 +--- +apiVersion: v1 +kind: Service +metadata: + name: svc2 + namespace: apps + +spec: + ports: + - name: http + port: 80 + selector: + app: traefiklabs + task: app2 +``` + +### Configuration Options + +| Field | Description | Default | Required | +|:---------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------| +| `services` | List of any combination of TraefikService and [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). Each service must have a weight assigned. | | Yes | +| `services[m].`
`kind`
| Kind of the service targeted.
Two values allowed:
- **Service**: Kubernetes Service
- **TraefikService**: Traefik Service. | "" | No | +| `services[m].`
`name`
| Service name.
The character `@` is not authorized. | "" | Yes | +| `services[m].`
`namespace`
| Service namespace. | "" | No | +| `services[m].`
`port`
| Service port (number or port name).
Evaluated only if the kind is **Service**. | "" | No | +| `services[m].`
`weight`
| Service weight used in the HRW algorithm. Higher weights increase the probability of selection for a given client IP. | 1 | No | +| `services[m].`
`responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind is **Service**. | 100ms | No | +| `services[m].`
`scheme`
| Scheme to use for the request to the upstream Kubernetes Service.
Evaluated only if the kind is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | +| `services[m].`
`serversTransport`
| Name of ServersTransport resource to use to configure the transport between Traefik and your servers.
Evaluated only if the kind is **Service**. | "" | No | +| `services[m].`
`passHostHeader`
| Forward client Host header to server.
Evaluated only if the kind is **Service**. | true | No | +| `services[m].`
`healthCheck.scheme`
| Server URL scheme for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "" | No | +| `services[m].`
`healthCheck.mode`
| Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "http" | No | +| `services[m].`
`healthCheck.path`
| Server URL path for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "" | No | +| `services[m].`
`healthCheck.interval`
| Frequency of the health check calls for healthy targets.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "100ms" | No | +| `services[m].`
`healthCheck.unhealthyInterval`
| Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "100ms" | No | +| `services[m].`
`healthCheck.method`
| HTTP method for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "GET" | No | +| `services[m].`
`healthCheck.status`
| Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind is **Service**. | | No | +| `services[m].`
`healthCheck.port`
| URL port for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | | No | +| `services[m].`
`healthCheck.timeout`
| Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "5s" | No | +| `services[m].`
`healthCheck.hostname`
| Value in the Host header of the health check request.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "" | No | +| `services[m].`
`healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | true | No | +| `services[m].`
`healthCheck.headers`
| Map of header to send to the health check endpoint
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | | No | +| `services[m].`
`sticky.`
`cookie.name`
| Name of the cookie used for the stickiness.
Evaluated only if the kind is **Service**. | Abbreviation of a sha1
(ex: `_1d52e`). | No | +| `services[m].`
`sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind is **Service**. | false | No | +| `services[m].`
`sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind is **Service**. | false | No | +| `services[m].`
`sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy.
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind is **Service**. | "" | No | +| `services[m].`
`sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind is **Service**. | 0 | No | +| `services[m].`
`strategy`
| Load balancing strategy between the servers.
RoundRobin is the only supported value yet.
Evaluated only if the kind is **Service**. | "RoundRobin" | No | +| `services[m].`
`nativeLB`
| Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind is **Service**. | false | No | +| `services[m].`
`nodePortLB`
| Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind is **Service**. | false | No | + +### How HRW Works + +The Highest Random Weight algorithm combines consistent hashing with weighted load balancing: + +1. **Consistent Hashing**: For each incoming request, the client's remote address is hashed to ensure consistent routing. +2. **Weighted Selection**: Each service is assigned a random value based on both the hash of the client IP and the service's weight. +3. **Highest Selection**: The service with the highest calculated value receives the request. + +This approach provides several benefits: + +- **Session Affinity**: Clients consistently reach the same backend service +- **Weighted Distribution**: Services with higher weights are more likely to be selected +- **No State Required**: Unlike sticky cookies, no client-side or server-side state is needed +- **Fault Tolerance**: If a service becomes unavailable, requests are redistributed consistently among remaining services + ## Mirroring The mirroring is able to mirror requests sent to a service to other services. diff --git a/integration/fixtures/highest_random_weight.toml b/integration/fixtures/highest_random_weight.toml new file mode 100644 index 000000000..de6bd1d4a --- /dev/null +++ b/integration/fixtures/highest_random_weight.toml @@ -0,0 +1,48 @@ +[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 = "hrw" + rule = "Path(`/whoami`)" + + +[http.services] + [http.services.hrw.highestRandomWeight] + [[http.services.hrw.highestRandomWeight.services]] + name = "service1" + weight = 10 + [[http.services.hrw.highestRandomWeight.services]] + name = "service2" + weight = 20 + [[http.services.hrw.highestRandomWeight.services]] + name = "service3" + weight = 30 + + [http.services.service1.loadBalancer] + [[http.services.service1.loadBalancer.servers]] + url = "{{ .Service1Server }}" + [http.services.service2.loadBalancer] + [[http.services.service2.loadBalancer.servers]] + url = "{{ .Service2Server }}" + [http.services.service3.loadBalancer] + [[http.services.service3.loadBalancer.servers]] + url = "{{ .Service3Server }}" \ No newline at end of file diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 46d70ef9f..afc32d9b9 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -331,11 +331,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: @@ -1292,11 +1293,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: @@ -2791,6 +2793,243 @@ spec: spec: description: TraefikServiceSpec defines the desired state of a TraefikService. properties: + highestRandomWeight: + description: HighestRandomWeight defines the highest random weight + service configuration. + properties: + services: + description: Services defines the list of Kubernetes Service and/or + TraefikService to load-balance, with weight. + items: + description: Service defines an upstream HTTP service to proxy + traffic to. + properties: + healthCheck: + description: Healthcheck defines health checks for ExternalName + services. + properties: + followRedirects: + description: |- + FollowRedirects defines whether redirects should be followed during the health check calls. + Default: true + type: boolean + headers: + additionalProperties: + type: string + description: Headers defines custom headers to be sent + to the health check endpoint. + type: object + hostname: + description: Hostname defines the value of hostname + in the Host header of the health check request. + type: string + interval: + anyOf: + - type: integer + - type: string + description: |- + Interval defines the frequency of the health check calls for healthy targets. + Default: 30s + x-kubernetes-int-or-string: true + method: + description: Method defines the healthcheck method. + type: string + mode: + description: |- + Mode defines the health check mode. + If defined to grpc, will use the gRPC health check protocol to probe the server. + Default: http + type: string + path: + description: Path defines the server URL path for the + health check endpoint. + type: string + port: + description: Port defines the server URL port for the + health check endpoint. + type: integer + scheme: + description: Scheme replaces the server URL scheme for + the health check endpoint. + type: string + status: + description: Status defines the expected HTTP status + code of the response to the health check request. + type: integer + timeout: + anyOf: + - type: integer + - type: string + description: |- + Timeout defines the maximum duration Traefik will wait for a health check request before considering the server unhealthy. + Default: 5s + x-kubernetes-int-or-string: true + unhealthyInterval: + anyOf: + - type: integer + - type: string + description: |- + UnhealthyInterval defines the frequency of the health check calls for unhealthy targets. + When UnhealthyInterval is not defined, it defaults to the Interval value. + Default: 30s + x-kubernetes-int-or-string: true + type: object + kind: + description: Kind defines the kind of the Service. + enum: + - Service + - TraefikService + type: string + name: + description: |- + Name defines the name of the referenced Kubernetes Service or TraefikService. + The differentiation between the two is specified in the Kind field. + type: string + namespace: + description: Namespace defines the namespace of the referenced + Kubernetes Service or TraefikService. + type: string + nativeLB: + description: |- + NativeLB controls, when creating the load-balancer, + whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. + The Kubernetes Service itself does load-balance to the pods. + By default, NativeLB is false. + type: boolean + nodePortLB: + description: |- + NodePortLB controls, when creating the load-balancer, + whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is NodePort. + It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. + By default, NodePortLB is false. + type: boolean + passHostHeader: + description: |- + PassHostHeader defines whether the client Host header is forwarded to the upstream Kubernetes Service. + By default, passHostHeader is true. + type: boolean + passiveHealthCheck: + description: PassiveHealthCheck defines passive health checks + for ExternalName services. + properties: + failureWindow: + anyOf: + - type: integer + - type: string + description: FailureWindow defines the time window during + which the failed attempts must occur for the server + to be marked as unhealthy. It also defines for how + long the server will be considered unhealthy. + x-kubernetes-int-or-string: true + maxFailedAttempts: + description: MaxFailedAttempts is the number of consecutive + failed attempts allowed within the failure window + before marking the server as unhealthy. + type: integer + type: object + port: + anyOf: + - type: integer + - type: string + description: |- + Port defines the port of a Kubernetes Service. + This can be a reference to a named port. + x-kubernetes-int-or-string: true + responseForwarding: + description: ResponseForwarding defines how Traefik forwards + the response from the upstream Kubernetes Service to the + client. + properties: + flushInterval: + description: |- + FlushInterval defines the interval, in milliseconds, in between flushes to the client while copying the response body. + A negative value means to flush immediately after each write to the client. + This configuration is ignored when ReverseProxy recognizes a response as a streaming response; + for such responses, writes are flushed to the client immediately. + Default: 100ms + type: string + type: object + scheme: + description: |- + Scheme defines the scheme to use for the request to the upstream Kubernetes Service. + It defaults to https when Kubernetes Service port is 443, http otherwise. + type: string + serversTransport: + description: |- + ServersTransport defines the name of ServersTransport resource to use. + It allows to configure the transport between Traefik and your servers. + Can only be used on a Kubernetes Service. + type: string + sticky: + description: |- + Sticky defines the sticky sessions configuration. + More info: https://doc.traefik.io/traefik/v3.5/routing/services/#sticky-sessions + properties: + cookie: + description: Cookie defines the sticky cookie configuration. + properties: + domain: + description: |- + Domain defines the host to which the cookie will be sent. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value + type: string + httpOnly: + description: HTTPOnly defines whether the cookie + can be accessed by client-side APIs, such as JavaScript. + type: boolean + maxAge: + description: |- + MaxAge defines the number of seconds until the cookie expires. + When set to a negative number, the cookie expires immediately. + When set to zero, the cookie never expires. + type: integer + name: + description: Name defines the Cookie name. + type: string + path: + description: |- + Path defines the path that must exist in the requested URL for the browser to send the Cookie header. + When not provided the cookie will be sent on every request to the domain. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value + type: string + sameSite: + description: |- + SameSite defines the same site policy. + More info: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite + enum: + - none + - lax + - strict + type: string + secure: + description: Secure defines whether the cookie can + only be transmitted over an encrypted connection + (i.e. HTTPS). + type: boolean + type: object + type: object + 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). + RoundRobin value is deprecated and supported for backward compatibility. + enum: + - wrr + - p2c + - hrw + - RoundRobin + type: string + weight: + description: |- + Weight defines the weight and should only be specified when Name references a TraefikService object + (and to be precise, one that embeds a Weighted Round Robin). + minimum: 0 + type: integer + required: + - name + type: object + type: array + type: object mirroring: description: Mirroring defines the Mirroring service configuration. properties: @@ -3100,11 +3339,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: @@ -3246,11 +3486,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: @@ -3479,11 +3720,12 @@ spec: strategy: description: |- Strategy defines the load balancing strategy between the servers. - Supported values are: wrr (Weighed round-robin) and p2c (Power of two choices). + Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). RoundRobin value is deprecated and supported for backward compatibility. enum: - wrr - p2c + - hrw - RoundRobin type: string weight: diff --git a/integration/fixtures/k8s/06-ingressroute-traefikservices.yml b/integration/fixtures/k8s/06-ingressroute-traefikservices.yml index d979bc425..b1ce5660b 100644 --- a/integration/fixtures/k8s/06-ingressroute-traefikservices.yml +++ b/integration/fixtures/k8s/06-ingressroute-traefikservices.yml @@ -28,6 +28,23 @@ spec: - name: whoami port: 80 +--- +apiVersion: traefik.io/v1alpha1 +kind: TraefikService +metadata: + name: hrw1 + namespace: default + +spec: + highestRandomWeight: + services: + - name: whoami + port: 80 + weight: 10 + - name: whoami + port: 80 + weight: 20 + --- apiVersion: traefik.io/v1alpha1 kind: IngressRoute @@ -47,6 +64,22 @@ spec: --- apiVersion: traefik.io/v1alpha1 kind: IngressRoute +metadata: + name: test4.route + namespace: default + +spec: + entryPoints: + - web + routes: + - match: Host(`foo.com`) && PathPrefix(`/hrw1`) + kind: Rule + services: + - name: hrw1 + kind: TraefikService +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute metadata: name: api.route namespace: default diff --git a/integration/simple_test.go b/integration/simple_test.go index 675e21c3d..2be9f7f06 100644 --- a/integration/simple_test.go +++ b/integration/simple_test.go @@ -1217,6 +1217,74 @@ func (s *SimpleSuite) TestMirrorCanceled() { assert.Equal(s.T(), int32(0), val2) } +func (s *SimpleSuite) TestHighestRandomWeight() { + var count1, count2, count3 int32 + + service1 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + atomic.AddInt32(&count1, 1) + rw.WriteHeader(http.StatusOK) + })) + + service2 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + atomic.AddInt32(&count2, 1) + rw.WriteHeader(http.StatusOK) + })) + + service3 := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + atomic.AddInt32(&count3, 1) + rw.WriteHeader(http.StatusOK) + })) + + service1Server := service1.URL + service2Server := service2.URL + service3Server := service3.URL + + file := s.adaptFile("fixtures/highest_random_weight.toml", struct { + Service1Server string + Service2Server string + Service3Server string + }{Service1Server: service1Server, Service2Server: service2Server, Service3Server: service3Server}) + + s.traefikCmd(withConfigFile(file)) + + err := try.GetRequest("http://127.0.0.1:8080/api/http/services", 3*time.Second, try.BodyContains("service1", "service2", "service3", "hrw")) + require.NoError(s.T(), err) + + // Make 10 requests from the same client (127.0.0.1) - should all go to the same service + client := &http.Client{} + for range 10 { + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil) + require.NoError(s.T(), err) + + response, err := client.Do(req) + require.NoError(s.T(), err) + assert.Equal(s.T(), http.StatusOK, response.StatusCode) + response.Body.Close() + } + + // Check if all requests went to the same service + val1 := atomic.LoadInt32(&count1) + val2 := atomic.LoadInt32(&count2) + val3 := atomic.LoadInt32(&count3) + + // All requests should have been handled (total should be 10) + assert.Equal(s.T(), int32(10), val1+val2+val3) + + // All requests from same remoteAddr (127.0.0.1) should go to exactly one service + servicesUsed := 0 + if val1 > 0 { + servicesUsed++ + } + if val2 > 0 { + servicesUsed++ + } + if val3 > 0 { + servicesUsed++ + } + + assert.Equal(s.T(), 1, servicesUsed, "All requests from same remoteAddr should go to exactly one service") +} + func (s *SimpleSuite) TestSecureAPI() { s.createComposeProject("base") diff --git a/integration/testdata/rawdata-crd.json b/integration/testdata/rawdata-crd.json index 9f1b2d33c..4f9c63322 100644 --- a/integration/testdata/rawdata-crd.json +++ b/integration/testdata/rawdata-crd.json @@ -78,6 +78,24 @@ "web" ] }, + "default-test4-route-466d8d3a547de55dfe8a@kubernetescrd": { + "entryPoints": [ + "web" + ], + "service": "default-hrw1", + "rule": "Host(`foo.com`) \u0026\u0026 PathPrefix(`/hrw1`)", + "priority": 38, + "observability": { + "accessLogs": true, + "metrics": true, + "tracing": true, + "traceVerbosity": "minimal" + }, + "status": "enabled", + "using": [ + "web" + ] + }, "default-testst-route-60ad45fcb5fc1f5f3629@kubernetescrd": { "entryPoints": [ "web" @@ -157,6 +175,24 @@ "dashboard@internal": { "status": "enabled" }, + "default-hrw1@kubernetescrd": { + "highestRandomWeight": { + "services": [ + { + "name": "default-whoami-80", + "weight": 10 + }, + { + "name": "default-whoami-80", + "weight": 20 + } + ] + }, + "status": "enabled", + "usedBy": [ + "default-test4-route-466d8d3a547de55dfe8a@kubernetescrd" + ] + }, "default-mirror1@kubernetescrd": { "mirroring": { "service": "default-whoami-80", diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index 4fbf06d58..b64c444f1 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -259,7 +259,7 @@ const ( BalancerStrategyWRR BalancerStrategy = "wrr" // BalancerStrategyP2C is the power of two choices strategy. BalancerStrategyP2C BalancerStrategy = "p2c" - // BalancerStrategyHRW is the power of two choices strategy. + // BalancerStrategyHRW is the highest random weight strategy. BalancerStrategyHRW BalancerStrategy = "hrw" ) diff --git a/pkg/provider/kubernetes/crd/fixtures/with_highest_random_weight.yml b/pkg/provider/kubernetes/crd/fixtures/with_highest_random_weight.yml new file mode 100644 index 000000000..f6684c03e --- /dev/null +++ b/pkg/provider/kubernetes/crd/fixtures/with_highest_random_weight.yml @@ -0,0 +1,107 @@ +--- +kind: EndpointSlice +apiVersion: discovery.k8s.io/v1 +metadata: + name: whoami1-abc + namespace: default + labels: + kubernetes.io/service-name: whoami1 + +addressType: IPv4 +ports: + - name: web + port: 8080 +endpoints: + - addresses: + - 10.10.0.1 + - 10.10.0.2 + conditions: + ready: true + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami1 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: traefiklabs + task: whoami1 + +--- +kind: EndpointSlice +apiVersion: discovery.k8s.io/v1 +metadata: + name: whoami2-abc + namespace: default + labels: + kubernetes.io/service-name: whoami2 + +addressType: IPv4 +ports: + - name: web + port: 8080 +endpoints: + - addresses: + - 10.10.0.3 + - 10.10.0.4 + conditions: + ready: true + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami2 + namespace: default + +spec: + ports: + - name: web + port: 8080 + selector: + app: traefiklabs + task: whoami2 + +--- +apiVersion: traefik.io/v1alpha1 +kind: TraefikService +metadata: + name: hrw1 + namespace: default + +spec: + highestRandomWeight: + services: + - name: whoami1 + kind: Service + port: 8080 + weight: 10 + - name: whoami2 + kind: Service + port: 8080 + weight: 20 + +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: test.route + namespace: default + +spec: + entryPoints: + - web + + routes: + - match: Host(`foo.com`) && PathPrefix(`/foo`) + kind: Rule + priority: 12 + services: + - name: hrw1 + kind: TraefikService \ No newline at end of file diff --git a/pkg/provider/kubernetes/crd/kubernetes_http.go b/pkg/provider/kubernetes/crd/kubernetes_http.go index 09c206a74..e6b4df448 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_http.go +++ b/pkg/provider/kubernetes/crd/kubernetes_http.go @@ -216,13 +216,17 @@ type configBuilder struct { func (c configBuilder) buildTraefikService(ctx context.Context, tService *traefikv1alpha1.TraefikService, conf map[string]*dynamic.Service) error { id := provider.Normalize(makeID(tService.Namespace, tService.Name)) - if tService.Spec.Weighted != nil { + switch { + case tService.Spec.Weighted != nil: return c.buildServicesLB(ctx, tService.Namespace, tService.Spec, id, conf) - } else if tService.Spec.Mirroring != nil { + case tService.Spec.Mirroring != nil: return c.buildMirroring(ctx, tService, id, conf) - } + case tService.Spec.HighestRandomWeight != nil: + return c.buildHRW(ctx, tService, id, conf) + default: - return errors.New("unspecified service type") + return errors.New("unspecified service type") + } } // buildServicesLB creates the configuration for the load-balancer of services named id, and defined in tService. @@ -329,7 +333,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: + case dynamic.BalancerStrategyWRR, dynamic.BalancerStrategyP2C, dynamic.BalancerStrategyHRW: lb.Strategy = svc.Strategy // Here we are just logging a warning as the default value is already applied. @@ -644,6 +648,38 @@ func (c configBuilder) nameAndService(ctx context.Context, parentNamespace strin } } +func (c configBuilder) buildHRW(ctx context.Context, tService *traefikv1alpha1.TraefikService, id string, conf map[string]*dynamic.Service) error { + var hrwServices []dynamic.HRWService + for _, hrwService := range tService.Spec.HighestRandomWeight.Services { + hrwServiceName, k8sService, err := c.nameAndService(ctx, tService.Namespace, hrwService.LoadBalancerSpec) + if err != nil { + return err + } + + if k8sService != nil { + conf[hrwServiceName] = k8sService + } + + weight := hrwService.Weight + if weight == nil { + weight = func(i int) *int { return &i }(1) + } + + hrwServices = append(hrwServices, dynamic.HRWService{ + Name: hrwServiceName, + Weight: weight, + }) + } + + conf[id] = &dynamic.Service{ + HighestRandomWeight: &dynamic.HighestRandomWeight{ + Services: hrwServices, + }, + } + + return nil +} + func splitSvcNameProvider(name string) (string, string) { parts := strings.Split(name, providerNamespaceSeparator) diff --git a/pkg/provider/kubernetes/crd/kubernetes_test.go b/pkg/provider/kubernetes/crd/kubernetes_test.go index ec6651091..ac6b2cbac 100644 --- a/pkg/provider/kubernetes/crd/kubernetes_test.go +++ b/pkg/provider/kubernetes/crd/kubernetes_test.go @@ -3150,6 +3150,79 @@ func TestLoadIngressRoutes(t *testing.T) { }, }, }, + { + desc: "one kube services in a highest random weight", + paths: []string{"with_highest_random_weight.yml"}, + expected: &dynamic.Configuration{ + UDP: &dynamic.UDPConfiguration{ + Routers: map[string]*dynamic.UDPRouter{}, + Services: map[string]*dynamic.UDPService{}, + }, + TLS: &dynamic.TLSConfiguration{}, + 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-77c62dfe9517144aeeaa": { + EntryPoints: []string{"web"}, + Service: "default-hrw1", + Rule: "Host(`foo.com`) && PathPrefix(`/foo`)", + Priority: 12, + }, + }, + Middlewares: map[string]*dynamic.Middleware{}, + Services: map[string]*dynamic.Service{ + "default-hrw1": { + HighestRandomWeight: &dynamic.HighestRandomWeight{ + Services: []dynamic.HRWService{ + {Name: "default-whoami1-8080", Weight: pointer(10)}, + {Name: "default-whoami2-8080", Weight: pointer(20)}, + }, + }, + }, + "default-whoami1-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Strategy: dynamic.BalancerStrategyWRR, + Servers: []dynamic.Server{ + { + URL: "http://10.10.0.1:8080", + }, + { + URL: "http://10.10.0.2:8080", + }, + }, + PassHostHeader: pointer(true), + ResponseForwarding: &dynamic.ResponseForwarding{ + FlushInterval: ptypes.Duration(100 * time.Millisecond), + }, + }, + }, + "default-whoami2-8080": { + LoadBalancer: &dynamic.ServersLoadBalancer{ + Strategy: dynamic.BalancerStrategyWRR, + 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{}, + }, + }, + }, { desc: "One ingress Route with two different services, with weights", paths: []string{"services.yml", "with_two_services_weight.yml"}, diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go index fb5513e4c..16088fd8b 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/ingressroute.go @@ -114,10 +114,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) and p2c (Power of two choices). + // Supported values are: wrr (Weighed round-robin), p2c (Power of two choices), and hrw (Highest Random Weight). // 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;RoundRobin + // +kubebuilder:validation:Enum=wrr;p2c;hrw;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. diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/service.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/service.go index 385aa6acc..aececb9e1 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/service.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/service.go @@ -44,6 +44,8 @@ type TraefikServiceSpec struct { Weighted *WeightedRoundRobin `json:"weighted,omitempty"` // Mirroring defines the Mirroring service configuration. Mirroring *Mirroring `json:"mirroring,omitempty"` + // HighestRandomWeight defines the highest random weight service configuration. + HighestRandomWeight *HighestRandomWeight `json:"highestRandomWeight,omitempty"` } // +k8s:deepcopy-gen=true @@ -86,3 +88,12 @@ type WeightedRoundRobin struct { // More info: https://doc.traefik.io/traefik/v3.5/routing/providers/kubernetes-crd/#stickiness-and-load-balancing Sticky *dynamic.Sticky `json:"sticky,omitempty"` } + +// +k8s:deepcopy-gen=true + +// HighestRandomWeight holds the highest random weight configuration. +// More info: https://doc.traefik.io/traefik/v3.5/routing/services/#highest-random-configuration +type HighestRandomWeight struct { + // Services defines the list of Kubernetes Service and/or TraefikService to load-balance, with weight. + Services []Service `json:"services,omitempty"` +} diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go index 62dd22d9e..be6159b65 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/zz_generated.deepcopy.go @@ -349,6 +349,29 @@ func (in *ForwardingTimeouts) DeepCopy() *ForwardingTimeouts { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HighestRandomWeight) DeepCopyInto(out *HighestRandomWeight) { + *out = *in + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make([]Service, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HighestRandomWeight. +func (in *HighestRandomWeight) DeepCopy() *HighestRandomWeight { + if in == nil { + return nil + } + out := new(HighestRandomWeight) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IngressRoute) DeepCopyInto(out *IngressRoute) { *out = *in @@ -2028,6 +2051,11 @@ func (in *TraefikServiceSpec) DeepCopyInto(out *TraefikServiceSpec) { *out = new(Mirroring) (*in).DeepCopyInto(*out) } + if in.HighestRandomWeight != nil { + in, out := &in.HighestRandomWeight, &out.HighestRandomWeight + *out = new(HighestRandomWeight) + (*in).DeepCopyInto(*out) + } return }