Microservices Gone Wild – Tech Dive Part 3

Tech Dive - Microservices

In this third post in the series about microservices, I’ll finish building my main application so that I can demonstrate a microservices-based application in action, albeit for a very basic set of functions. This post may be a little go-heavy in places, but bear with it and I’ll get to the demo soon enough. It doesn’t really matter what language is being used; I just used go because it’s good practice for me.

Building The Main Application

As a reminder, the main application will need to accept two numbers on the command line then will need to multiply the two numbers and then square that product. The two mathematical functions (multiply and square) are now offered via a REST API, and each one has its own separate Docker container with apache/PHP to service those requests.

I have created hostnames for the two microservice containers (DNS is the only smart way to address a microservice, after all) and they are accessed as:

  • multiply.userv.myapp:5001
  • square.userv.myapp:5002

The API path is /api/ followed by the name of the function, multiply or square, and the values to feed to the function are supplied as the query string. Most APIs tend to be written based on objects rather than functions, but for simplicity let’s run with this for now, e.g.:

Finally, I’ve renamed my main program usquariply to indicate that it’s the µService version of the amazing squariply application.

usquariply

May I add my usual disclaimer once more that I am not a programmer? Good, thank you. I am sure I am doing bad and inefficient things here, but the point is to make it work above all else (though I welcome corrections and tips in the comments!). If you’re not interested in the detail, just scroll on by.

// usquariply - an exciting program to calculate (a * b)^2
// microservice version

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"
    "strconv"
    "strings"
)

// Globals
var err error

// Define JSON structures for REST responses
type Answer struct {
    Result Result
    Answer int
}

type Result struct {
    Status        string
    ErrorCount    string
    ErrorMessages []string
}

// MAIN
func main() {

    // Vaguely check if the arguments exist. Not a recommended approach :-)
    str_a := os.Args[1]
    if str_a == "" {
        Usage()
    }

    str_b := os.Args[2]
    if str_b == "" {
        Usage()
    }

    // Create an HTTP client
    httpClient := &http.Client{}

    // Multiply the two numbers via REST call
    multiplyResult := doMath(*httpClient, "http://multiply.userv.myapp:5001/api/multiply/", "a="+str_a+"&b="+str_b)
    checkErr(err)

    // Square the result via REST call
    strMResult := strconv.Itoa(multiplyResult)
    squareResult := doMath(*httpClient, "http://square.userv.myapp:5002/api/square/", "a="+strMResult)
    checkErr(err)

    // Print out the result
    fmt.Printf("Result is %d\n", squareResult)

}

func Usage() {
    fmt.Println("\nSupply two integers on the command line.\n")
    os.Exit(1)
}

func doMath(client http.Client, uri string, query string) int {

    // This byte array will hold the http response
    var content []byte

    // Build full URI and issue REST call
    queryString := uri + "?" + query
    content = restRequest(client, queryString)

    // Process the received response (content) as json using the Answer structure
    jsonData := Answer{}

    // Turn json into something useful (unmarshal it)
    err = json.Unmarshal(content, &jsonData)
    if err != nil {
        fmt.Println("Error during unmarshal.")
    }
    checkErr(err)

    // Check whether our call was successful
    if jsonData.Result.Status == "failed" {
        fmt.Printf("Rest call to %s failed.\n", queryString)
    }

    // If there were errors, find out what they were and print them out
    errorCount, _ := strconv.Atoi(jsonData.Result.ErrorCount)

    if errorCount > 0 {
        error_messages := jsonData.Result.ErrorMessages
        for _, msg := range error_messages {
            fmt.Println(msg)
        }

        // Not exactly great error handling to quit on failure, but for testing purposes it's ok
        os.Exit(1)
    }

    // If we got here, it was successful, so return the Answer from the JSON
    return jsonData.Answer

}

