I’ve previously looked at the likes of Spring Boot and how comparatively bloated it could be considered for simple microservices. One of the questions that cropped up that made me think was, how does performance differ across the languages that I covered?

Whilst a language may be more suited for use in a microservice architecture due to low resource consumption, in some scenarios it will also have to be efficient at handling computationally expensive tasks. Whilst you may be able to reduce the memory demand by re-writing your Spring Boot based microservice in Python or Go, if you have to run 10x the number of instances in order to handle the same throughput then what’s the point?

The previous article in this series can be found here:

Testing our Performance

So in my previous article I constructed several hello-world style applications all written in different languages, these ran inside their own docker images on different ports and simply returned “Hello World” when their root path is hit.

In order to test performance I’m going to see how well they can cope dealing with 1000 requests per second for a total of 100 seconds apiece. The length of these tests will hopefully give a better indication as to how these services handle a consistent load. It has to be said though that this is my first real attempt at analysing performance and if my techniques are off then please let me know how to improve upon them in the comments section!

Basic Load Tests

In order to do the basic load tests I’ll be using something called vegeta which was written in Go and can be found here: https://github.com/tsenart/vegeta. This will give cool results like so:



Requests [total, rate] 100000, 1000.01

Duration [total, attack, wait] 1m40.001952896s, 1m39.998999902s, 2.952994ms

Latencies [mean, 50, 95, 99, max] 84.052687ms, 56.7525ms, 231.76403ms, 363.020153ms, 601.23415ms

Bytes In [total, mean] 1542912, 15.43

Bytes Out [total, mean] 0, 0.00

Success [ratio] 96.43%

Status Codes [code:count] 200:96432 0:3568 $ echo “GET http://localhost:9009/ " | ./attack attack -rate=1000 -duration=100s | ./attack report100000, 1000.011m40.001952896s, 1m39.998999902s, 2.952994ms84.052687ms, 56.7525ms, 231.76403ms, 363.020153ms, 601.23415ms1542912, 15.430, 0.0096.43%200:96432 0:3568

Results

Let’s walk through the results for each of the original microservices. I’ll be focusing mostly on the success ratio, and the latencies for our calls as these could be considered the most important factors for our microservices.

I should point out that the source code for all of these tests can be found in my previous article here: https://medium.com/r/?url=https%3A%2F%2Fhackernoon.com%2Fmicroservices-in-java-a-second-look-460ba3909c44

Python AIOHTTP:

Upon running vegeta against the Python 3.6 aiohttp the average CPU utilization for that particular docker image spiked to around 98% and then fluctuated between 60–90%. It managed to cope with 96.43% of the 100,000 requests that were thrown against it which was fairly impressive and the mean latency was 84.1ms spiking up to 601.2ms.



Requests [total, rate] 100000, 1000.01

Duration [total, attack, wait] 1m40.001952896s, 1m39.998999902s, 2.952994ms

Latencies [mean, 50, 95, 99, max] 84.052687ms, 56.7525ms, 231.76403ms, 363.020153ms, 601.23415ms

Bytes In [total, mean] 1542912, 15.43

Bytes Out [total, mean] 0, 0.00

Success [ratio] 96.43%

Status Codes [code:count] 200:96432 0:3568 $ echo “GET http://localhost:9009/ " | ./attack attack -rate=1000 -duration=100s | ./attack report100000, 1000.011m40.001952896s, 1m39.998999902s, 2.952994ms84.052687ms, 56.7525ms, 231.76403ms, 363.020153ms, 601.23415ms1542912, 15.430, 0.0096.43%200:96432 0:3568

Overall I was fairly impressed by how aiohttp handled the load.

Python 2.7

The Python 2.7 equivalent on the other hand really struggled, it managed to successfully respond to a meagre 15.02% of the 100,000 requests and the latencies reported were somewhat shocking. A mean latency of 5.9s and a maximum of 54s. Not ideal.



Requests [total, rate] 100000, 1000.01

Duration [total, attack, wait] 2m10.338643608s, 1m39.999326627s, 30.339316981s

Latencies [mean, 50, 95, 99, max] 5.885101113s, 39.101412ms, 31.712857638s, 46.433677648s, 54.905216337s

Bytes In [total, mean] 165275, 1.65

Bytes Out [total, mean] 0, 0.00

Success [ratio] 15.02%

Status Codes [code:count] 200:15025 0:84975 $ echo “GET http://localhost:9005/ " | ./attack attack -rate=1000 -duration=100s | ./attack report100000, 1000.012m10.338643608s, 1m39.999326627s, 30.339316981s5.885101113s, 39.101412ms, 31.712857638s, 46.433677648s, 54.905216337s165275, 1.650, 0.0015.02%200:15025 0:84975

Python 3.6

Upon running this I saw initial CPU utilization spike to around 98% and then average between 70–90%. The results show that it performed equally poorly as the Python 2.7 based counterpart. Flask just doesn’t seem up to the task of dealing with a massive number of requests per second.

It was able to handle just 11.71% of the 100,000 requests and for those requests that it did respond to it did so in an untimely fashion.



