diff --git a/docs/basics.md b/docs/basics.md index 113d3574e..749f77d1d 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -70,7 +70,7 @@ Frontends can be defined using the following rules: - `PathPrefixStrip`: Same as `PathPrefix` but strip the given prefix from the request URL's Path. You can optionally enable `passHostHeader` to forward client `Host` header to the backend. - + Here is an example of frontends definition: ```toml @@ -107,7 +107,7 @@ A circuit breaker can also be applied to a backend, preventing high loads on fai Initial state is Standby. CB observes the statistics and does not modify the request. In case if condition matches, CB enters Tripped state, where it responds with predefines code or redirects to another frontend. Once Tripped timer expires, CB enters Recovering state and resets all stats. -In case if the condition does not match and recovery timer expries, CB enters Standby state. +In case if the condition does not match and recovery timer expires, CB enters Standby state. It can be configured using: @@ -120,6 +120,26 @@ For example: - `LatencyAtQuantileMS(50.0) > 50`: watch latency at quantile in milliseconds. - `ResponseCodeRatio(500, 600, 0, 600) > 0.5`: ratio of response codes in range [500-600) to [0-600) +To proactively prevent backends from being overwhelmed with high load, a maximum connection limit can +also be applied to each backend. + +Maximum connections can be configured by specifying an integer value for `maxconn.amount` and +`maxconn.extractorfunc` which is a strategy used to determine how to categorize requests in order to +evaluate the maximum connections. + +For example: +```toml +[backends] + [backends.backend1] + [backends.backend1.maxconn] + amount = 10 + extractorfunc = "request.host" +``` + +- `backend1` will return `HTTP code 429 Too Many Requests` if there are already 10 requests in progress for the same Host header. +- Another possible value for `extractorfunc` is `client.ip` which will categorize requests based on client source ip. +- Lastly `extractorfunc` can take the value of `request.header.ANY_HEADER` which will categorize requests based on `ANY_HEADER` that you provide. + ## Servers Servers are simply defined using a `URL`. You can also apply a custom `weight` to each server (this will be used by load-balacning). diff --git a/docs/toml.md b/docs/toml.md index 792624f64..d8334c7da 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -138,7 +138,7 @@ # storageFile = "acme.json" # Entrypoint to proxy acme challenge to. -# WARNING, must point to an entrypoint on port 443 +# WARNING, must point to an entrypoint on port 443 # # Required # @@ -218,6 +218,9 @@ defaultEntryPoints = ["http", "https"] url = "http://172.17.0.3:80" weight = 1 [backends.backend2] + [backends.backend1.maxconn] + amount = 10 + extractorfunc = "request.host" [backends.backend2.LoadBalancer] method = "drr" [backends.backend2.servers.server1] @@ -281,6 +284,9 @@ filename = "rules.toml" url = "http://172.17.0.3:80" weight = 1 [backends.backend2] + [backends.backend1.maxconn] + amount = 10 + extractorfunc = "request.host" [backends.backend2.LoadBalancer] method = "drr" [backends.backend2.servers.server1] @@ -851,6 +857,8 @@ The Keys-Values structure should look (using `prefix = "/traefik"`): | Key | Value | |-----------------------------------------------------|------------------------| +| `/traefik/backends/backend2/maxconn/amount` | `10` | +| `/traefik/backends/backend2/maxconn/extractorfunc` | `request.host` | | `/traefik/backends/backend2/loadbalancer/method` | `drr` | | `/traefik/backends/backend2/servers/server1/url` | `http://172.17.0.4:80` | | `/traefik/backends/backend2/servers/server1/weight` | `1` | @@ -1006,4 +1014,4 @@ entryPoint = "https" entrypoints = ["http", "https"] # overrides defaultEntryPoints backend = "backend2" rule = "Path:/test" -``` \ No newline at end of file +``` diff --git a/provider/provider_test.go b/provider/provider_test.go index 490a82a7b..70da81893 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -168,3 +168,41 @@ func TestReplace(t *testing.T) { } } } + +func TestGetConfigurationReturnsCorrectMaxConnConfiguration(t *testing.T) { + templateFile, err := ioutil.TempFile("", "provider-configuration") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(templateFile.Name()) + data := []byte(`[backends] + [backends.backend1] + [backends.backend1.maxconn] + amount = 10 + extractorFunc = "request.host"`) + err = ioutil.WriteFile(templateFile.Name(), data, 0700) + if err != nil { + t.Fatal(err) + } + + provider := &myProvider{ + BaseProvider{ + Filename: templateFile.Name(), + }, + } + configuration, err := provider.getConfiguration(templateFile.Name(), nil, nil) + if err != nil { + t.Fatalf("Shouldn't have error out, got %v", err) + } + if configuration == nil { + t.Fatalf("Configuration should not be nil, but was") + } + + if configuration.Backends["backend1"].MaxConn.Amount != 10 { + t.Fatalf("Configuration did not parse MaxConn.Amount properly") + } + + if configuration.Backends["backend1"].MaxConn.ExtractorFunc != "request.host" { + t.Fatalf("Configuration did not parse MaxConn.ExtractorFunc properly") + } +} diff --git a/server.go b/server.go index c9dbbc551..a36892b4d 100644 --- a/server.go +++ b/server.go @@ -22,9 +22,11 @@ import ( log "github.com/Sirupsen/logrus" "github.com/codegangsta/negroni" "github.com/containous/oxy/cbreaker" + "github.com/containous/oxy/connlimit" "github.com/containous/oxy/forward" "github.com/containous/oxy/roundrobin" "github.com/containous/oxy/stream" + "github.com/containous/oxy/utils" "github.com/containous/traefik/middlewares" "github.com/containous/traefik/provider" "github.com/containous/traefik/safe" @@ -423,6 +425,18 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo } } } + maxConns := configuration.Backends[frontend.Backend].MaxConn + if maxConns != nil && maxConns.Amount != 0 { + extractFunc, err := utils.NewExtractor(maxConns.ExtractorFunc) + if err != nil { + return nil, err + } + log.Debugf("Creating loadd-balancer connlimit") + lb, err = connlimit.New(lb, extractFunc, maxConns.Amount, connlimit.Logger(oxyLogger)) + if err != nil { + return nil, err + } + } // retry ? if globalConfiguration.Retry != nil { retries := len(configuration.Backends[frontend.Backend].Servers) diff --git a/templates/kv.tmpl b/templates/kv.tmpl index 168ba0e5c..edb641658 100644 --- a/templates/kv.tmpl +++ b/templates/kv.tmpl @@ -17,6 +17,16 @@ method = "{{$loadBalancer}}" {{end}} +{{$maxConnAmt := Get "" . "/maxconn/" "amount"}} +{{$maxConnExtractorFunc := Get "" . "/maxconn/" "extractorfunc"}} +{{with $maxConnAmt}} +{{with $maxConnExtractorFunc}} +[backends.{{Last $backend}}.maxConn] + amount = {{$maxConnAmt}} + extractorFunc = "{{$maxConnExtractorFunc}}" +{{end}} +{{end}} + {{range $servers}} [backends.{{Last $backend}}.servers.{{Last .}}] url = "{{Get "" . "/url"}}" diff --git a/types/types.go b/types/types.go index 2168ac49f..44661e836 100644 --- a/types/types.go +++ b/types/types.go @@ -10,6 +10,13 @@ type Backend struct { Servers map[string]Server `json:"servers,omitempty"` CircuitBreaker *CircuitBreaker `json:"circuitBreaker,omitempty"` LoadBalancer *LoadBalancer `json:"loadBalancer,omitempty"` + MaxConn *MaxConn `json:"maxConn,omitempty"` +} + +// MaxConn holds maximum connection configuraiton +type MaxConn struct { + Amount int64 `json:"amount,omitempty"` + ExtractorFunc string `json:"extractorFunc,omitempty"` } // LoadBalancer holds load balancing configuration.