Building a load balancer with concurrent patterns

When we built our server pinging application earlier in this chapter, it was probably pretty easy to imagine taking this to a more usable and valuable space.

Pinging a server is often the first step in a health check for a load balancer. Just as Go provides a usable out-of-the-box web server solution, it also presents a very clean Proxy and ReverseProxy struct and methods, which makes creating a load balancer rather simple.

Of course, a round-robin load balancer will need a lot of background work, specifically on checking and rechecking as it changes the ReverseProxy location between requests. We'll handle these with the goroutines triggered with each request.

Finally, note that we have some dummy URLs at the bottom in the configuration—changing those to production URLs should immediately turn the server that runs this into a working load balancer. Let's look at the main setup for the application:

package main

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

const MAX_SERVER_FAILURES = 10
const DEFAULT_TIMEOUT_SECONDS = 5
const MAX_TIMEOUT_SECONDS = 60
const TIMEOUT_INCREMENT = 5
const MAX_RETRIES = 5

In the previous code, we defined our constants, much like we did previously. We have a MAX_RETRIES, which limits how many failures we can have, MAX_TIMEOUT_SECONDS, which defines the longest amount of time we'll wait before trying again, and our TIMEOUT_INCREMENT for changing that value between failures. Next, let's look at the basic construction of our Server struct:

type Server struct {
  Name        string
  Failures    int
  InService   bool
  Status      bool
  StatusCode  int
  Addr        string
  Timeout     int
  LastChecked time.Time
  Recheck     chan bool
}

As we can see in the previous code, we have a generic Server struct that maintains the present state, the last status code, and information on the last time the server was checked.

Note that we also have a Recheck channel that triggers the delayed attempt to check the Server again for availability. Each Boolean passed across this channel will either remove the server from the available pool or reannounce that it is still in service:

func (s *Server) serverListen(serverChan chan bool) {
  for {
    select {
    case msg := <-s.Recheck:
      var statusText string
      if msg == false {
        statusText = "NOT in service"
        s.Failures++
        s.Timeout = s.Timeout + TIMEOUT_INCREMENT
        if s.Timeout > MAX_TIMEOUT_SECONDS {
          s.Timeout = MAX_TIMEOUT_SECONDS
        }
      } else {
        if ServersAvailable == false {
          ServersAvailable = true
          serverChan <- true
        }
        statusText = "in service"
        s.Timeout = DEFAULT_TIMEOUT_SECONDS
      }

      if s.Failures >= MAX_SERVER_FAILURES {
        s.InService = false
        fmt.Println("	Server", s.Name, "failed too many times.")
      } else {
        timeString := strconv.FormatInt(int64(s.Timeout), 10)
        fmt.Println("	Server", s.Name, statusText, "will check again in", timeString, "seconds")
        s.InService = true
        time.Sleep(time.Second * time.Duration(s.Timeout))
        go s.checkStatus()
      }

    }
  }
}

This is the instantiated method that listens on each server for messages delivered on the availability of a server at any given time. While running a goroutine, we keep a perpetually listening channel open to listen to Boolean responses from checkStatus(). If the server is available, the next delay is set to default; otherwise, TIMEOUT_INCREMENT is added to the delay. If the server has failed too many times, it's taken out of rotation by setting its InService property to false and no longer invoking the checkStatus() method. Let's next look at the method for checking the present status of Server:

func (s *Server) checkStatus() {
  previousStatus := "Unknown"
  if s.Status == true {
    previousStatus = "OK"
  } else {
    previousStatus = "down"
  }
  fmt.Println("Checking Server", s.Name)
  fmt.Println("	Server was", previousStatus, "on last check at", s.LastChecked)
  response, err := http.Get(s.Addr)
  if err != nil {
    fmt.Println("	Error: ", err)
    s.Status = false
    s.StatusCode = 0
  } else {
    s.StatusCode = response.StatusCode
    s.Status = true
  }

  s.LastChecked = time.Now()
  s.Recheck <- s.Status
}

Our checkStatus() method should look pretty familiar based on the server ping example. We look for the server; if it is available, we pass true to our Recheck channel; otherwise false, as shown in the following code:

func healthCheck(sc chan bool) {
  fmt.Println("Running initial health check")
  for i := range Servers {
    Servers[i].Recheck = make(chan bool)
    go Servers[i].serverListen(sc)
    go Servers[i].checkStatus()
  }
}

Our healthCheck function simply kicks off the loop of each server checking (and re-checking) its status. It's run only one time, and initializes the Recheck channel via the make statement:

func roundRobin() Server {
  var AvailableServer Server

  if nextServerIndex > (len(Servers) - 1) {
    nextServerIndex = 0
  }

  if Servers[nextServerIndex].InService == true {
    AvailableServer = Servers[nextServerIndex]
  } else {
    serverReady := false
    for serverReady == false {
      for i := range Servers {
        if Servers[i].InService == true {
          AvailableServer = Servers[i]
          serverReady = true
        }
      }

    }
  }
  nextServerIndex++
  return AvailableServer
}

The roundRobin function first checks the next available Server in the queue—if that server happens to be down, it loops through the remaining to find the first available Server. If it loops through all, it will reset to 0. Let's look at the global configuration variables:

var Servers []Server
var nextServerIndex int
var ServersAvailable bool
var ServerChan chan bool
var Proxy *httputil.ReverseProxy
var ResetProxy chan bool

These are our global variables—our Servers slice of Server structs, the nextServerIndex variable, which serves to increment the next Server to be returned, ServersAvailable and ServerChan, which start the load balancer only after a viable server is available, and then our Proxy variables, which tell our http handler where to go. This requires a ReverseProxy method, which we'll look at now in the following code:

func handler(p *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) {
  Proxy = setProxy()
  return func(w http.ResponseWriter, r *http.Request) {

    r.URL.Path = "/"

    p.ServeHTTP(w, r)

  }
}

Note that we're operating on a ReverseProxy struct here, which is different from our previous forays into serving webpages. Our next function executes the round robin and gets our next available server:

func setProxy() *httputil.ReverseProxy {

  nextServer := roundRobin()
  nextURL, _ := url.Parse(nextServer.Addr)
  log.Println("Next proxy source:", nextServer.Addr)
  prox := httputil.NewSingleHostReverseProxy(nextURL)

  return prox
}

The setProxy function is called after every request, and you can see it as the first line in our handler. Next we have the general listening function that looks out for requests we'll be reverse proxying:

func startListening() {
  http.HandleFunc("/index.html", handler(Proxy))
  _ = http.ListenAndServe(":8080", nil)

}

func main() {
  nextServerIndex = 0
  ServersAvailable = false
  ServerChan := make(chan bool)
  done := make(chan bool)

  fmt.Println("Starting load balancer")
  Servers = []Server{{Name: "Web Server 01", Addr: "http://www.google.com", Status: false, InService: false}, {Name: "Web Server 02", Addr: "http://www.amazon.com", Status: false, InService: false}, {Name: "Web Server 03", Addr: "http://www.apple.zom", Status: false, InService: false}}

  go healthCheck(ServerChan)

  for {
    select {
    case <-ServerChan:
      Proxy = setProxy()
      startListening()
      return

    }
  }

  <-done
}

With this application, we have a simple but extensible load balancer that works with the common, core components in Go. Its concurrency features keep it lean and fast, and we wrote it in a very small amount of code using exclusively standard Go.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.212.124