Support sticky sessions under SWARM Mode.
SWARM Mode has it's own built in Load balancer, so if we want to leverage sticky sessions, or if we would just prefer to bypass it and go directly to the containers (aka tasks), via --label traefik.backend.disable.swarm.loadbalancer=true then we need to let Traefik know about the underlying tasks and register them as services within it's backend.
This commit is contained in:
parent
106e5c1f92
commit
d2b47a5681
3 changed files with 157 additions and 9 deletions
|
@ -804,6 +804,7 @@ Labels can be used on containers to override default behaviour:
|
||||||
- `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.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.method=drr`: override the default `wrr` load balancer algorithm
|
||||||
- `traefik.backend.loadbalancer.sticky=true`: enable backend sticky sessions
|
- `traefik.backend.loadbalancer.sticky=true`: enable backend sticky sessions
|
||||||
|
- `traefik.backend.loadbalancer.swarm=true `: use Swarm's inbuilt load balancer (only relevant under Swarm Mode).
|
||||||
- `traefik.backend.circuitbreaker.expression=NetworkErrorRatio() > 0.5`: create a [circuit breaker](/basics/#backends) to be used against the backend
|
- `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.port=80`: register this port. Useful when the container exposes multiples ports.
|
||||||
- `traefik.protocol=https`: override the default `http` protocol
|
- `traefik.protocol=https`: override the default `http` protocol
|
||||||
|
|
|
@ -47,6 +47,7 @@ docker-machine ssh worker1 "docker swarm join \
|
||||||
--listen-addr $(docker-machine ip worker1) \
|
--listen-addr $(docker-machine ip worker1) \
|
||||||
--advertise-addr $(docker-machine ip worker1) \
|
--advertise-addr $(docker-machine ip worker1) \
|
||||||
$(docker-machine ip manager)"
|
$(docker-machine ip manager)"
|
||||||
|
|
||||||
docker-machine ssh worker2 "docker swarm join \
|
docker-machine ssh worker2 "docker swarm join \
|
||||||
--token=${worker_token} \
|
--token=${worker_token} \
|
||||||
--listen-addr $(docker-machine ip worker2) \
|
--listen-addr $(docker-machine ip worker2) \
|
||||||
|
@ -119,15 +120,22 @@ docker-machine ssh manager "docker service create \
|
||||||
--name whoami0 \
|
--name whoami0 \
|
||||||
--label traefik.port=80 \
|
--label traefik.port=80 \
|
||||||
--network traefik-net \
|
--network traefik-net \
|
||||||
|
--label traefik.frontend.rule=Host:whoami0.traefik \
|
||||||
|
--label traefik.backend=whoami0 \
|
||||||
emilevauge/whoami"
|
emilevauge/whoami"
|
||||||
|
|
||||||
docker-machine ssh manager "docker service create \
|
docker-machine ssh manager "docker service create \
|
||||||
--name whoami1 \
|
--name whoami1 \
|
||||||
--label traefik.port=80 \
|
--label traefik.port=80 \
|
||||||
--network traefik-net \
|
--network traefik-net \
|
||||||
|
--label traefik.frontend.rule=Host:whoam1.traefik \
|
||||||
|
--label traefik.backend=whoami1 \
|
||||||
|
--label traefik.backend.loadbalancer.sticky=true \
|
||||||
emilevauge/whoami"
|
emilevauge/whoami"
|
||||||
```
|
```
|
||||||
|
|
||||||
NOTE: If using `docker stack deploy`, there is [a specific way that the labels must be defined in the docker-compose file](https://github.com/containous/traefik/issues/994#issuecomment-269095109).
|
Note that we set whoami1 to use sticky sessions (`--label traefik.backend.loadbalancer.sticky=true`). We'll demonstrate that later.
|
||||||
|
If using `docker stack deploy`, there is [a specific way that the labels must be defined in the docker-compose file](https://github.com/containous/traefik/issues/994#issuecomment-269095109).
|
||||||
|
|
||||||
Check that everything is scheduled and started:
|
Check that everything is scheduled and started:
|
||||||
|
|
||||||
|
@ -220,6 +228,84 @@ X-Forwarded-Proto: http
|
||||||
X-Forwarded-Server: 8fbc39271b4c
|
X-Forwarded-Server: 8fbc39271b4c
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Scale both services
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-machine ssh manager "docker service scale whoami0=5"
|
||||||
|
|
||||||
|
docker-machine ssh manager "docker service scale whoami1=5"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Check that we now have 5 replicas of each `whoami` service:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-machine ssh manager "docker service ls"
|
||||||
|
ID NAME REPLICAS IMAGE COMMAND
|
||||||
|
ab046gpaqtln whoami0 5/5 emilevauge/whoami
|
||||||
|
cgfg5ifzrpgm whoami1 5/5 emilevauge/whoami
|
||||||
|
dtpl249tfghc traefik 1/1 traefik --docker --docker.swarmmode --docker.domain=traefik --docker.watch --web
|
||||||
|
```
|
||||||
|
## Access to your whoami0 through Træfɪk multiple times.
|
||||||
|
|
||||||
|
Repeat the following command multiple times and note that the Hostname changes each time as Traefik load balances each request against the 5 tasks.
|
||||||
|
```sh
|
||||||
|
curl -H Host:whoami0.traefik http://$(docker-machine ip manager)
|
||||||
|
Hostname: 8147a7746e7a
|
||||||
|
IP: 127.0.0.1
|
||||||
|
IP: ::1
|
||||||
|
IP: 10.0.9.3
|
||||||
|
IP: fe80::42:aff:fe00:903
|
||||||
|
IP: 172.18.0.3
|
||||||
|
IP: fe80::42:acff:fe12:3
|
||||||
|
GET / HTTP/1.1
|
||||||
|
Host: 10.0.9.3:80
|
||||||
|
User-Agent: curl/7.35.0
|
||||||
|
Accept: */*
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
X-Forwarded-For: 192.168.99.1
|
||||||
|
X-Forwarded-Host: 10.0.9.3:80
|
||||||
|
X-Forwarded-Proto: http
|
||||||
|
X-Forwarded-Server: 8fbc39271b4c
|
||||||
|
```
|
||||||
|
|
||||||
|
Do the same against whoami1.
|
||||||
|
```sh
|
||||||
|
curl -H Host:whoami1.traefik http://$(docker-machine ip manager)
|
||||||
|
Hostname: ba2c21488299
|
||||||
|
IP: 127.0.0.1
|
||||||
|
IP: ::1
|
||||||
|
IP: 10.0.9.4
|
||||||
|
IP: fe80::42:aff:fe00:904
|
||||||
|
IP: 172.18.0.2
|
||||||
|
IP: fe80::42:acff:fe12:2
|
||||||
|
GET / HTTP/1.1
|
||||||
|
Host: 10.0.9.4:80
|
||||||
|
User-Agent: curl/7.35.0
|
||||||
|
Accept: */*
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
X-Forwarded-For: 192.168.99.1
|
||||||
|
X-Forwarded-Host: 10.0.9.4:80
|
||||||
|
X-Forwarded-Proto: http
|
||||||
|
X-Forwarded-Server: 8fbc39271b4c
|
||||||
|
```
|
||||||
|
Wait, I thought we added the sticky flag to whoami1? Traefik relies on a cookie to maintain stickyness so you'll need to test this with a browser.
|
||||||
|
|
||||||
|
First you need to add whoami1.traefik to your hosts file:
|
||||||
|
```ssh
|
||||||
|
if [ -n "$(grep whoami1.traefik /etc/hosts)" ];
|
||||||
|
then
|
||||||
|
echo "whoami1.traefik already exists (make sure the ip is current)";
|
||||||
|
else
|
||||||
|
sudo -- sh -c -e "echo '$(docker-machine ip manager)\twhoami1.traefik'
|
||||||
|
>> /etc/hosts";
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
Now open your browser and go to http://whoami1.traefik/
|
||||||
|
|
||||||
|
You will now see that stickyness is maintained.
|
||||||
|
|
||||||
![](http://i.giphy.com/ujUdrdpX7Ok5W.gif)
|
![](http://i.giphy.com/ujUdrdpX7Ok5W.gif)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po
|
||||||
log.Debugf("Docker connection established with docker %s (API %s)", version.Version, version.APIVersion)
|
log.Debugf("Docker connection established with docker %s (API %s)", version.Version, version.APIVersion)
|
||||||
var dockerDataList []dockerData
|
var dockerDataList []dockerData
|
||||||
if provider.SwarmMode {
|
if provider.SwarmMode {
|
||||||
dockerDataList, err = listServices(ctx, dockerClient)
|
dockerDataList, err = provider.listServices(ctx, dockerClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to list services for docker swarm mode, error %s", err)
|
log.Errorf("Failed to list services for docker swarm mode, error %s", err)
|
||||||
return err
|
return err
|
||||||
|
@ -156,7 +156,7 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
services, err := listServices(ctx, dockerClient)
|
services, err := provider.listServices(ctx, dockerClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to list services for docker, error %s", err)
|
log.Errorf("Failed to list services for docker, error %s", err)
|
||||||
return
|
return
|
||||||
|
@ -256,8 +256,8 @@ func (provider *Docker) loadDockerConfig(containersInspected []dockerData) *type
|
||||||
"getMaxConnAmount": provider.getMaxConnAmount,
|
"getMaxConnAmount": provider.getMaxConnAmount,
|
||||||
"getMaxConnExtractorFunc": provider.getMaxConnExtractorFunc,
|
"getMaxConnExtractorFunc": provider.getMaxConnExtractorFunc,
|
||||||
"getSticky": provider.getSticky,
|
"getSticky": provider.getSticky,
|
||||||
|
"getIsBackendLBSwarm": provider.getIsBackendLBSwarm,
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter containers
|
// filter containers
|
||||||
filteredContainers := fun.Filter(func(container dockerData) bool {
|
filteredContainers := fun.Filter(func(container dockerData) bool {
|
||||||
return provider.containerFilter(container)
|
return provider.containerFilter(container)
|
||||||
|
@ -455,8 +455,15 @@ func (provider *Docker) getWeight(container dockerData) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (provider *Docker) getSticky(container dockerData) string {
|
func (provider *Docker) getSticky(container dockerData) string {
|
||||||
if _, err := getLabel(container, "traefik.backend.loadbalancer.sticky"); err == nil {
|
if label, err := getLabel(container, "traefik.backend.loadbalancer.sticky"); err == nil {
|
||||||
return "true"
|
return label
|
||||||
|
}
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (provider *Docker) getIsBackendLBSwarm(container dockerData) string {
|
||||||
|
if label, err := getLabel(container, "traefik.backend.loadbalancer.swarm"); err == nil {
|
||||||
|
return label
|
||||||
}
|
}
|
||||||
return "false"
|
return "false"
|
||||||
}
|
}
|
||||||
|
@ -591,7 +598,7 @@ func (provider *Docker) getSubDomain(name string) string {
|
||||||
return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1)
|
return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) {
|
func (provider *Docker) listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) {
|
||||||
serviceList, err := dockerClient.ServiceList(ctx, dockertypes.ServiceListOptions{})
|
serviceList, err := dockerClient.ServiceList(ctx, dockertypes.ServiceListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []dockerData{}, err
|
return []dockerData{}, err
|
||||||
|
@ -612,11 +619,21 @@ func listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerD
|
||||||
}
|
}
|
||||||
|
|
||||||
var dockerDataList []dockerData
|
var dockerDataList []dockerData
|
||||||
|
var dockerDataListTasks []dockerData
|
||||||
|
|
||||||
for _, service := range serviceList {
|
for _, service := range serviceList {
|
||||||
dockerData := parseService(service, networkMap)
|
dockerData := parseService(service, networkMap)
|
||||||
|
useSwarmLB, _ := strconv.ParseBool(provider.getIsBackendLBSwarm(dockerData))
|
||||||
|
|
||||||
|
if useSwarmLB {
|
||||||
dockerDataList = append(dockerDataList, dockerData)
|
dockerDataList = append(dockerDataList, dockerData)
|
||||||
|
} else {
|
||||||
|
dockerDataListTasks, err = listTasks(ctx, dockerClient, service.ID, dockerData, networkMap)
|
||||||
|
|
||||||
|
for _, dockerDataTask := range dockerDataListTasks {
|
||||||
|
dockerDataList = append(dockerDataList, dockerDataTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return dockerDataList, err
|
return dockerDataList, err
|
||||||
|
|
||||||
|
@ -648,7 +665,51 @@ func parseService(service swarmtypes.Service, networkMap map[string]*dockertypes
|
||||||
} else {
|
} else {
|
||||||
log.Debug("Network not found, id: %s", virtualIP.NetworkID)
|
log.Debug("Network not found, id: %s", virtualIP.NetworkID)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dockerData
|
||||||
|
}
|
||||||
|
|
||||||
|
func listTasks(ctx context.Context, dockerClient client.APIClient, serviceID string,
|
||||||
|
serviceDockerData dockerData, networkMap map[string]*dockertypes.NetworkResource) ([]dockerData, error) {
|
||||||
|
serviceIDFilter := filters.NewArgs()
|
||||||
|
serviceIDFilter.Add("service", serviceID)
|
||||||
|
taskList, err := dockerClient.TaskList(ctx, dockertypes.TaskListOptions{Filter: serviceIDFilter})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return []dockerData{}, err
|
||||||
|
}
|
||||||
|
var dockerDataList []dockerData
|
||||||
|
|
||||||
|
for _, task := range taskList {
|
||||||
|
dockerData := parseTasks(task, serviceDockerData, networkMap)
|
||||||
|
dockerDataList = append(dockerDataList, dockerData)
|
||||||
|
}
|
||||||
|
return dockerDataList, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTasks(task swarmtypes.Task, serviceDockerData dockerData, networkMap map[string]*dockertypes.NetworkResource) dockerData {
|
||||||
|
dockerData := dockerData{
|
||||||
|
Name: serviceDockerData.Name + "." + strconv.Itoa(task.Slot),
|
||||||
|
Labels: serviceDockerData.Labels,
|
||||||
|
NetworkSettings: networkSettings{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.NetworksAttachments != nil {
|
||||||
|
dockerData.NetworkSettings.Networks = make(map[string]*networkData)
|
||||||
|
for _, virtualIP := range task.NetworksAttachments {
|
||||||
|
if networkService, present := networkMap[virtualIP.Network.ID]; present {
|
||||||
|
// Not sure about this next loop - when would a task have multiple IP's for the same network?
|
||||||
|
for _, addr := range virtualIP.Addresses {
|
||||||
|
ip, _, _ := net.ParseCIDR(addr)
|
||||||
|
network := &networkData{
|
||||||
|
ID: virtualIP.Network.ID,
|
||||||
|
Name: networkService.Name,
|
||||||
|
Addr: ip.String(),
|
||||||
|
}
|
||||||
|
dockerData.NetworkSettings.Networks[network.Name] = network
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue