Skip to content

Commit

Permalink
Support sticky sessions under SWARM Mode.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Mike Foley committed Jan 13, 2017
1 parent dd85cbc commit 59cec1c
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 5 deletions.
1 change: 1 addition & 0 deletions docs/toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,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.loadbalancer.method=drr`: override the default `wrr` load balancer algorithm
- `traefik.backend.loadbalancer.sticky=true`: enable backend sticky sessions
- `traefik.backend.loadbalancer.swarm=true `: use Swarm's inbuilt load balncer (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.port=80`: register this port. Useful when the container exposes multiples ports.
- `traefik.protocol=https`: override the default `http` protocol
Expand Down
87 changes: 87 additions & 0 deletions docs/user-guide/swarm-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ docker-machine ssh worker1 "docker swarm join \
--listen-addr $(docker-machine ip worker1) \
--advertise-addr $(docker-machine ip worker1) \
$(docker-machine ip manager)"

docker-machine ssh worker2 "docker swarm join \
--token=${worker_token} \
--listen-addr $(docker-machine ip worker2) \
Expand Down Expand Up @@ -119,14 +120,22 @@ docker-machine ssh manager "docker service create \
--name whoami0 \
--label traefik.port=80 \
--network traefik-net \
--label traefik.frontend.rule=Host:whoami0.traefik \
--label traefik.backend=whoami0 \
emilevauge/whoami"

docker-machine ssh manager "docker service create \
--name whoami1 \
--label traefik.port=80 \
--network traefik-net \
--label traefik.frontend.rule=Host:whoam1.traefik \
--label traefik.backend=whoami1 \
--label traefik.backend.loadbalancer.sticky=true \
emilevauge/whoami"
```

Note that we set whoami1 to use sticky sessions (`--label traefik.backend.loadbalancer.sticky=true`). We'll demonstrate that later.

Check that everything is scheduled and started:

```sh
Expand Down Expand Up @@ -218,6 +227,84 @@ X-Forwarded-Proto: http
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)


73 changes: 68 additions & 5 deletions provider/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po
log.Debugf("Docker connection established with docker %s (API %s)", version.Version, version.APIVersion)
var dockerDataList []dockerData
if provider.SwarmMode {
dockerDataList, err = listServices(ctx, dockerClient)
dockerDataList, err = provider.listServices(ctx, dockerClient)
if err != nil {
log.Errorf("Failed to list services for docker swarm mode, error %s", err)
return err
Expand All @@ -158,7 +158,7 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po
for {
select {
case <-ticker.C:
services, err := listServices(ctx, dockerClient)
services, err := provider.listServices(ctx, dockerClient)
if err != nil {
log.Errorf("Failed to list services for docker, error %s", err)
return
Expand Down Expand Up @@ -258,8 +258,8 @@ func (provider *Docker) loadDockerConfig(containersInspected []dockerData) *type
"getMaxConnAmount": provider.getMaxConnAmount,
"getMaxConnExtractorFunc": provider.getMaxConnExtractorFunc,
"getSticky": provider.getSticky,
"getIsBackendLBSwarm": provider.getIsBackendLBSwarm,
}

// filter containers
filteredContainers := fun.Filter(func(container dockerData) bool {
return provider.containerFilter(container)
Expand Down Expand Up @@ -463,6 +463,13 @@ func (provider *Docker) getSticky(container dockerData) string {
return "false"
}

func (provider *Docker) getIsBackendLBSwarm(container dockerData) string {
if _, err := getLabel(container, "traefik.backend.loadbalancer.swarm"); err == nil {
return "true"
}
return "false"
}

func (provider *Docker) getDomain(container dockerData) string {
if label, err := getLabel(container, "traefik.domain"); err == nil {
return label
Expand Down Expand Up @@ -593,7 +600,7 @@ func (provider *Docker) getSubDomain(name string) string {
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{})
if err != nil {
return []dockerData{}, err
Expand All @@ -614,11 +621,21 @@ func listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerD
}

var dockerDataList []dockerData
var dockerDataListTasks []dockerData

for _, service := range serviceList {
dockerData := parseService(service, networkMap)
useSwarmLB, _ := strconv.ParseBool(provider.getIsBackendLBSwarm(dockerData))

if useSwarmLB {
dockerDataList = append(dockerDataList, dockerData)
} else {
dockerDataListTasks, err = listTasks(ctx, dockerClient, service.ID, dockerData, networkMap)

dockerDataList = append(dockerDataList, dockerData)
for _, dockerDataTask := range dockerDataListTasks {
dockerDataList = append(dockerDataList, dockerDataTask)
}
}
}
return dockerDataList, err

Expand Down Expand Up @@ -656,3 +673,49 @@ func parseService(service swarmtypes.Service, networkMap map[string]*dockertypes
}
return dockerData
}

func listTasks(ctx context.Context, dockerClient client.APIClient, serviceID string,
serviceDockerData dockerData, networkMap map[string]*dockertypes.NetworkResource) ([]dockerData, error) {
taskList, err := dockerClient.TaskList(ctx, dockertypes.TaskListOptions{})
if err != nil {
return []dockerData{}, err
}
var dockerDataList []dockerData

for _, task := range taskList {
if task.ServiceID == serviceID {
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 {
networkService := networkMap[virtualIP.Network.ID]
if networkService != nil {
// 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
}
}
}
}

return dockerData
}

0 comments on commit 59cec1c

Please sign in to comment.