Requests [total, rate] 100000, 1000.01

Duration [total, attack, wait] 2m9.728098902s, 1m39.998999893s, 29.729099009s

Latencies [mean, 50, 95, 99, max] 5.920617368s, 33.917942ms, 31.378125586s, 46.263062803s, 50.731172785s

Bytes In [total, mean] 128832, 1.29

Bytes Out [total, mean] 0, 0.00

Success [ratio] 11.71%

Status Codes [code:count] 200:11712 0:88288 $ echo "GET http://localhost:9006/ " | ./attack attack -rate=1000 -duration=100s | ./attack report100000, 1000.012m9.728098902s, 1m39.998999893s, 29.729099009s5.920617368s, 33.917942ms, 31.378125586s, 46.263062803s, 50.731172785s128832, 1.290, 0.0011.71%200:11712 0:88288

Golang

Whilst running this, I have to say the CPU Utilization never really surpassed 55% according to docker stats. It managed to perform spectacularly well and we managed to respond to 96.28% of the 100,000 requests hammering it. Compared to it’s aiohttp equivalent which was previously our title contender it managed to respond to these requests, roughly 4 times quicker based on mean latencies.



Requests [total, rate] 100000, 1000.01

Duration [total, attack, wait] 1m40.002449566s, 1m39.99899971s, 3.449856ms

Latencies [mean, 50, 95, 99, max] 71.700782ms, 36.891869ms, 221.566909ms, 239.26289ms, 529.145223ms

Bytes In [total, mean] 1059102, 10.59

Bytes Out [total, mean] 0, 0.00

Success [ratio] 96.28%

Status Codes [code:count] 200:96282 0:3718 $ echo “GET http://localhost:9008/ " | ./attack attack -rate=1000 -duration=100s | ./attack report100000, 1000.011m40.002449566s, 1m39.99899971s, 3.449856ms71.700782ms, 36.891869ms, 221.566909ms, 239.26289ms, 529.145223ms1059102, 10.590, 0.0096.28%200:96282 0:3718

Normal Java

The lightweight Java server managed to respond to 99.58% of the 100,000 requests thrown at it with fairly decent latencies. 49.5ms for the mean latency and spiking up to 577.9ms for a maximum latency. These results mean that Go has been knocked off it’s top spot on our list and Java can claim the crown as the most performant language in this particular use case. Go slightly edged out Java in terms of response times for 50% of all results but overall Java takes the cake.



Requests [total, rate] 100000, 1000.01

Duration [total, attack, wait] 1m40.059716738s, 1m39.998999562s, 60.717176ms

Latencies [mean, 50, 95, 99, max] 49.486463ms, 47.494146ms, 62.721628ms, 169.727284ms, 577.917062ms

Bytes In [total, mean] 1194924, 11.95

Bytes Out [total, mean] 0, 0.00

Success [ratio] 99.58%

Status Codes [code:count] 200:99577 0:423 $ echo “GET http://localhost:9007/ " | ./attack attack -rate=1000 -duration=100s | ./attack report100000, 1000.011m40.059716738s, 1m39.998999562s, 60.717176ms49.486463ms, 47.494146ms, 62.721628ms, 169.727284ms, 577.917062ms1194924, 11.950, 0.0099.58%200:99577 0:423

Spring Boot

For posterity’s sake, I need to include the Spring Boot results as well. Before I ran the test I had a feeling the large framework may struggle with the consistent load being placed upon it but considering how well the lightweight Java version performed, I did not expect the difference to be so large.

The average CPU Utilization for this docker image never really came below 100% and spiked up to 130%. The success ratio dropped to a staggering 46.28% and the latency figures were almost on par with the likes of the Python services.



Requests [total, rate] 100000, 1000.01

Duration [total, attack, wait] 2m9.693501302s, 1m39.998999835s, 29.694501467s

Latencies [mean, 50, 95, 99, max] 3.71126393s, 166.507724ms, 30.098205716s, 30.525277942s, 31.460032818s

Bytes In [total, mean] 1749871, 17.50

Bytes Out [total, mean] 0, 0.00

Success [ratio] 46.28%

Status Codes [code:count] 200:46278 0:53722 $ echo “GET http://localhost:9011/greeting " | ./attack attack -rate=1000 -duration=100s | ./attack report100000, 1000.012m9.693501302s, 1m39.998999835s, 29.694501467s3.71126393s, 166.507724ms, 30.098205716s, 30.525277942s, 31.460032818s1749871, 17.500, 0.0046.28%200:46278 0:53722

Conclusion

These tests represent a very simplistic view as to how each of these “microservices” perform under an intense amount of load over a somewhat lengthy duration of time. I was actually most surprised and impressed by the way the aiohttp server was able to handle itself considering Python itself isn’t known for its speed.

The results do highlight though that even though Java remains one of the top languages for writing highly performant microservices, you can very quickly kill any performance benefits by employing heavy frameworks such as Spring Boot as the foundations to your services.

There was also a very minimal difference between the likes of Go and the lightweight Java service. Whilst I may personally favour Go as my language of choice over the two, Java certainly seems like a very good option for this type of work.