With the release of Spring Boot 3.2, support for virtual threads has officially been added!
So I thought I would run load tests on two Spring Boot applications, one using the reactive paradigm,
the other using precisely virtual threads. I created two simple applications that perform common operations;
data retrieval from a database and REST call to an external service.
Specifically, the two applications have the following REST API in GET: /users?name=${name}&surname=${surname}
.
This API:
- performs the following query on a Postgres table:
SELECT * FROM USERS WHERE NAME=${name} AND SURNAME=${surname}
- for each resulting row of the query (which in practice will always be one), a REST call is made to an HTTP client that given the surname as input, returns the latter in uppercase. This HTTP service responds in about 300 milliseconds
- finally a list of
name,surname
pairs with the surname in uppercase is returned to the client.
Two lines about WebFlux
Spring WebFlux uses the Reactor
library, an implementation of Reactive Streams. This API makes it possible to create
non-blocking applications. Instead of using the classic "thread per request" paradigm, it uses that of
"event-loop" so dear to Javascript. With this approach it is possible to manage computation resources with more optimization,
since we do not have threads "waiting".
The reactive approach involves a type of functional programming, which for those coming from the world of pure Java, is not easy to digest.
Two lines about Virtual Threads
Virtual threads are officially available with Java version 21, thanks to the Loom project.
In summary, prior to this feature, Java threads were associated 1:1 with operating system threads.
Creating threads in Java therefore was wasteful, as was keeping them stuck waiting for something!
With Virtual Threads this is no longer true! Virtual Threads are always instances of java.lang.Thread
that are, however, not
tied 1:1 to operating system threads. When the code of a virtual thread encounters a blocking operation, the JVM will
suspend that virtual thread until the result of the operation is available. The operating system thread associated
to that virtual thread is "freed" for use by another virtual thread.
Virtual threads are lightweight objects; we can create thousands of them.
They are suitable for applications that have many blocking operations. They are not suitable for CPU-intensive applications.
Also, with the fact that we can create thousands of threads, it might be risky to abuse them when we want to store something
in the context of threads (think about the use of ThreadLocal).
Test preparation data
- Machine used: AWS t3.micro (2 vCPU, 1 GiB RAM) with Amazon Linux 2023 operating system.
- Database: Postgres table with about 1800 records.
- Network: the server application (the WebFlux one and the "virtual thread" one), mock-client application and Postgres database on the same VPC.
- HTTP Client used: WebClient for WebFlux, RestClient for virtual thread.
- Database connection drivers: R2DBC for WebFlux (with Spring Data R2DBC), JDBC for "virtual thread" (with Spring Data JDBC).
- Spring Boot Version: 3.2.0
- Applications Code: spring-virtual-thread-test
- Tool used for load testing: jMeter
For test results, the Throughput parameter will be taken into account (you can find the parameter definition
here) and the average response time, expressed in milliseconds.
The higher the Throughput value, the better.
The lower the value of Average Response Time, the better.
The test uses random values of name and surname (values, however, present in the Database) for each HTTP call made.
In each test, for each type of parameter, the winner will be colored green, the loser red. They will both be colored gray if there is no winner.
First load test: 100 concurrent users, repeated 20 times
- Number of Threads (users): 100.
- Loop Count: 20.
- Total requests: 2000.
Average Response Time: WebFlux 351 - VT 350 (no winner)
WebFlux |
Virtual Threads |
Throughput: WebFlux 270.4 - VT 258.2 (+ 12.2 for WebFlux)
WebFlux |
Virtual Threads |
CPU/Heap:
WebFlux |
Virtual Threads |
Second load test: 200 concurrent users, repeated 20 times
- Number of Threads (users): 200.
- Loop Count: 20.
- Total requests: 4000.
Average Response Time: WebFlux 374 - VT 436 (- 62 for WebFlux)
WebFlux |
Virtual Threads |
Throughput: WebFlux 453.6/sec - VT 390.9/sec (+ 62.7 for WebFlux)
WebFlux |
Virtual Threads |
CPU/Heap:
WebFlux |
Virtual Threads |
Third load test: 400 concurrent users, repeated 20 times
- Number of Threads (users): 400.
- Loop Count: 20.
- Total requests: 8000.
Average Response Time: WebFlux 528 - VT 549 (- 21 for WebFlux)
WebFlux |
Virtual Threads |
Throughput: WebFlux 611.7/sec - VT 595.8/sec (+ 15.9 for WebFlux)
WebFlux |
Virtual Threads |
CPU and heap usage:
WebFlux |
Virtual Threads |
Fourth load test: 800 concurrent users, repeated 20 times
- Number of Threads (users): 800.
- Loop Count: 20.
- Total requests: 16000.
Average Response Time: WebFlux 999 - VT 998 (no winner)
WebFlux |
Virtual Threads |
Throughput: WebFlux 653.2/sec - VT 614.8/sec (+ 38.4 for WebFlux)
WebFlux |
Virtual Threads |
CPU and heap usage:
WebFlux |
Virtual Threads |
Conclusions
From these tests, we see the fact that the more concurrent requests increase, the more WebFlux takes advantage over Virtual Threads, in terms of Throughput. On average response times, again WebFlux wins by a small margin, but I did not find a pattern as for Throughput.
On the CPU, it seems that WebFlux uses more CPU at low load than VT. By increasing the load of requests, the values of WebFlux and VT are comparable. Finally, with regard to Heap utilization, it appears that VT uses more memory than WebFlux.
Under these conditions, the winner of this challenge is WebFlux!
However, this article is not intended to express a preference for using WebFlux at the expense of virtual threads.
The use of one or the other depends on project to project, and team to team (how confident you are in the reactive approach).
More articles on Spring: Spring.
Articles about Docker: Docker.
Recommended books on Spring, Docker, and Kubernetes:
- Cloud Native Spring in Action: https://amzn.to/3xZFg1S
- Pro Spring 6 (Spring da zero a hero): https://amzn.to/41AIpD7
- Docker: Sviluppare e rilasciare software tramite container: https://amzn.to/3AZEGDI
- Kubernetes. Guida per gestire e orchestrare i container: https://amzn.to/3RvLZKe