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.:
- http://multiply.userv.myapp:5001/api/multiply/?a=2&b=4
- http://square.userv.myapp:5002/api/square/?a=8
Finally, I’ve renamed my main program
to indicate that it’s the µService version of the amazing usquariply
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.
Leave a Reply