Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support sticky sessions under SWARM Mode. #1024 #1033

Merged
merged 2 commits into from
Feb 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.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 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.port=80`: register this port. Useful when the container exposes multiples ports.
- `traefik.protocol=https`: override the default `http` protocol
Expand Down
84 changes: 83 additions & 1 deletion 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 @@ -120,14 +121,17 @@ docker-machine ssh manager "docker service create \
--label traefik.port=80 \
--network traefik-net \
emilevauge/whoami"

docker-machine ssh manager "docker service create \
--name whoami1 \
--label traefik.port=80 \
--network traefik-net \
--label traefik.backend.loadbalancer.sticky=true \
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:

Expand Down Expand Up @@ -220,6 +224,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)


83 changes: 74 additions & 9 deletions provider/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type Docker struct {

// dockerData holds the need data to the Docker provider
type dockerData struct {
ServiceName string
Name string
Labels map[string]string // List of labels set to container or service
NetworkSettings networkSettings
Expand Down Expand Up @@ -129,7 +130,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 @@ -156,7 +157,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 @@ -256,8 +257,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 @@ -393,14 +394,14 @@ func (provider *Docker) getFrontendRule(container dockerData) string {
if label, err := getLabel(container, "traefik.frontend.rule"); err == nil {
return label
}
return "Host:" + provider.getSubDomain(container.Name) + "." + provider.Domain
return "Host:" + provider.getSubDomain(container.ServiceName) + "." + provider.Domain
}

func (provider *Docker) getBackend(container dockerData) string {
if label, err := getLabel(container, "traefik.backend"); err == nil {
return normalize(label)
}
return normalize(container.Name)
return normalize(container.ServiceName)
}

func (provider *Docker) getIPAddress(container dockerData) string {
Expand Down Expand Up @@ -455,8 +456,15 @@ func (provider *Docker) getWeight(container dockerData) string {
}

func (provider *Docker) getSticky(container dockerData) string {
if _, err := getLabel(container, "traefik.backend.loadbalancer.sticky"); err == nil {
return "true"
if label, err := getLabel(container, "traefik.backend.loadbalancer.sticky"); err == nil {
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"
}
Expand Down Expand Up @@ -552,6 +560,7 @@ func parseContainer(container dockertypes.ContainerJSON) dockerData {

if container.ContainerJSONBase != nil {
dockerData.Name = container.ContainerJSONBase.Name
dockerData.ServiceName = dockerData.Name //Default ServiceName to be the container's Name.

if container.ContainerJSONBase.HostConfig != nil {
dockerData.NetworkSettings.NetworkMode = container.ContainerJSONBase.HostConfig.NetworkMode
Expand Down Expand Up @@ -591,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 @@ -612,18 +621,29 @@ 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))

dockerDataList = append(dockerDataList, dockerData)
if useSwarmLB {
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

}

func parseService(service swarmtypes.Service, networkMap map[string]*dockertypes.NetworkResource) dockerData {
dockerData := dockerData{
ServiceName: service.Spec.Annotations.Name,
Name: service.Spec.Annotations.Name,
Labels: service.Spec.Annotations.Labels,
NetworkSettings: networkSettings{},
Expand All @@ -648,7 +668,52 @@ func parseService(service swarmtypes.Service, networkMap map[string]*dockertypes
} else {
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{
ServiceName: serviceDockerData.Name,
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
}
}
}
}
Expand Down