Add limit-connections support

This commit is contained in:
Kangmin Kim 2026-04-29 23:22:06 +09:00 committed by GitHub
parent c1d3c08390
commit 2433b18fef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 145 additions and 1 deletions

View File

@ -376,6 +376,7 @@ The following annotations are organized by category for easier navigation.
| <a id="opt-nginx-ingress-kubernetes-iolimit-rps" href="#opt-nginx-ingress-kubernetes-iolimit-rps" title="#opt-nginx-ingress-kubernetes-iolimit-rps">`nginx.ingress.kubernetes.io/limit-rps`</a> | Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. |
| <a id="opt-nginx-ingress-kubernetes-iolimit-rpm" href="#opt-nginx-ingress-kubernetes-iolimit-rpm" title="#opt-nginx-ingress-kubernetes-iolimit-rpm">`nginx.ingress.kubernetes.io/limit-rpm`</a> | Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. |
| <a id="opt-nginx-ingress-kubernetes-iolimit-burst-multiplier" href="#opt-nginx-ingress-kubernetes-iolimit-burst-multiplier" title="#opt-nginx-ingress-kubernetes-iolimit-burst-multiplier">`nginx.ingress.kubernetes.io/limit-burst-multiplier`</a> | Default to a multiplier of 5 if the configured value is less than 1. Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. |
| <a id="opt-nginx-ingress-kubernetes-iolimit-connections" href="#opt-nginx-ingress-kubernetes-iolimit-connections" title="#opt-nginx-ingress-kubernetes-iolimit-connections">`nginx.ingress.kubernetes.io/limit-connections`</a> | Exceeding the limit returns `429 Too Many Requests` instead of NGINX's default `503 Service Unavailable`. The concurrent connection limit is evaluated per client IP address. Values less than or equal to `0` are safely ignored. |
### Buffering
@ -454,7 +455,6 @@ In practice, Traefik is slightly more lenient under bursty load, as it smooths o
| <a id="opt-nginx-ingress-kubernetes-iolimit-rate-after" href="#opt-nginx-ingress-kubernetes-iolimit-rate-after" title="#opt-nginx-ingress-kubernetes-iolimit-rate-after">`nginx.ingress.kubernetes.io/limit-rate-after`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-rate" href="#opt-nginx-ingress-kubernetes-iolimit-rate" title="#opt-nginx-ingress-kubernetes-iolimit-rate">`nginx.ingress.kubernetes.io/limit-rate`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-whitelist" href="#opt-nginx-ingress-kubernetes-iolimit-whitelist" title="#opt-nginx-ingress-kubernetes-iolimit-whitelist">`nginx.ingress.kubernetes.io/limit-whitelist`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-connections" href="#opt-nginx-ingress-kubernetes-iolimit-connections" title="#opt-nginx-ingress-kubernetes-iolimit-connections">`nginx.ingress.kubernetes.io/limit-connections`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioglobal-rate-limit" href="#opt-nginx-ingress-kubernetes-ioglobal-rate-limit" title="#opt-nginx-ingress-kubernetes-ioglobal-rate-limit">`nginx.ingress.kubernetes.io/global-rate-limit`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioglobal-rate-limit-window" href="#opt-nginx-ingress-kubernetes-ioglobal-rate-limit-window" title="#opt-nginx-ingress-kubernetes-ioglobal-rate-limit-window">`nginx.ingress.kubernetes.io/global-rate-limit-window`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioglobal-rate-limit-key" href="#opt-nginx-ingress-kubernetes-ioglobal-rate-limit-key" title="#opt-nginx-ingress-kubernetes-ioglobal-rate-limit-key">`nginx.ingress.kubernetes.io/global-rate-limit-key`</a> | |

View File

