Skip to content

Commit

Permalink
Rate limiting for frontends
Browse files Browse the repository at this point in the history
  • Loading branch information
bparli authored and traefiker committed Sep 9, 2017
1 parent 9fba37b commit d54417a
Show file tree
Hide file tree
Showing 16 changed files with 1,410 additions and 3 deletions.
29 changes: 29 additions & 0 deletions docs/basics.md
Expand Up @@ -292,6 +292,35 @@ In this example, traffic routed through the first frontend will have the `X-Fram

The detailed documentation for those security headers can be found in [unrolled/secure](https://github.com/unrolled/secure#available-options).

#### Rate limiting

Rate limiting can be configured per frontend.
Multiple sets of rates can be added to each frontend, but the time periods must be unique.

```toml
[frontends]
[frontends.frontend1]
passHostHeader = true
entrypoints = ["http"]
backend = "backend1"
[frontends.frontend1.routes.test_1]
rule = "Path:/"
[frontends.frontend1.ratelimit]
extractorfunc = "client.ip"
[frontends.frontend1.ratelimit.rateset.rateset1]
period = "10s"
average = 100
burst = 200
[frontends.frontend1.ratelimit.rateset.rateset2]
period = "3s"
average = 5
burst = 10
```

In the above example, frontend1 is configured to limit requests by the client's ip address.
An average of 5 requests every 3 seconds is allowed and an average of 100 requests every 10 seconds.
These can "burst" up to 10 and 200 in each period respectively.

### Backends

A backend is responsible to load-balance the traffic coming from one or more frontends to a set of http servers.
Expand Down
9 changes: 7 additions & 2 deletions glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion glide.yaml
Expand Up @@ -22,6 +22,7 @@ import:
- roundrobin
- stream
- utils
- ratelimit
- package: github.com/urfave/negroni
version: 490e6a555d47ca891a89a150d0c1ef3922dfffe9
- package: github.com/containous/staert
Expand Down Expand Up @@ -79,9 +80,9 @@ import:
vcs: git
- package: github.com/abbot/go-http-auth
- package: github.com/NYTimes/gziphandler
version: ^v1002.0.0
repo: https://github.com/containous/gziphandler.git
vcs: git
version: ^v1002.0.0
- package: github.com/docker/leadership
- package: github.com/satori/go.uuid
version: ^1.1.0
Expand Down
30 changes: 30 additions & 0 deletions integration/fixtures/ratelimit/simple.toml
@@ -0,0 +1,30 @@
defaultEntryPoints = ["http"]

logLevel = "DEBUG"

[entryPoints]
[entryPoints.http]
address = ":80"

[file]

[backends]
[backends.backend1]
[backends.backend1.servers.server1]
url = "http://{{.Server1}}:80"
[frontends]
[frontends.frontend1]
passHostHeader = true
backend = "backend1"
[frontends.frontend1.routes.test_1]
rule = "Path:/"
[frontends.frontend1.ratelimit]
extractorfunc = "client.ip"
[frontends.frontend1.ratelimit.rateset.rateset1]
period = "60s"
average = 4
burst = 5
[frontends.frontend1.ratelimit.rateset.rateset2]
period = "3s"
average = 1
burst = 2
1 change: 1 addition & 0 deletions integration/integration_test.go
Expand Up @@ -39,6 +39,7 @@ func init() {
check.Suite(&LogRotationSuite{})
check.Suite(&MarathonSuite{})
check.Suite(&MesosSuite{})
check.Suite(&RateLimitSuite{})
check.Suite(&SimpleSuite{})
check.Suite(&TimeoutSuite{})
check.Suite(&WebsocketSuite{})
Expand Down
61 changes: 61 additions & 0 deletions integration/ratelimit_test.go
@@ -0,0 +1,61 @@
package integration

import (
"net/http"
"os"
"time"

"github.com/containous/traefik/integration/try"
"github.com/go-check/check"
checker "github.com/vdemeester/shakers"
)

type RateLimitSuite struct {
BaseSuite
ServerIP string
}

func (s *RateLimitSuite) SetUpSuite(c *check.C) {
s.createComposeProject(c, "ratelimit")
s.composeProject.Start(c)

s.ServerIP = s.composeProject.Container(c, "nginx1").NetworkSettings.IPAddress
}

func (s *RateLimitSuite) TestSimpleConfiguration(c *check.C) {
file := s.adaptFile(c, "fixtures/ratelimit/simple.toml", struct {
Server1 string
}{s.ServerIP})
defer os.Remove(file)

cmd, _ := s.cmdTraefik(withConfigFile(file))
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()

err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)
err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusTooManyRequests))
c.Assert(err, checker.IsNil)

// sleep for 4 seconds to be certain the configured time period has elapsed
// then test another request and verify a 200 status code
time.Sleep(4 * time.Second)
err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)

// continue requests at 3 second intervals to test the other rate limit time period
time.Sleep(3 * time.Second)
err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)

time.Sleep(3 * time.Second)
err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusOK))
c.Assert(err, checker.IsNil)

time.Sleep(3 * time.Second)
err = try.GetRequest("http://127.0.0.1:80/", 500*time.Millisecond, try.StatusCodeIs(http.StatusTooManyRequests))
c.Assert(err, checker.IsNil)
}
2 changes: 2 additions & 0 deletions integration/resources/compose/ratelimit.yml
@@ -0,0 +1,2 @@
nginx1:
image: nginx:alpine
25 changes: 25 additions & 0 deletions server/server.go
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/vulcand/oxy/cbreaker"
"github.com/vulcand/oxy/connlimit"
"github.com/vulcand/oxy/forward"
"github.com/vulcand/oxy/ratelimit"
"github.com/vulcand/oxy/roundrobin"
"github.com/vulcand/oxy/utils"
"golang.org/x/net/http2"
Expand Down Expand Up @@ -891,6 +892,15 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf
}
}

if frontend.RateLimit != nil && len(frontend.RateLimit.RateSet) > 0 {
lb, err = server.buildRateLimiter(lb, frontend.RateLimit)
if err != nil {
log.Errorf("Error creating rate limiter: %v", err)
log.Errorf("Skipping frontend %s...", frontendName)
continue frontend
}
}

maxConns := config.Backends[frontend.Backend].MaxConn
if maxConns != nil && maxConns.Amount != 0 {
extractFunc, err := utils.NewExtractor(maxConns.ExtractorFunc)
Expand Down Expand Up @@ -1189,6 +1199,21 @@ func stopMetricsClients() {
metrics.StopStatsd()
}

func (server *Server) buildRateLimiter(handler http.Handler, rlConfig *types.RateLimit) (http.Handler, error) {
extractFunc, err := utils.NewExtractor(rlConfig.ExtractorFunc)
if err != nil {
return nil, err
}
log.Debugf("Creating load-balancer rate limiter")
rateSet := ratelimit.NewRateSet()
for _, rate := range rlConfig.RateSet {
if err := rateSet.Add(time.Duration(rate.Period), rate.Average, rate.Burst); err != nil {
return nil, err
}
}
return ratelimit.New(handler, extractFunc, rateSet, ratelimit.Logger(oxyLogger))
}

func (server *Server) buildRetryMiddleware(handler http.Handler, globalConfig configuration.GlobalConfiguration, countServers int, backendName string) http.Handler {
retryListeners := middlewares.RetryListeners{}
if server.metricsRegistry.IsEnabled() {
Expand Down
15 changes: 15 additions & 0 deletions types/types.go
Expand Up @@ -12,6 +12,7 @@ import (
"io/ioutil"
"os"

"github.com/containous/flaeg"
"github.com/containous/traefik/log"
"github.com/docker/libkv/store"
"github.com/ryanuber/go-glob"
Expand Down Expand Up @@ -67,6 +68,19 @@ type ErrorPage struct {
Query string `json:"query,omitempty"`
}

// Rate holds a rate limiting configuration for a specific time period
type Rate struct {
Period flaeg.Duration `json:"period,omitempty"`
Average int64 `json:"average,omitempty"`
Burst int64 `json:"burst,omitempty"`
}

// RateLimit holds a rate limiting configuration for a given frontend
type RateLimit struct {
RateSet map[string]*Rate `json:"rateset,omitempty"`
ExtractorFunc string `json:"extractorFunc,omitempty"`
}

// Headers holds the custom header configuration
type Headers struct {
CustomRequestHeaders map[string]string `json:"customRequestHeaders,omitempty"`
Expand Down Expand Up @@ -131,6 +145,7 @@ type Frontend struct {
WhitelistSourceRange []string `json:"whitelistSourceRange,omitempty"`
Headers Headers `json:"headers,omitempty"`
Errors map[string]ErrorPage `json:"errors,omitempty"`
RateLimit *RateLimit `json:"ratelimit,omitempty"`
}

// LoadBalancerMethod holds the method of load balancing to use.
Expand Down

0 comments on commit d54417a

Please sign in to comment.