nginx

Having played with nginx both as a simple file server and a reverse proxy, it's time to explore another way it can easily improve your application. Using it as a load balancer can give your application a boost in the terms of availability, while not requiring that much of the configuration. Just take a look.

Load balancing

Long story short, a load balancer is an entry point to your system, which takes all incoming requests and pushes them forward to the application servers. Although this may seem like a bad idea to rely on one server to handle everything, in reality, those servers do just a tiny bit of processing which makes them more reliable than those servers that do some heavy work. On the other hand, having such a server gives you the ability to handle and distribute traffic more efficiently and, as it acts as a layer of abstraction, allows you to make some changes in the underlying servers without showing any downtime to the end users.

When it comes to traffic distribution, there are several algorithms that are widely used to make your system as reliable as possible. The simplest approach is called round-robin, which spreads the requests equally across all application servers. It might be modified with weights, so that one of the servers may take N-times more requests than others (eg. it's a faster machine and is capable of doing so). Other popular solutions include using IP hashes (so that request coming from given IP always hits the same server), or measures defining how much workload is currently on each machine (eg. least connection, least response time, etc.)

Sample project

In order to see how load balancing works in action, we need to create a few application servers and one load balancer node which will use nginx to do the heavy work.

First, we create a simple application in Go, which will greet users using its hostname to see if different servers handle our requests:

package main

import (
    "net/http"
    "os"
)

func main() {
    hostname, err := os.Hostname()
    if err != nil {
        panic("no hostname?!")
    }

    http.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
        rw.Write([]byte("Howdy from " + hostname))
    })

    http.ListenAndServe(":9000", nil)
}

Next, we need to prepare a Dockerfile which will take a binary and start it on container startup:

FROM alpine:latest

# Add application and make it executable
ADD hello-go /hello-go
RUN chmod +x /hello-go

# Run the web server
EXPOSE 9000
CMD "/hello-go"

Then, for convenience, we need to compile our main.go to an Alpine-friendly version:

CGO_ENABLED=0 go build -a -installsuffix cgo -o hello-go .

Once we have everything ready for running the application servers, let's move to the load balancer. In the configuration file, we need to define two directives. First, upstream, defines a list of servers that will serve the application. We'll use docker-compose later, so we can use service names here (as Docker's DNS will resolve them anyway):

...
http {
    upstream hellogo {
        server app1:9000;
        server app2:9000;
        server app3:9000;
    }
    ...
}

Once we do that, we need to create a server directive, where we state that all requests coming in will be directed to the servers defined in upstream:

...
http {
    ...
    server {
        listen 80;
        location / {
            proxy_pass http://hellogo;
        }
    }
}

Now we can create a Dockefile to add our configuration to nginx:alpine image:

FROM nginx:alpine
MAINTAINER "Paweł Słomka <pslomka@pslomka.com>"

COPY nginx.conf /etc/nginx/nginx.conf

Finally, we can now create our infrastructure in docker-compose.yml:

version: '2'
services:
lb:
    build: lb
    ports:
     - "10080:80"
app1:
    build: app
app2:
    build: app
app3:
    build: app

Showtime

Now we can start it all up:

docker-compose up

and see that while we hit the same URL, we get different greetings:

$ curl http://localhost:10080
Howdy from cf77b51ef2ca // app1
$ curl http://localhost:10080
Howdy from a97e2e9d9af2 // app2
$ curl http://localhost:10080
Howdy from 2d3b69903b38 // app3
$ curl http://localhost:10080
Howdy from cf77b51ef2ca // app1
$ curl http://localhost:10080
Howdy from a97e2e9d9af2 // app2
$ curl http://localhost:10080
Howdy from 2d3b69903b38 // app3

It works!