A comparison of technologies in a typical microservice scenario, focusing on resource consumption under high load.
A common scenario for microservices is that they orchestrate a number of backend services. Often those services are legacy with high latency. The goal of this exercise is to highlight the differences between several tech choices in terms of how well they perform in that situation.
We're focusing on a RESTful HTTP API, which for each incoming request, makes a small number of upstream API requests, collects data from the responses and responds to the original request. The majority of the time taken to respond is waiting for responses from upstream requests. Therefore the performance is IO-bound. We believe this is a fairly typical scenario in microservice architectures in large-scale enterprise systems.
We're assuming these services run under cluster orchestration (e.g. Kubernetes) and inside Linux containers (e.g. Debian in Docker). On that basis we're focusing on:
The experiment runs on a Kubernetes cluster (GKE in GCP), running 3 nodes:
wrk
)The service under test for each incoming request will make n
upstream requests in parallel to a simulated back-end, wait for responses and return aggregated data to the load source.
The simulated back-end will hold the connection open, wait for t
seconds and respond.
Running this in Kubernetes adds a significant amount of complexity and overhead, but because each test is under the same conditions, this should not matter. Note that the absolute values are therefore not significant in any way; only the relative results matter.
@Async
). Thread pool concurrency model (blocking IO) for incoming and outgoing requestsWe are comparing Java to the other technologies because it is a common choice made by our clients for building microservices.
We expect the latter 3 to outperform the former 2 and are interested in by how much.
We will also test the performance limits of the legacy back-end to ensure that capacity here is not a problem. However, tech choice is much less relevant here, so we choose Go and Rust, for self-indulgent purposes :-)
The back-end service will have excess resources available to it.
GET /api
, application/json)
# install drill if not already installed
cargo install drill
# build and start relevant backend in a Docker container
(cd legacy-backend-rust && make docker && make docker-run)
# or...
(cd legacy-backend-go && make docker && make docker-run)
# run benchmark
drill --benchmark backend.yaml --stats --quiet