Load balance a Swarm Service behind HA Proxy with automatic reconfiguration

In this blog post, I'll be using Victor Farcic (Cloudbees senior employee, Docker captain, written books about Devops) Docker flow proxy and docker flow swarm listener to reconfigure an HAproxy based on a Swarm service!

If you want to see how to do the same with Traefik, check this post!

These two services work hand in hand. Docker Flow Listener will listen to the Swarm events over Docker's remote API. It will inform Docker flow proxy so it reconfigure the HAPRoxy frontend and backend.

Enough talk. Let's go.

Admin/Viz - if you need to look into what is happening

Let's start by installing Portainer and Docker Swarm Visualizer to be able to follow along on what is happening on our cluster. To do that:

docker service create \
  --publish 9008:9000 \
  --limit-cpu 0.5 \
  --name portainer-swarm \
  --constraint=node.role==manager \
  --mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
  portainer/portainer --swarm

docker service create \
  --publish 9009:9000 \
  --limit-cpu 0.5 \
  --name portainer \
  --mode global \
  --mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
  portainer/portainer

docker service create \
  --publish=9090:8080 \
  --limit-cpu 0.5 \
  --name=viz \
  --env PORT=9090 \
  --constraint=node.role==manager \
  --mount=type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock \
  manomarks/visualizer

Next Open - http://stratus-clay:9090 and http://stratus-clay:9008, and http://stratus-clay:9009 (and on each of the swarm nodes if needed)

Utility function to wait for a service... (used below for demo purposes)

function wait_for_service()
 if [ $# -ne 1 ]
  then
	echo usage $FUNCNAME "service";
	echo e.g: $FUNCNAME docker-proxy
  else
serviceName=$1

while true; do
	REPLICAS=$(docker service ls | grep -E "(^| )$serviceName( |$)" | awk '{print $3}')
	if [[ $REPLICAS == "1/1" ]]; then
		break
	else
		echo "Waiting for the $serviceName service... ($REPLICAS)"
		sleep 5
	fi
done
fi

Alternatively you can run watch in another shell and visually check that the service is ready

watch -d -n 2 docker service ls

Create Networks

In order to isolate our application from the outside, we will configure two networks. One that will include the services which are accessible from the outside (read HAProxy), and another which will have our microservices Stack deployed on it. The networks will be overlay networks, meaning they will span the whole cluster.

docker network create --driver overlay proxy
docker network create --driver overlay go-demo

Create Docker Listener

In order to create the listener service, add it to the network proxy, enable it to monitor the swarm (mount the docker.sock and constraint it to managers nodes), and link it to the docker flow proxy (which we will be creating next) we need to issue the below command:

docker service create --name swarm-listener --network proxy --mount "type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock" -e DF_NOTIF_CREATE_SERVICE_URL=http://docker-proxy:8080/v1/docker-flow-proxy/reconfigure -e DF_NOTIF_REMOVE_SERVICE_URL=http://docker-proxy:8080/v1/docker-flow-proxy/remove --constraint 'node.role==manager' vfarcic/docker-flow-swarm-listener

wait_for_service swarm-listener

Create HAProxy with automatic reconfiguration

Let's create the Docker Flow proxy now. It will be listening on the port 80 and 443 (secure). It also need to link to the previous swarm-listener service, be active in swarm mode

docker service create --name docker-proxy -p 80:80 -p 443:443 -p 8080:8080 --network proxy -e SERVICE_NAME=docker-proxy  -e MODE=swarm -e LISTENER_ADDRESS=swarm-listener vfarcic/docker-flow-proxy

wait_for_service docker-proxy

Alternatively, if you need to customize the configuration or the image, you can rebuild it and use your own image instead (like the below)

docker service create --name docker-proxy -p 80:80 -p 443:443 -p 8080:8080 --network proxy -e SERVICE_NAME=docker-proxy -e MODE=swarm -e LISTENER_ADDRESS=swarm-listener jmkhael/myproxy:0.0.1

Prepare demo services

Here we can deploy our "complicated" stack offering. Could be a Mongo DB with a REST service based on it or the hello from "hostname" we've built in previous posts... but we don't really care... you can go with one or the other, or both...

docker service create --name go-demo-db --network go-demo mongo
wait_for_service go-demo-db

docker service create --name go-demo -e DB=go-demo-db --network go-demo --network proxy --label com.df.notify=true --label com.df.distribute=true --label com.df.servicePath=/demo --label com.df.port=8080 vfarcic/go-demo
wait_for_service go-demo

docker service create --name hello-svc --network go-demo --network proxy --label com.df.notify=true --label com.df.distribute=true --label com.df.servicePath=/jmkhael --label com.df.port=5000 jmkhael/myservice:0.0.1
wait_for_service hello-svc

The labels com.df.notify=true, com.df.distribute=true, com.df.servicePath=/jmkhael and com.df.port=5000 are needed to inform Docker Flow Proxy how to reconfigure the service we are exposing. They will get into the HAProxy config as per the below:

acl url_hello-svc path_beg /jmkhael
use_backend hello-svc-be if url_hello-svc

backend hello-svc-be
    mode http
    server hello-svc hello-svc:5000

As you can see, the HAProxy will use the Swarm load balancing on the service name we used. Given that the Docker Flow Proxy is on both networks (the internal one too), it will be able to communicate with our service!

Test

Let's test that that work.

Check demo-service respond hello, world!

curl http://stratus-clay/demo/hello
time for i in {1..50} ; do curl http://stratus-clay/demo/hello; done

Check the hello-svc responds with "Hello from hostname"

for i in {1..5} ; do curl http://stratus-clay/jmkhael ; done

Check HAProxy config

curl http://`hostname`:8080/v1/docker-flow-proxy/config

Benchmark it

ab -r -c 10000 -n 100000 http://stratus-clay/jmkhael | grep "per sec"

Reconfigure HAProxy

curl "http://`hostname`:8080/v1/docker-flow-proxy/reconfigure?serviceName=go-demo&servicePath=/demo&port=8080&distribute=true"

Cleanup Services and networks

docker service rm swarm-listener go-demo-db docker-proxy go-demo portainer portainer-swarm viz hello-svc
docker network rm proxy go-demo

HA Proxy breaking under load

The below is mainly due to my ignorance about this subject. I am still learning. If you have any insights, feel free to leave a comment.

When trying to benchmark my service with Apache Benchmark to see how it behaves under load, I was faced with the dreaded apr_socket_recv: Connection reset by peer (104)

ab -c 50 -n 100000 http://stratus-clay/demo/hello

I though maybe the problem is HAProxy dropping connections, and I found that there is no default logging active. I opened this issue

Then this post lead me to investigate a Possible SYN flooding on port 8080. Sending cookies. Check SNMP counters.

Debugging and checking logs

Check docker Deamon logs

sudo journalctl -fu docker.service

Check dmesg

dmesg | grep SYN

I've also tried to fiddle with Kernel configuration as per the slides from HA proxy tech: http://www.slideshare.net/haproxytech/haproxy-best-practice (slide 13)

sysctl -w net.ipv4.tcp_max_syn_backlog=100000
sysctl -w net.core.netdev_max_backlog=100000
sysctl -w net.core.somaxconn=65534
sysctl -w net.ipv4.tcp_syncookies=0

Check them using:

sysctl -a | grep "net.ipv4.tcp_max_syn_backlog"
sysctl -a | grep "net.core.netdev_max_backlog"
sysctl -a | grep "net.core.somaxconn"

but this didn't solve my issue. I've abandoned going further this route for now. if you have any insights to make things clearer for me, please drop me a comment :)