@ -89,6 +89,7 @@ type IngressConfig struct {
LimitRPM *int `annotation:"nginx.ingress.kubernetes.io/limit-rpm"`
LimitRPS *int `annotation:"nginx.ingress.kubernetes.io/limit-rps"`
LimitBurstMultiplier *int `annotation:"nginx.ingress.kubernetes.io/limit-burst-multiplier"`
LimitConnections *int `annotation:"nginx.ingress.kubernetes.io/limit-connections"`
CustomHeaders *string `annotation:"nginx.ingress.kubernetes.io/custom-headers"`
UpstreamVHost *string `annotation:"nginx.ingress.kubernetes.io/upstream-vhost"`

View File

@ -0,0 +1,20 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-limit-connections
namespace: default
annotations:
nginx.ingress.kubernetes.io/limit-connections: "10"
spec:
ingressClassName: nginx
rules:
- host: whoami.localhost
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: whoami
port:
number: 80

View File

@ -14232,6 +14232,105 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Limit connections",
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-limit-connections.yml",
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-ingress-with-limit-connections-rule-0-path-0": {
EntryPoints: []string{"http"},
Rule: `Host("whoami.localhost") && PathPrefix("/")`,
RuleSyntax: "default",
Middlewares: []string{"default-ingress-with-limit-connections-rule-0-path-0-limit-connections", "default-ingress-with-limit-connections-rule-0-path-0-retry"},
Service: "default-ingress-with-limit-connections-whoami-80",
Observability: &dynamic.RouterObservabilityConfig{
Metadata: &dynamic.ObservabilityMetadata{
Ingress: &dynamic.KubernetesIngressMetadata{
Namespace: "default",
IngressName: "ingress-with-limit-connections",
ServiceName: "whoami",
ServicePort: "80",
},
},
},
},
"default-ingress-with-limit-connections-rule-0-path-0-tls": {
EntryPoints: []string{"https"},
Rule: `Host("whoami.localhost") && PathPrefix("/")`,
RuleSyntax: "default",
Middlewares: []string{"default-ingress-with-limit-connections-rule-0-path-0-tls-limit-connections", "default-ingress-with-limit-connections-rule-0-path-0-tls-retry"},
Service: "default-ingress-with-limit-connections-whoami-80",
TLS: &dynamic.RouterTLSConfig{},
Observability: &dynamic.RouterObservabilityConfig{
Metadata: &dynamic.ObservabilityMetadata{
Ingress: &dynamic.KubernetesIngressMetadata{
Namespace: "default",
IngressName: "ingress-with-limit-connections",
ServiceName: "whoami",
ServicePort: "80",
},
},
},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-limit-connections-rule-0-path-0-retry": {Retry: &dynamic.Retry{Attempts: 3}},
"default-ingress-with-limit-connections-rule-0-path-0-tls-retry": {Retry: &dynamic.Retry{Attempts: 3}},
"default-ingress-with-limit-connections-rule-0-path-0-limit-connections": {
InFlightReq: &dynamic.InFlightReq{
Amount: 10,
SourceCriterion: &dynamic.SourceCriterion{
IPStrategy: &dynamic.IPStrategy{},
},
},
},
"default-ingress-with-limit-connections-rule-0-path-0-tls-limit-connections": {
InFlightReq: &dynamic.InFlightReq{
Amount: 10,
SourceCriterion: &dynamic.SourceCriterion{
IPStrategy: &dynamic.IPStrategy{},
},
},
},
},
Services: map[string]*dynamic.Service{
"unavailable-service": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
"default-ingress-with-limit-connections-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{{URL: "http://10.10.0.1:80"}, {URL: "http://10.10.0.2:80"}},
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{FlushInterval: dynamic.DefaultFlushInterval},
ServersTransport: "default-ingress-with-limit-connections",
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-limit-connections": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{DialTimeout: ptypes.Duration(60 * time.Second), ReadTimeout: ptypes.Duration(60 * time.Second), WriteTimeout: ptypes.Duration(60 * time.Second), IdleConnTimeout: ptypes.Duration(60 * time.Second)},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Use Regex with Prefix pathType and StrictValidatePathType enabled",
paths: []string{

View File

@ -34,6 +34,7 @@ func (p *Provider) buildMiddlewares(ctx context.Context, loc *location, hostname
p.buildRewriteTarget(loc)
p.buildUpstreamVhost(loc)
p.buildRateLimit(loc)
p.buildLimitConnections(loc)
p.buildAuthTLSPassCert(loc)
p.buildCustomHeaders(loc)
p.buildSnippetAuth(loc)
@ -310,6 +311,20 @@ func (p *Provider) buildRateLimit(loc *location) {
}
}
func (p *Provider) buildLimitConnections(loc *location) {
limit := ptr.Deref(loc.Config.LimitConnections, 0)
if limit <= 0 {
return
}
loc.LimitConnections = &dynamic.InFlightReq{
Amount: int64(limit),
SourceCriterion: &dynamic.SourceCriterion{
IPStrategy: &dynamic.IPStrategy{},
},
}
}
func (p *Provider) buildAuthTLSPassCert(loc *location) {
if !ptr.Deref(loc.Config.AuthTLSPassCertificateToUpstream, false) || loc.Config.AuthTLSSecret == nil {
return

View File

@ -193,6 +193,9 @@ type location struct {
// RateLimitRPS, if non-nil, applies a per-second request rate limit.
RateLimitRPS *dynamic.RateLimit
// LimitConnections, if non-nil, caps concurrent in-flight requests per source IP.
LimitConnections *dynamic.InFlightReq
// AuthTLSPassCert, if non-nil, forwards the client TLS certificate to the backend.
AuthTLSPassCert *dynamic.AuthTLSPassCertificateToUpstream

View File

@ -501,6 +501,12 @@ func (p *Provider) applyMiddlewares(mc *model, loc *location, routerKey string,
rt.Middlewares = append(rt.Middlewares, name)
}
if loc.LimitConnections != nil {
name := routerKey + "-limit-connections"
conf.HTTP.Middlewares[name] = &dynamic.Middleware{InFlightReq: loc.LimitConnections}
rt.Middlewares = append(rt.Middlewares, name)
}
if loc.AuthTLSPassCert != nil && rt.TLS != nil {
name := routerKey + "-pass-certificate-to-upstream"
conf.HTTP.Middlewares[name] = &dynamic.Middleware{AuthTLSPassCertificateToUpstream: loc.AuthTLSPassCert}