..

Building a Simple Load Balancer Part 2

round I am quite late on the second part. I had implemented it just after writing the first part but forgot to write the second part. So, here we are.

Checkout the first part, if you haven’t already. It’s nothing fancy, very basic and simple Go. The third iteration of this post (if I get to it) would probably be implementing other algorithms, apart from round robin.

Implementing round robin is dead simple. You just create a slice of servers and keep going over each index of servers.


Implementation

So, we will be running a load balancer (remember, load balancer is also just a server) and forward requests to the servers in the slice or array one by one. Simple right? Let’s dive in.

Create a separate folder named balancer and create a new file balancer.go

First, we will create a slice. Before that, make sure that the servers are running on the desired port.

I have added a port flag for the previous program which makes it easy to test this on a single machine.

go run server.go -port 8081
go run server.go -port 8082
...

And then declare a slice:

var servers = []string{
    "127.0.0.1:8081",
    "127.0.0.1:8082",
}

Now, as we did in the previous part, create a simple HTTP server with route to “/hello”.

Note: I used /hello endpoint in the last post and that is why I am using /hello here. You can use any other routes as well.

func main() {
    http.HandleFunc("/hello", loadBalancer)

    log.Println("starting load balancer at 8000")
    log.Fatal(http.ListenAndServe(":8000", nil))
}

All that’s left to do is configure the route to redirect or so to say act as a proxy (just like nginx) for the underlying servers.

How do we approach it? First let’s make a counter. We will increment it after choosing the server.

var counter uint64

For selecting the server, we can use modulo (%) operator. The purpose of using the modulo operator (%) here is to ensure that the index variable always falls within the range of valid server indices (0 to len(servers)-1).

index := counter % uint64(len(servers))
serverURL := servers[index]
counter++

We got the address, now we will redirect the request to the desired server. For that, we can use NewSingleHostReverseProxy function from httputil package.

newhost

proxy := httputil.NewSingleHostReverseProxy(&url.URL{
	Scheme: "http", Host: serverURL
})
proxy.ServeHTTP(w, r)

After that, just print out the address of the client and the server.

fmt.Printf("Received request from %s, forwarding to %s\n", 
r.RemoteAddr, serverURL)

The complete program:

package main

import (
    "fmt"
    "log"
    "net/http"
    "net/http/httputil"
    "net/url"
)

var servers = []string{
    "127.0.0.1:8081",
    "127.0.0.1:8082",
}

var counter uint64

func main() {
    http.HandleFunc("/hello", loadBalancer)
    log.Println("starting load balancer at 8000")
    log.Fatal(http.ListenAndServe(":8000", nil))
}

func loadBalancer(w http.ResponseWriter, r *http.Request) {
    index := counter % uint64(len(servers))
    serverURL := servers[index]
    counter++

    proxy := httputil.NewSingleHostReverseProxy(&url.URL{
		Scheme: "http", Host: serverURL
		})
    proxy.ServeHTTP(w, r)

    fmt.Printf("Received request from %s, forwarding to %s\n",
	r.RemoteAddr, serverURL)
}


Testing time

Run the load balancer:

go run balancer/balancer.go

balancer

Send a curl request to it:

curl 127.0.0.1:8000/hello

curl

Let’s see the server log, we should get the headers and “hello world!” message that we implemented in the previous part.

balance

Score! We got the forwarded request to the desired server. We can make multiple curl requests to verify if round robin is working.

verify

Seems to be working as expected! In the next part, I will explore different load balancing algorithms and try to implement it.

Keep experimenting!