Sunday, April 29, 2018

How to scale your services with session affinity on docker in 5 minutes!

Assumes you know docker, docker swarm concepts and can understand something easy about node.js
Developing services with an eye on scalability? Who isn't right! This should give you some ideas on how to minimally setup and run a load balancer with your docker swarm to create sticky sessions where you want them.
I have two example Node.js services in this use case running, one of which I wish to track application state, and the other running stateless.
  1. Create two of these index.js files in separate directories, service1 and service2.
  2. Change the src root /serviceX and the res.end(`<h1> I'm serviceX.... to service1 and service2 to match the aforementioned directory naming
  3. In each service dir run npm init, then npm i S cookie-parser express express-session
  4. run node index.js and test each out individually by hitting localhost:8080/serviceX, you should see each subsequent reload increment a counter.
var express = require('express');
var cookieParser = require('cookie-parser');
var session = require('express-session');
var os = require('os');
var app = express();

app.use(cookieParser());

app.use(session({secret: "Shh, its a secret!"}));

app.get('/service1', function(req, res){

   if(req.session.page_views){

      req.session.page_views++;

   } else {

      req.session.page_views = 1;

   }

     res.writeHead(200, {'Content-Type': 'text/html'});

     res.write(JSON.stringify(req.headers));     

     res.end(`<h1>I'm service1 on ${os.hostname()} non unique visits ${req.session.page_views}</h1>`);

});

app.listen(8080);

Now dockerize each service like this in directories service1, service2, with a Dockerfile.
FROM node

RUN mkdir -p /usr/src/app

COPY index.js /usr/src/app

COPY package*.json ./

RUN npm install

EXPOSE 8080

CMD ["node","/usr/src/app/index"]
  1. Add a .dockerignore file in each to leave behind the modules.
node_modules

npm-debug.log
2. Build a docker image in each directory docker build -t serviceX .
3. Build a docker compose file, one level up from the service dirs, here it is
version: '3'

services:

  service1:

   image: service1 

   ports:

     - 8080

   environment:

     - SERVICE_PORTS=8080

     - COOKIE=connect.sid prefix nocache

     - VIRTUAL_HOST=*/service1

   deploy:

     replicas: 2

     update_config:

       parallelism: 5

       delay: 10s

     restart_policy:

       condition: on-failure

       max_attempts: 3

       window: 120s

   networks:

     - web



  service2:

   image: service2 

   ports:

     - 8080

   environment:

     - SERVICE_PORTS=8080

     - VIRTUAL_HOST=*/service2

   deploy:

     replicas: 2

     update_config:

       parallelism: 5

       delay: 10s

     restart_policy:

       condition: on-failure

       max_attempts: 3

       window: 120s

   networks:

     - web



  proxy:

    image: dockercloud/haproxy

Only two replicas, make it whatever you want. What's really interesting about this are two things, BOTH javascript files build a session but only one in the compose file is affinity assigned, service1.
- COOKIE=connect.sid prefix nocache
That's the default cookie name given by express-session you can of course make changes to that, prefix adds the server routing and nocache is uh, no cacheing it.
The other thing interesting is this entry, virtual hosts, these must match the /root of your services for routing purposes, both services must have this unless you are serving from a plain root e.g. "/"
 - VIRTUAL_HOST=*/service1
Run this command to launch the replicated services with affinity on service1.
  1. docker swarm init
  2. docker stack deploy --compose-file=docker-compose.yml test
  3. docker service -ls
  4. point to localhost/serviceX for results, service1 will be sticky and incrementing each time, service2 you will see a new service and session each time. Clear cookies to get over to another server1.
Here are the official options for haproxy.
http://cbonte.github.io/haproxy-dconv/1.9/configuration.html#7.1
I'd add this to git but five min was up ten minutes ago.