diff --git a/configuration.go b/configuration.go index a50b97701..25c412848 100644 --- a/configuration.go +++ b/configuration.go @@ -1,5 +1,10 @@ package main +import ( + "errors" + "strings" +) + type GlobalConfiguration struct { Port string GraceTimeOut int64 @@ -27,7 +32,17 @@ func NewGlobalConfiguration() *GlobalConfiguration { } type Backend struct { - Servers map[string]Server + Servers map[string]Server + CircuitBreaker *CircuitBreaker + LoadBalancer *LoadBalancer +} + +type LoadBalancer struct { + Method string +} + +type CircuitBreaker struct { + Expression string } type Server struct { @@ -46,6 +61,34 @@ type Frontend struct { } type Configuration struct { - Backends map[string]Backend - Frontends map[string]Frontend + Backends map[string]*Backend + Frontends map[string]*Frontend } + +// Load Balancer Method +type LoadBalancerMethod uint8 + +const ( + // wrr (default) = Weighted Round Robin + wrr LoadBalancerMethod = iota + // drr = Dynamic Round Robin + drr +) + +var loadBalancerMethodNames = []string{ + "wrr", + "drr", +} + +func NewLoadBalancerMethod(loadBalancer *LoadBalancer) (LoadBalancerMethod, error) { + if loadBalancer != nil { + for i, name := range loadBalancerMethodNames { + if strings.EqualFold(name, loadBalancer.Method) { + return LoadBalancerMethod(i), nil + } + } + } + return wrr, ErrInvalidLoadBalancerMethod +} + +var ErrInvalidLoadBalancerMethod = errors.New("Invalid method, using default") diff --git a/consul.go b/consul.go index d43c9b6e6..849fefc97 100644 --- a/consul.go +++ b/consul.go @@ -48,6 +48,8 @@ var ConsulFuncMap = template.FuncMap{ if err != nil { log.Error("Error getting key ", joinedKeys, err) return "" + } else if keyPair == nil { + return "" } return string(keyPair.Value) }, @@ -75,28 +77,34 @@ func (provider *ConsulProvider) Provide(configurationChan chan<- *Configuration) consulClient, _ := api.NewClient(config) provider.consulClient = consulClient if provider.Watch { - var waitIndex uint64 keypairs, meta, err := consulClient.KV().Keys("", "", nil) - if keypairs == nil && err == nil { - log.Error("Key was not found.") + if keypairs == nil { + log.Error("Key was not found") + } else if err != nil { + log.Error("Error connecting to consul %s", err) + } else { + var waitIndex uint64 + waitIndex = meta.LastIndex + go func() { + for { + opts := api.QueryOptions{ + WaitIndex: waitIndex, + } + keypairs, meta, err := consulClient.KV().Keys("", "", &opts) + if keypairs == nil { + log.Error("Key was not found") + } else if err != nil { + log.Error("Error connecting to consul %s", err) + } else { + waitIndex = meta.LastIndex + configuration := provider.loadConsulConfig() + if configuration != nil { + configurationChan <- configuration + } + } + } + }() } - waitIndex = meta.LastIndex - go func() { - for { - opts := api.QueryOptions{ - WaitIndex: waitIndex, - } - keypairs, meta, err := consulClient.KV().Keys("", "", &opts) - if keypairs == nil && err == nil { - log.Error("Key was not found.") - } - waitIndex = meta.LastIndex - configuration := provider.loadConsulConfig() - if configuration != nil { - configurationChan <- configuration - } - } - }() } configuration := provider.loadConsulConfig() configurationChan <- configuration diff --git a/docs/index.md b/docs/index.md index 12dddae30..b8a0e4868 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,12 +39,21 @@ Frontends can be defined using the following rules: ### HTTP Backends A backend is responsible to load-balance the traffic coming from one or more frontends to a set of http servers. -Various types of load-balancing is supported: +Various methods of load-balancing is supported: -* Weighted round robin -* Rebalancer: increases weights on servers that perform better than others. It also rolls back to original weights if the servers have changed. +* ```wrr```: Weighted Round Robin +* ```drr```: Dynamic Round Robin: increases weights on servers that perform better than others. It also rolls back to original weights if the servers have changed. A circuit breaker can also be applied to a backend, preventing high loads on failing servers. +It can be configured using: + +* Methods: ```LatencyAtQuantileMS```, ```NetworkErrorRatio```, ```ResponseCodeRatio``` +* Operators: ```AND```, ```OR```, ```EQ```, ```NEQ```, ```LT```, ```LE```, ```GT```, ```GE``` + +For example: +* ```NetworkErrorRatio() > 0.5``` +* ```LatencyAtQuantileMS(50.0) > 50``` +* ```ResponseCodeRatio(500, 600, 0, 600) > 0.5``` ## Global configuration @@ -115,6 +124,8 @@ logLevel = "DEBUG" # rules [backends] [backends.backend1] + [backends.backend1.circuitbreaker] + expression = "NetworkErrorRatio() > 0.5" [backends.backend1.servers.server1] url = "http://172.17.0.2:80" weight = 10 @@ -122,21 +133,27 @@ logLevel = "DEBUG" url = "http://172.17.0.3:80" weight = 1 [backends.backend2] + [backends.backend2.LoadBalancer] + method = "drr" [backends.backend2.servers.server1] url = "http://172.17.0.4:80" weight = 1 + [backends.backend2.servers.server2] + url = "http://172.17.0.5:80" + weight = 2 + +[frontends] + [frontends.frontend1] + backend = "backend2" + [frontends.frontend1.routes.test_1] + rule = "Host" + value = "test.localhost" + [frontends.frontend2] + backend = "backend1" + [frontends.frontend2.routes.test_2] + rule = "Path" + value = "/test" - [frontends] - [frontends.frontend1] - backend = "backend2" - [frontends.frontend1.routes.test_1] - rule = "Host" - value = "test.localhost" - [frontends.frontend2] - backend = "backend1" - [frontends.frontend2.routes.test_2] - rule = "Path" - value = "/test" ``` @@ -156,6 +173,8 @@ filename = "rules.toml" # rules.toml [backends] [backends.backend1] + [backends.backend1.circuitbreaker] + expression = "NetworkErrorRatio() > 0.5" [backends.backend1.servers.server1] url = "http://172.17.0.2:80" weight = 10 @@ -163,9 +182,14 @@ filename = "rules.toml" url = "http://172.17.0.3:80" weight = 1 [backends.backend2] + [backends.backend2.LoadBalancer] + method = "drr" [backends.backend2.servers.server1] url = "http://172.17.0.4:80" weight = 1 + [backends.backend2.servers.server2] + url = "http://172.17.0.5:80" + weight = 2 [frontends] [frontends.frontend1] @@ -178,6 +202,7 @@ filename = "rules.toml" [frontends.frontend2.routes.test_2] rule = "Path" value = "/test" + ``` If you want Træfɪk to watch file changes automatically, just add: diff --git a/middlewares/cbreaker.go b/middlewares/cbreaker.go index 371146f93..6ed02b28e 100644 --- a/middlewares/cbreaker.go +++ b/middlewares/cbreaker.go @@ -13,8 +13,8 @@ type CircuitBreaker struct { circuitBreaker *cbreaker.CircuitBreaker } -func NewCircuitBreaker(next http.Handler, options ...cbreaker.CircuitBreakerOption) *CircuitBreaker { - circuitBreaker, _ := cbreaker.New(next, "NetworkErrorRatio() > 0.5", options...) +func NewCircuitBreaker(next http.Handler, expression string, options ...cbreaker.CircuitBreakerOption) *CircuitBreaker { + circuitBreaker, _ := cbreaker.New(next, expression, options...) return &CircuitBreaker{circuitBreaker} } diff --git a/providerTemplates/consul.tmpl b/providerTemplates/consul.tmpl index c38b23c0a..e535e414e 100644 --- a/providerTemplates/consul.tmpl +++ b/providerTemplates/consul.tmpl @@ -4,6 +4,19 @@ {{range $backends}} {{$backend := .}} {{$servers := "servers/" | List $backend }} + +{{$circuitBreaker := Get . "circuitbreaker/" "expression"}} +{{with $circuitBreaker}} +[backends.{{Last $backend}}.circuitBreaker] + expression = "{{$circuitBreaker}}" +{{end}} + +{{$loadBalancer := Get . "loadbalancer/" "method"}} +{{with $loadBalancer}} +[backends.{{Last $backend}}.loadBalancer] + method = "{{$loadBalancer}}" +{{end}} + {{range $servers}} [backends.{{Last $backend}}.servers.{{Last .}}] url = "{{Get . "/url"}}" @@ -12,13 +25,13 @@ {{end}} [frontends]{{range $frontends}} - {{$frontend := Last .}} - [frontends.{{$frontend}}] - backend = "{{Get . "/backend"}}" - {{$routes := "routes/" | List .}} - {{range $routes}} - [frontends.{{$frontend}}.routes.{{Last .}}] - rule = "{{Get . "/rule"}}" - value = "{{Get . "/value"}}" - {{end}} -{{end}} \ No newline at end of file + {{$frontend := Last .}} + [frontends.{{$frontend}}] + backend = "{{Get . "/backend"}}" + {{$routes := "routes/" | List .}} + {{range $routes}} + [frontends.{{$frontend}}.routes.{{Last .}}] + rule = "{{Get . "/rule"}}" + value = "{{Get . "/value"}}" + {{end}} +{{end}} diff --git a/templates/configuration.tmpl b/templates/configuration.tmpl index d3768e9c3..602c16e38 100644 --- a/templates/configuration.tmpl +++ b/templates/configuration.tmpl @@ -31,7 +31,6 @@
{{$keyFrontends}}
- @@ -60,6 +59,18 @@ {{range $keyBackends, $valueBackends := .Configuration.Backends}}
{{$keyBackends}}
+
+ {{with $valueBackends.LoadBalancer}} + + Load Balancer: {{.Method}} + + {{end}} + {{with $valueBackends.CircuitBreaker}} + + Circuit Breaker: {{.Expression}} + + {{end}} +
diff --git a/tests/consul-config.sh b/tests/consul-config.sh new file mode 100755 index 000000000..0d0c1f5a9 --- /dev/null +++ b/tests/consul-config.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# backend 1 +curl -i -H "Accept: application/json" -X PUT -d "NetworkErrorRatio() > 0.5" http://localhost:8500/v1/kv/backends/backend1/circuitbreaker/expression +curl -i -H "Accept: application/json" -X PUT -d "http://172.17.0.2:80" http://localhost:8500/v1/kv/backends/backend1/servers/server1/url +curl -i -H "Accept: application/json" -X PUT -d "10" http://localhost:8500/v1/kv/backends/backend1/servers/server1/weight +curl -i -H "Accept: application/json" -X PUT -d "http://172.17.0.3:80" http://localhost:8500/v1/kv/backends/backend1/servers/server2/url +curl -i -H "Accept: application/json" -X PUT -d "1" http://localhost:8500/v1/kv/backends/backend1/servers/server2/weight + +# backend 2 +curl -i -H "Accept: application/json" -X PUT -d "drr" http://localhost:8500/v1/kv/backends/backend2/loadbalancer/method +curl -i -H "Accept: application/json" -X PUT -d "http://172.17.0.4:80" http://localhost:8500/v1/kv/backends/backend2/servers/server1/url +curl -i -H "Accept: application/json" -X PUT -d "1" http://localhost:8500/v1/kv/backends/backend2/servers/server1/weight +curl -i -H "Accept: application/json" -X PUT -d "http://172.17.0.5:80" http://localhost:8500/v1/kv/backends/backend2/servers/server2/url +curl -i -H "Accept: application/json" -X PUT -d "2" http://localhost:8500/v1/kv/backends/backend2/servers/server2/weight + +# frontend 1 +curl -i -H "Accept: application/json" -X PUT -d "backend2" http://localhost:8500/v1/kv/frontends/frontend1/backend +curl -i -H "Accept: application/json" -X PUT -d "Host" http://localhost:8500/v1/kv/frontends/frontend1/routes/test_1/rule +curl -i -H "Accept: application/json" -X PUT -d "test.localhost" http://localhost:8500/v1/kv/frontends/frontend1/routes/test_1/value + +# frontend 2 +curl -i -H "Accept: application/json" -X PUT -d "backend1" http://localhost:8500/v1/kv/frontends/frontend2/backend +curl -i -H "Accept: application/json" -X PUT -d "Path" http://localhost:8500/v1/kv/frontends/frontend2/routes/test_2/rule +curl -i -H "Accept: application/json" -X PUT -d "/test" http://localhost:8500/v1/kv/frontends/frontend2/routes/test_2/value diff --git a/traefik.go b/traefik.go index d4214773d..1a0b8ede7 100644 --- a/traefik.go +++ b/traefik.go @@ -225,27 +225,54 @@ func LoadConfig(configuration *Configuration, globalConfiguration *GlobalConfigu } if backends[frontend.Backend] == nil { log.Debugf("Creating backend %s", frontend.Backend) - lb, _ := roundrobin.New(fwd) - rb, _ := roundrobin.NewRebalancer(lb, roundrobin.RebalancerLogger(oxyLogger)) - for serverName, server := range configuration.Backends[frontend.Backend].Servers { - url, err := url.Parse(server.URL) - if err != nil { - return nil, err - } - log.Debugf("Creating server %s %s", serverName, url.String()) - rb.UpsertServer(url, roundrobin.Weight(server.Weight)) + var lb http.Handler + rr, _ := roundrobin.New(fwd) + lbMethod, err := NewLoadBalancerMethod(configuration.Backends[frontend.Backend].LoadBalancer) + if err != nil { + configuration.Backends[frontend.Backend].LoadBalancer = &LoadBalancer{Method: "wrr"} } - backends[frontend.Backend] = rb + switch lbMethod { + case drr: + log.Debugf("Creating load-balancer drr") + rebalancer, _ := roundrobin.NewRebalancer(rr, roundrobin.RebalancerLogger(oxyLogger)) + lb = rebalancer + for serverName, server := range configuration.Backends[frontend.Backend].Servers { + url, err := url.Parse(server.URL) + if err != nil { + return nil, err + } + log.Debugf("Creating server %s %s", serverName, url.String()) + rebalancer.UpsertServer(url, roundrobin.Weight(server.Weight)) + } + case wrr: + log.Debugf("Creating load-balancer wrr") + lb = rr + for serverName, server := range configuration.Backends[frontend.Backend].Servers { + url, err := url.Parse(server.URL) + if err != nil { + return nil, err + } + log.Debugf("Creating server %s %s", serverName, url.String()) + rr.UpsertServer(url, roundrobin.Weight(server.Weight)) + } + } + var negroni = negroni.New() + if configuration.Backends[frontend.Backend].CircuitBreaker != nil { + log.Debugf("Creating circuit breaker %s", configuration.Backends[frontend.Backend].CircuitBreaker.Expression) + negroni.Use(middlewares.NewCircuitBreaker(lb, configuration.Backends[frontend.Backend].CircuitBreaker.Expression, cbreaker.Logger(oxyLogger))) + } else { + negroni.UseHandler(lb) + } + backends[frontend.Backend] = negroni } else { log.Debugf("Reusing backend %s", frontend.Backend) } - // stream.New(backends[frontend.Backend], stream.Retry("IsNetworkError() && Attempts() <= " + strconv.Itoa(globalConfiguration.Replay)), stream.Logger(oxyLogger)) - var negroni = negroni.New() - negroni.Use(middlewares.NewCircuitBreaker(backends[frontend.Backend], cbreaker.Logger(oxyLogger))) - newRoute.Handler(negroni) + // stream.New(backends[frontend.Backend], stream.Retry("IsNetworkError() && Attempts() <= " + strconv.Itoa(globalConfiguration.Replay)), stream.Logger(oxyLogger)) + + newRoute.Handler(backends[frontend.Backend]) err := newRoute.GetError() if err != nil { - log.Error("Error building route ", err) + log.Error("Error building route: %s", err) } } return router, nil diff --git a/traefik.sample.toml b/traefik.sample.toml index 341bc9645..7b7de5422 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -187,7 +187,7 @@ # # Required # -# endpoint = "http://127.0.0.1:8500" +# endpoint = "127.0.0.1:8500" # Enable watch Consul changes # @@ -214,6 +214,8 @@ ################################################################ # [backends] # [backends.backend1] +# [backends.backend1.circuitbreaker] +# expression = "NetworkErrorRatio() > 0.5" # [backends.backend1.servers.server1] # url = "http://172.17.0.2:80" # weight = 10 @@ -221,6 +223,8 @@ # url = "http://172.17.0.3:80" # weight = 1 # [backends.backend2] +# [backends.backend2.LoadBalancer] +# method = "drr" # [backends.backend2.servers.server1] # url = "http://172.17.0.4:80" # weight = 1
Server