Configuration on cicuitbreakers and load balancers

This commit is contained in:
emile 2015-09-25 11:44:19 +02:00
parent 4fb25ca358
commit 0bdd537fb4
9 changed files with 222 additions and 66 deletions

View file

@ -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")

View file

@ -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

View file

@ -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```
## <a id="global"></a> 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:

View file

@ -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}
}

View file

@ -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}}
{{$frontend := Last .}}
[frontends.{{$frontend}}]
backend = "{{Get . "/backend"}}"
{{$routes := "routes/" | List .}}
{{range $routes}}
[frontends.{{$frontend}}.routes.{{Last .}}]
rule = "{{Get . "/rule"}}"
value = "{{Get . "/value"}}"
{{end}}
{{end}}

View file

@ -31,7 +31,6 @@
<div class="panel panel-primary">
<div class="panel-heading">{{$keyFrontends}}</div>
<div class="panel-body">
<!--<button type="button" class="btn btn-info">{{$valueFrontends.Backend}}</button>-->
<a class="btn btn-info" role="button" data-toggle="collapse" href="#{{$valueFrontends.Backend}}" aria-expanded="false">
{{$valueFrontends.Backend}}
</a>
@ -60,6 +59,18 @@
{{range $keyBackends, $valueBackends := .Configuration.Backends}}
<div class="panel panel-primary" id="{{$keyBackends}}">
<div class="panel-heading">{{$keyBackends}}</div>
<div class="panel-body">
{{with $valueBackends.LoadBalancer}}
<a class="btn btn-info" role="button">
Load Balancer: {{.Method}}
</a>
{{end}}
{{with $valueBackends.CircuitBreaker}}
<a class="btn btn-info" role="button">
Circuit Breaker: {{.Expression}}
</a>
{{end}}
</div>
<table class="table table-striped table-hover">
<tr>
<td><em>Server</em></td>

25
tests/consul-config.sh Executable file
View file

@ -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

View file

@ -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

View file

@ -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