diff --git a/docs/basics.md b/docs/basics.md index df967f308..e0c700a16 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -222,6 +222,17 @@ For example: - 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. +Sticky sessions are supported with both load balancers. When sticky sessions are enabled, a cookie called `_TRAEFIK_BACKEND` is set on the initial +request. On subsequent requests, the client will be directed to the backend stored in the cookie if it is still healthy. If not, a new backend +will be assigned. + +For example: +```toml +[backends] + [backends.backend1] + [backends.backend1.loadbalancer] + sticky = true +``` ## Servers Servers are simply defined using a `URL`. You can also apply a custom `weight` to each server (this will be used by load-balancing). diff --git a/docs/toml.md b/docs/toml.md index 0c5557d04..95feaa327 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -711,6 +711,7 @@ Labels can be used on containers to override default behaviour: - `traefik.backend.maxconn.amount=10`: set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. - `traefik.backend.maxconn.extractorfunc=client.ip`: set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. - `traefik.backend.loadbalancer.method=drr`: override the default `wrr` load balancer algorithm +- `traefik.backend.loadbalancer.sticky=true`: enable backend sticky sessions - `traefik.backend.circuitbreaker.expression=NetworkErrorRatio() > 0.5`: create a [circuit breaker](/basics/#backends) to be used against the backend - `traefik.port=80`: register this port. Useful when the container exposes multiples ports. - `traefik.protocol=https`: override the default `http` protocol @@ -810,6 +811,7 @@ Labels can be used on containers to override default behaviour: - `traefik.backend.maxconn.amount=10`: set a maximum number of connections to the backend. Must be used in conjunction with the below label to take effect. - `traefik.backend.maxconn.extractorfunc=client.ip`: set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect. - `traefik.backend.loadbalancer.method=drr`: override the default `wrr` load balancer algorithm +- `traefik.backend.loadbalancer.sticky=true`: enable backend sticky sessions - `traefik.backend.circuitbreaker.expression=NetworkErrorRatio() > 0.5`: create a [circuit breaker](/basics/#backends) to be used against the backend - `traefik.portIndex=1`: register port by index in the application's ports array. Useful when the application exposes multiple ports. - `traefik.port=80`: register the explicit application port value. Cannot be used alongside `traefik.portIndex`. diff --git a/glide.lock b/glide.lock index 9caaf5b2c..4097907e3 100644 --- a/glide.lock +++ b/glide.lock @@ -1,10 +1,10 @@ hash: c0ac205a859d78847e21d3cd63f427ffba985755c6ae84373e4a20364ba39b05 -updated: 2016-09-28T16:50:04.352639437+01:00 +updated: 2016-09-30T10:57:42.336729457+02:00 imports: - name: github.com/abbot/go-http-auth version: cb4372376e1e00e9f6ab9ec142e029302c9e7140 - name: github.com/boltdb/bolt - version: fff57c100f4dea1905678da7e90d92429dff2904 + version: 5cc10bbbc5c141029940133bb33c9e969512a698 - name: github.com/BurntSushi/toml version: 99064174e013895bbd9b025c31100bd1d9b590ca - name: github.com/BurntSushi/ty @@ -18,7 +18,7 @@ imports: - name: github.com/codegangsta/cli version: 1efa31f08b9333f1bd4882d61f9d668a70cd902e - name: github.com/codegangsta/negroni - version: 3f7ce7b928e14ff890b067e5bbbc80af73690a9c + version: dc6b9d037e8dab60cbfc09c61d6932537829be8b - name: github.com/containous/flaeg version: a731c034dda967333efce5f8d276aeff11f8ff87 - name: github.com/containous/mux @@ -32,11 +32,11 @@ imports: - pkg/pathutil - pkg/types - name: github.com/davecgh/go-spew - version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 + version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d subpackages: - spew - name: github.com/docker/distribution - version: 99cb7c0946d2f5a38015443e515dc916295064d7 + version: 87917f30529e6a7fca8eaff2932424915fb11225 subpackages: - context - digest @@ -116,13 +116,13 @@ imports: - types/time - types/versions - name: github.com/docker/go-connections - version: 988efe982fdecb46f01d53465878ff1f2ff411ce + version: 990a1a1a70b0da4c4cb70e117971a4f0babfbf1a subpackages: - nat - sockets - tlsconfig - name: github.com/docker/go-units - version: f2145db703495b2e525c59662db69a7344b00bb8 + version: f2d77a61e3c169b43402a0a1e84f06daf29b8190 - name: github.com/docker/libcompose version: d1876c1d68527a49c0aac22a0b161acc7296b740 subpackages: @@ -141,7 +141,7 @@ imports: - version - yaml - name: github.com/docker/libkv - version: 3fce6a0f26e07da3eac45796a8e255547a47a750 + version: 35d3e2084c650109e7bcc7282655b1bc8ba924ff subpackages: - store - store/boltdb @@ -151,13 +151,13 @@ imports: - name: github.com/donovanhide/eventsource version: fd1de70867126402be23c306e1ce32828455d85b - name: github.com/elazarl/go-bindata-assetfs - version: 9a6736ed45b44bf3835afeebb3034b57ed329f3e + version: 57eb5e1fc594ad4b0b1dbea7b286d299e0cb43c2 - name: github.com/gambol99/go-marathon version: a558128c87724cd7430060ef5aedf39f83937f55 - name: github.com/go-check/check - version: 11d3bc7aa68e238947792f30573146a3231fc0f1 + version: 4f90aeace3a26ad7021961c297b22c42160c7b25 - name: github.com/gogo/protobuf - version: 89f1976ff373a3e549675d2f212c10f98b6c6316 + version: e33835a643a970c11ac74f6333f5f6866387a101 subpackages: - proto - name: github.com/golang/glog @@ -167,17 +167,18 @@ imports: subpackages: - query - name: github.com/gorilla/context - version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 + version: aed02d124ae4a0e94fea4541c8effd05bf0c8296 - name: github.com/hashicorp/consul - version: d5b7530ec593f1ec2a8f8a7c145bcadafa88b572 + version: fce7d75609a04eeb9d4bf41c8dc592aac18fc97d subpackages: - api - name: github.com/hashicorp/go-cleanhttp - version: ad28ea4487f05916463e2423a55166280e8254b5 + version: 875fb671b3ddc66f8e2f0acc33829c8cb989a38d - name: github.com/hashicorp/serf - version: b7a120a5fc494f6dd5e858f42fd0fd4022d6320f + version: 6c4672d66fc6312ddde18399262943e21175d831 subpackages: - coordinate + - serf - name: github.com/jarcoal/httpmock version: 145b10d659265440f062c31ea15326166bae56ee - name: github.com/libkermit/compose @@ -219,7 +220,7 @@ imports: - name: github.com/miekg/dns version: 5d001d020961ae1c184f9f8152fdc73810481677 - name: github.com/mitchellh/mapstructure - version: ca63d7c062ee3c9f34db231e352b60012b4fd0c1 + version: d2dd0262208475919e1a362f675cfc0e7c10e905 - name: github.com/moul/http2curl version: b1479103caacaa39319f75e7f57fc545287fca0d - name: github.com/NYTimes/gziphandler @@ -227,11 +228,11 @@ imports: - name: github.com/ogier/pflag version: 45c278ab3607870051a2ea9040bb85fcb8557481 - name: github.com/opencontainers/runc - version: d9fec4c63b089ddfc267194ecb6cda58a13f072c + version: 1a81e9ab1f138c091fe5c86d0883f87716088527 subpackages: - libcontainer/user - name: github.com/parnurzeal/gorequest - version: 29ced6f360a5ac3823a3675b4e29fbab336cc186 + version: 045012d33ef41ea146c1b675df9296d0dc1a212d - name: github.com/pmezard/go-difflib version: d8ed2627bdf02c080bf22230dbb337003b7aba2d subpackages: @@ -239,22 +240,22 @@ imports: - name: github.com/ryanuber/go-glob version: 572520ed46dbddaed19ea3d9541bdd0494163693 - name: github.com/samuel/go-zookeeper - version: 87e1bca4477a3cc767ca71be023ced183d74e538 + version: e64db453f3512cade908163702045e0f31137843 subpackages: - zk - name: github.com/Sirupsen/logrus - version: 3ec0642a7fb6488f65b06f9040adc67e3990296a + version: a283a10442df8dc09befd873fab202bf8a253d6a - name: github.com/streamrail/concurrent-map - version: 8bf1e9bacbf65b10c81d0f4314cf2b1ebef728b5 + version: 65a174a3a4188c0b7099acbc6cfa0c53628d3287 - name: github.com/stretchr/objx version: cbeaeb16a013161a98496fad62933b1d21786672 - name: github.com/stretchr/testify - version: 976c720a22c8eb4eb6a0b4348ad85ad12491a506 + version: d77da356e56a7428ad25149ca77381849a6a5232 subpackages: - assert - mock - name: github.com/thoas/stats - version: 152b5d051953fdb6e45f14b6826962aadc032324 + version: 79b768ff1780f4e5b0ed132e192bfeefe9f85a9c - name: github.com/tv42/zbase32 version: 03389da7e0bf9844767f82690f4d68fc097a1306 - name: github.com/ugorji/go @@ -262,7 +263,7 @@ imports: subpackages: - codec - name: github.com/unrolled/render - version: 3f4913244021dede105b62caecfb01f0c1eebeda + version: 198ad4d8b8a4612176b804ca10555b222a086b40 - name: github.com/vdemeester/docker-events version: be74d4929ec1ad118df54349fda4b0cba60f849b - name: github.com/vdemeester/shakers @@ -284,7 +285,7 @@ imports: - name: github.com/vulcand/route version: cb89d787ddbb1c5849a7ac9f79004c1fd12a4a32 - name: github.com/vulcand/vulcand - version: 643ca8acff8386e3b276f6feb8ba9b5893dbc4a2 + version: 28a4e5c0892167589737b95ceecbcef00295be50 subpackages: - conntracker - plugin @@ -314,11 +315,11 @@ imports: - name: gopkg.in/fsnotify.v1 version: a8a77c9133d2d6fd8334f3260d06f60e8d80a5fb - name: gopkg.in/mgo.v2 - version: 22287bab4379e1fbf6002fb4eb769888f3fb224c + version: 29cc868a5ca65f401ff318143f9408d02f4799cc subpackages: - bson - name: gopkg.in/square/go-jose.v1 - version: aa2e30fdd1fe9dd3394119af66451ae790d50e0d + version: e3f973b66b91445ec816dd7411ad1b6495a5a2fc subpackages: - cipher - json @@ -336,9 +337,9 @@ testImports: - name: github.com/libkermit/docker-check version: cbe0ef03b3d23070eac4d00ba8828f2cc7f7e5a3 - name: github.com/spf13/pflag - version: 08b1a584251b5b62f458943640fc8ebd4d50aaa5 + version: 5644820622454e71517561946e3d94b9f9db6842 - name: github.com/vbatts/tar-split - version: bd4c5d64c3e9297f410025a3b1bd0c58f659e721 + version: 6810cedb21b2c3d0b9bb8f9af12ff2dc7a2f14df subpackages: - archive/tar - tar/asm diff --git a/provider/docker.go b/provider/docker.go index e33c11831..f816b3130 100644 --- a/provider/docker.go +++ b/provider/docker.go @@ -252,6 +252,7 @@ func (provider *Docker) loadDockerConfig(containersInspected []dockerData) *type "hasMaxConnLabels": provider.hasMaxConnLabels, "getMaxConnAmount": provider.getMaxConnAmount, "getMaxConnExtractorFunc": provider.getMaxConnExtractorFunc, + "getSticky": provider.getSticky, "replace": replace, } @@ -261,18 +262,27 @@ func (provider *Docker) loadDockerConfig(containersInspected []dockerData) *type }, containersInspected).([]dockerData) frontends := map[string][]dockerData{} + backends := map[string]dockerData{} + servers := map[string][]dockerData{} for _, container := range filteredContainers { frontendName := provider.getFrontendName(container) frontends[frontendName] = append(frontends[frontendName], container) + backendName := provider.getBackend(container) + backends[backendName] = container + servers[backendName] = append(servers[backendName], container) } templateObjects := struct { Containers []dockerData Frontends map[string][]dockerData + Backends map[string]dockerData + Servers map[string][]dockerData Domain string }{ filteredContainers, frontends, + backends, + servers, provider.Domain, } @@ -291,7 +301,9 @@ func (provider *Docker) hasCircuitBreakerLabel(container dockerData) bool { } func (provider *Docker) hasLoadBalancerLabel(container dockerData) bool { - if _, err := getLabel(container, "traefik.backend.loadbalancer.method"); err != nil { + _, errMethod := getLabel(container, "traefik.backend.loadbalancer.method") + _, errSticky := getLabel(container, "traefik.backend.loadbalancer.sticky") + if errMethod != nil && errSticky != nil { return false } return true @@ -439,6 +451,13 @@ func (provider *Docker) getWeight(container dockerData) string { return "1" } +func (provider *Docker) getSticky(container dockerData) string { + if _, err := getLabel(container, "traefik.backend.loadbalancer.sticky"); err == nil { + return "true" + } + return "false" +} + func (provider *Docker) getDomain(container dockerData) string { if label, err := getLabel(container, "traefik.domain"); err == nil { return label diff --git a/provider/docker_test.go b/provider/docker_test.go index c344eb5f1..45f5ccbd8 100644 --- a/provider/docker_test.go +++ b/provider/docker_test.go @@ -940,7 +940,6 @@ func TestDockerLoadDockerConfig(t *testing.T) { }, }, CircuitBreaker: nil, - LoadBalancer: nil, }, }, }, @@ -1027,7 +1026,6 @@ func TestDockerLoadDockerConfig(t *testing.T) { }, }, CircuitBreaker: nil, - LoadBalancer: nil, }, }, }, diff --git a/provider/marathon.go b/provider/marathon.go index ecf736c78..10b99f999 100644 --- a/provider/marathon.go +++ b/provider/marathon.go @@ -138,6 +138,7 @@ func (provider *Marathon) loadMarathonConfig() *types.Configuration { "getMaxConnAmount": provider.getMaxConnAmount, "getLoadBalancerMethod": provider.getLoadBalancerMethod, "getCircuitBreakerExpression": provider.getCircuitBreakerExpression, + "getSticky": provider.getSticky, } applications, err := provider.marathonClient.Applications(nil) @@ -347,6 +348,13 @@ func (provider *Marathon) getProtocol(task marathon.Task, applications []maratho return "http" } +func (provider *Marathon) getSticky(application marathon.Application) string { + if sticky, err := provider.getLabel(application, "traefik.backend.loadbalancer.sticky"); err == nil { + return sticky + } + return "false" +} + func (provider *Marathon) getPassHostHeader(application marathon.Application) string { if passHostHeader, err := provider.getLabel(application, "traefik.frontend.passHostHeader"); err == nil { return passHostHeader @@ -411,7 +419,9 @@ func (provider *Marathon) hasCircuitBreakerLabels(application marathon.Applicati } func (provider *Marathon) hasLoadBalancerLabels(application marathon.Application) bool { - if _, err := provider.getLabel(application, "traefik.backend.loadbalancer.method"); err != nil { + _, errMethod := provider.getLabel(application, "traefik.backend.loadbalancer.method") + _, errSticky := provider.getLabel(application, "traefik.backend.loadbalancer.sticky") + if errMethod != nil && errSticky != nil { return false } return true diff --git a/provider/marathon_test.go b/provider/marathon_test.go index cb4edea27..030cc2fb8 100644 --- a/provider/marathon_test.go +++ b/provider/marathon_test.go @@ -107,7 +107,6 @@ func TestMarathonLoadConfig(t *testing.T) { }, }, CircuitBreaker: nil, - LoadBalancer: nil, }, }, }, diff --git a/server.go b/server.go index 0d6cf7fb6..9587ba872 100644 --- a/server.go +++ b/server.go @@ -537,16 +537,30 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo log.Errorf("Skipping frontend %s...", frontendName) continue frontend } + lbMethod, err := types.NewLoadBalancerMethod(configuration.Backends[frontend.Backend].LoadBalancer) if err != nil { log.Errorf("Error loading load balancer method '%+v' for frontend %s: %v", configuration.Backends[frontend.Backend].LoadBalancer, frontendName, err) log.Errorf("Skipping frontend %s...", frontendName) continue frontend } + + stickysession := configuration.Backends[frontend.Backend].LoadBalancer.Sticky + cookiename := "_TRAEFIK_BACKEND" + var sticky *roundrobin.StickySession + + if stickysession { + sticky = roundrobin.NewStickySession(cookiename) + } + switch lbMethod { case types.Drr: log.Debugf("Creating load-balancer drr") rebalancer, _ := roundrobin.NewRebalancer(rr, roundrobin.RebalancerLogger(oxyLogger)) + if stickysession { + log.Debugf("Sticky session with cookie %v", cookiename) + rebalancer, _ = roundrobin.NewRebalancer(rr, roundrobin.RebalancerLogger(oxyLogger), roundrobin.RebalancerStickySession(sticky)) + } lb = rebalancer for serverName, server := range configuration.Backends[frontend.Backend].Servers { url, err := url.Parse(server.URL) @@ -565,6 +579,10 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo } case types.Wrr: log.Debugf("Creating load-balancer wrr") + if stickysession { + log.Debugf("Sticky session with cookie %v", cookiename) + rr, _ = roundrobin.New(saveBackend, roundrobin.EnableStickySession(sticky)) + } lb = rr for serverName, server := range configuration.Backends[frontend.Backend].Servers { url, err := url.Parse(server.URL) diff --git a/templates/docker.tmpl b/templates/docker.tmpl index 5b9acce33..e67232ebc 100644 --- a/templates/docker.tmpl +++ b/templates/docker.tmpl @@ -1,23 +1,29 @@ -[backends]{{range .Containers}} - {{if hasCircuitBreakerLabel .}} - [backends.backend-{{getBackend .}}.circuitbreaker] - expression = "{{getCircuitBreakerExpression .}}" +{{$backendServers := .Servers}} +[backends]{{range $backendName, $backend := .Backends}} + {{if hasCircuitBreakerLabel $backend}} + [backends.backend-{{$backendName}}.circuitbreaker] + expression = "{{getCircuitBreakerExpression $backend}}" {{end}} - {{if hasLoadBalancerLabel .}} - [backends.backend-{{getBackend .}}.loadbalancer] - method = "{{getLoadBalancerMethod .}}" + {{if hasLoadBalancerLabel $backend}} + [backends.backend-{{$backendName}}.loadbalancer] + method = "{{getLoadBalancerMethod $backend}}" + sticky = {{getSticky $backend}} {{end}} - {{if hasMaxConnLabels .}} - [backends.backend-{{getBackend .}}.maxconn] - amount = {{getMaxConnAmount . }} - extractorfunc = "{{getMaxConnExtractorFunc . }}" + {{if hasMaxConnLabels $backend}} + [backends.backend-{{$backendName}}.maxconn] + amount = {{getMaxConnAmount $backend}} + extractorfunc = "{{getMaxConnExtractorFunc $backend}}" + {{end}} + + {{$servers := index $backendServers $backendName}} + {{range $serverName, $server := $servers}} + [backends.backend-{{$backendName}}.servers.server-{{$server.Name | replace "/" "" | replace "." "-"}}] + url = "{{getProtocol $server}}://{{getIPAddress $server}}:{{getPort $server}}" + weight = {{getWeight $server}} {{end}} - [backends.backend-{{getBackend .}}.servers.server-{{.Name | replace "/" "" | replace "." "-"}}] - url = "{{getProtocol .}}://{{getIPAddress .}}:{{getPort .}}" - weight = {{getWeight .}} {{end}} [frontends]{{range $frontend, $containers := .Frontends}} diff --git a/templates/kv.tmpl b/templates/kv.tmpl index 153f12b9c..c642ff7b1 100644 --- a/templates/kv.tmpl +++ b/templates/kv.tmpl @@ -12,9 +12,11 @@ {{end}} {{$loadBalancer := Get "" . "/loadbalancer/" "method"}} +{{$sticky := Get "false" . "/loadbalancer/" "sticky"}} {{with $loadBalancer}} [backends."{{Last $backend}}".loadBalancer] method = "{{$loadBalancer}}" + sticky = {{$sticky}} {{end}} {{$maxConnAmt := Get "" . "/maxconn/" "amount"}} diff --git a/templates/marathon.tmpl b/templates/marathon.tmpl index fbd2bf801..04a6912c2 100644 --- a/templates/marathon.tmpl +++ b/templates/marathon.tmpl @@ -14,6 +14,7 @@ {{ if hasLoadBalancerLabels . }} [backends.backend{{getFrontendBackend . }}.loadbalancer] method = "{{getLoadBalancerMethod . }}" + sticky = {{getSticky .}} {{end}} {{ if hasCircuitBreakerLabels . }} [backends.backend{{getFrontendBackend . }}.circuitbreaker] diff --git a/types/types.go b/types/types.go index 8b4d2a264..d5a0f0ead 100644 --- a/types/types.go +++ b/types/types.go @@ -24,6 +24,7 @@ type MaxConn struct { // LoadBalancer holds load balancing configuration. type LoadBalancer struct { Method string `json:"method,omitempty"` + Sticky bool `json:"sticky,omitempty"` } // CircuitBreaker holds circuit breaker configuration.