func restRequest(client http.Client, url string) []byte {

    // Make the request
    req, err := http.NewRequest("GET", url, nil)
    checkErr(err)

    resp, err := client.Do(req)

    if err != nil {
        // Break this down a little

        if strings.Contains(err.Error(), "no such host") {
            log.Fatal("\n\nError contacting site: DNS failure.\n")
        } else if strings.Contains(err.Error(), "tcp dial") {
            log.Fatal("\n\nError contacting site: check your network connection.\n")
        }
        os.Exit(1)
    }

    if resp != nil {
        defer resp.Body.Close()
    }

    // Get the rest of the content
    var content []byte
    content, err = ioutil.ReadAll(resp.Body)

    if err != nil {
        fmt.Printf("\n\nError during data stream read: %v\n", err)
        fmt.Printf("Diagnostics:\nContent= '%s'\n", string(content))
        os.Exit(1)
    }

    // Check we actually got some data back
    if string(content) == "" {
        fmt.Printf("\n\nError: REST call returned empty string.\n")
        os.Exit(1)
    }

    // Return the byte array we got from the web server
    return content
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

Testing The New Application

One quick build later, and I have an executable to test:

$ usquariply> ./usquariply 2 4
Result is 64
$ usquariply> ./usquariply 5 8
Result is 1600
$ usquariply> ./usquariply 17 6227
Result is 11206127881
$ usquariply> ./usquariply -3 -9
Result is 729

That seems to work quite nicely.

Comparing Speeds – Monolithic Versus Microservices

It would be rude not to see which is faster, and I doubt the result will be a surprise. I’ll run five tests of each type to make sure the results are vaguely consistent:

$ squariply> time ./squariply 2 4
Result is 64
./squariply 2 4  0.00s user 0.00s system 69% cpu 0.005 total
$ squariply> time ./squariply 2 4
Result is 64
./squariply 2 4  0.00s user 0.00s system 58% cpu 0.006 total
$ squariply> time ./squariply 2 4
Result is 64
./squariply 2 4  0.00s user 0.00s system 58% cpu 0.006 total
$ squariply> time ./squariply 2 4
Result is 64
./squariply 2 4  0.00s user 0.00s system 58% cpu 0.006 total
$ squariply> time ./squariply 2 4
Result is 64
./squariply 2 4  0.00s user 0.00s system 59% cpu 0.006 total



$ usquariply> time usquariply 2 4
Result is 64
usquariply 2 4  0.00s user 0.01s system 35% cpu 0.027 total
$ usquariply> time usquariply 2 4
Result is 64
usquariply 2 4  0.00s user 0.01s system 40% cpu 0.023 total
$ usquariply> time usquariply 2 4
Result is 64
usquariply 2 4  0.00s user 0.01s system 36% cpu 0.026 total
$ usquariply> time usquariply 2 4
Result is 64
usquariply 2 4  0.00s user 0.01s system 37% cpu 0.025 total
$ usquariply> time usquariply 2 4
Result is 64
usquariply 2 4  0.00s user 0.01s system 36% cpu 0.030 total

The monolithic app takes roughly 0.06 seconds to run versus an average of around 0.26 seconds for the microservices version, which makes the microservices-based app around 4 times slower than the monolithic application. Clearly though, this is not a representative test for microservices in general. The overhead involved in opening an HTTP session and processing the JSON response by far exceeds the time spent on the calculation itself, so the overhead here represents a larger percentage of the run time than it would with a more complex function. Where a microservice performs a function that is more complex and takes longer to run, overhead as a percentage of the over all processing time decreases, and the numbers will start to look more similar.

To prove this, imagine that our mathematical functions (multiply and square) are in fact terribly complex, and to multiply two numbers will take 2 seconds, and to square them will take 3 seconds. For those of you thinking “But wait, squaring a number is the same as running the multiply service with A and B set to the same value so how can it take a second longer?” can have a gold star and be quiet again while I simulate processing time in both the monolithic and the microservices models by adding in delays:

Monolithic

To add a delay, I add the time module as an import, then use its Sleep function:

import (
    ...
    "time"
    ...
 )
...
    multiplyResult := int_a * int_b
    time.Sleep(2 * time.Second)

    squareResult := multiplyResult * multiplyResult
    time.Sleep(3 * time.Second)

Microservices

For the microservice version, I will add the delay in the PHP microservices themselves:

...
// Multiply - add sleep
if ($function === "multiply") {
    sleep(2);
    ...
...
// Square - add sleep
if ($function === "square") {
    sleep(3);
    ...

Note that for usquariply, in order to change the behavior of the microservices, I did not have to alter the main application in any way; it was all done within the microservices themselves.

Comparing Speeds – Take 2

Let’s try our new, extremely slow and complex functions. The single measurements shown below show representative values from a number of samples:

$ squariply> time ./squariply 2 4
Result is 64
./squariply 2 4 0.00s user 0.00s system 0% cpu 5.010 total

$ usquariply> time ./usquariply 2 4
Result is 64
./usquariply 2 4 0.00s user 0.01s system 0% cpu 5.032 total

This time the microservice is 0.44% slower than the monolithic code, compared to the 400% slower in the original test. So don’t assume that microservices code must be slow.

Add “Deployed Microservices-Based Application” To Résumé

This is all great, but so what? It seems like I’ve had to write a whole bunch more code, I now need three machines (or VMs / containers) instead of one, and I also need to add code to handle delays and failures over the network. This is true, and it’s one reason why microservices are not the right fit for every application, nor for ever dev team. However, maybe there are some other benefits to all of this, and that’s what I’ll look at in the next post.

Be the first to comment

Leave a Reply

Your email address will not be published.


*


This site uses Akismet to reduce spam. Learn how your comment data is processed.