Con la versione di Spring Boot 3.2, è stato ufficialmente aggiunto il supporto ai virtual threads!
Ho pensato quindi di effettuare dei test di carico su due applicazioni Spring Boot, una che utilizza il paradigma reattivo,
l'altra che utilizza appunto i virtual thread. Ho creato due applicazioni semplici che effettuano operazioni comuni;
recupero dati da un database e chiamata REST verso un servizio esterno.
In particolare, le due applicazioni hanno la seguente API REST in GET: /users?name=${name}&surname=${surname}
.
Questa API:
- effettua la seguente query su una tabella di Postgres:
SELECT * FROM USERS WHERE NAME=${name} AND SURNAME=${surname}
- per ogni riga risultante della query (che nella pratica sarà sempre una), viene effettuata una chiamata REST a un client HTTP che dato in input il surname, restituisce quest'ultimo in uppercase. Questo servizio HTTP risponde in circa 300 millisecondi
- infine viene restituito al client una lista di coppie
name,surname
col surname in uppercase.
Due righe su WebFlux
Spring WebFlux utilizza la libreria Reactor
, una implementazione di Reactive Streams. Queste API permettono di creare
applicazioni non bloccanti. Invece di utilizzare il classico paradigma "thread per request", utilizza quello de
"l'event-loop" tanto caro a Javascript. Con questo approccio è possibile gestire le risorse computazioni con più ottimizzazione,
poiché non abbiamo thread che "aspettano".
L'approccio reactive implica un tipo di programmazione funzionale, che per chi viene dal mondo di Java puro, non è facile da digerire.
Due righe sui Virtual Threads
I virtual thread sono disponibili ufficialmente con la versione 21 di Java, grazie al progetto Loom.
In sintesi, prima di questa feature, i thread Java erano associati 1:1 con i thread del sistema operativo.
Creare thread in Java quindi era dispendioso, così come tenerli bloccati in attesa di qualcosa!
Con i Virtual Threads questo non è più vero! I virtual thread sono sempre istanze di java.lang.Thread
che però non sono
legati 1:1 ai thread del sistema operativo. Quando il codice di un virtual thread incontra un'operazione bloccante, la JVM
sospende quel thread virtuale finché il risultato dell'operazione non sarà disponibile. Il thread del sistema operativo associato
a quel virtual thread viene "liberato" per essere utilizzato da un altro virtual thread.
I virtual thread sono oggetti leggeri, ne possiamo creare a migliaia.
Sono adatti per applicazioni che hanno molte operazioni bloccanti. Non sono adatte per applicazioni CPU-intensive.
Inoltre, col fatto di poter creare migliaia di thread, potrebbe essere rischioso abusare di loro quando vogliamo memorizzare qualcosa
nel contesto dei thread (si pensi all'uso dei ThreadLocal).
Dati sulla preparazione dei test
- Macchina utilizzata: AWS t3.micro (2 vCPU, 1 GiB di RAM) con sistema operativo Amazon Linux 2023.
- Database: Tabella di Postgres con circa 1800 record.
- Rete: l'applicazione server (quella WebFlux e quella "virtual thread"), applicazione mock-client e database Postgres sulla stessa VPC.
- HTTP Client utilizzati: WebClient per WebFlux, RestClient per virtual thread.
- Driver connessione database: R2DBC per WebFlux (con Spring Data R2DBC), JDBC per "virtual thread" (con Spring Data JDBC).
- Versione Spring Boot: 3.2.0
- Codice applicazioni: spring-virtual-thread-test
- Tool utilizzato per i test di carico: jMeter
Per i risultati dei test, verrà preso in considerazione il parametro Throughput (puoi trovare la definizione del parametro
qui) e il tempo medio di risposta, espresso in millisecondi.
Più è alto il valore di Throughput, meglio è.
Più è basso il valore del tempo medio di risposta, meglio è.
Il test utilizza dei valori casuali di name e surname (valori però presenti nel Database) per ogni chiamata HTTP effettuata.
In ogni test, per ogni tipologia di parametro, il vincitore verrà colorato di verde, il perdente di rosso. Verranno colorati entrambi di grigio se non c'è un vincitore.
Primo test di carico: 100 utenti concorrenti, ripetuto 20 volte
- Number of Threads (users): 100.
- Loop Count: 20.
- Total requests: 2000.
Average Response Time: WebFlux 351 - VT 350 (nessun vincitore)
WebFlux |
Virtual Threads |
Throughput: WebFlux 270.4 - VT 258.2 (+ 12.2 per WebFlux)
WebFlux |
Virtual Threads |
CPU/Heap:
WebFlux |
Virtual Threads |
Secondo test di carico: 200 utenti concorrenti, ripetuto 20 volte
- Number of Threads (users): 200.
- Loop Count: 20.
- Total requests: 4000.
Average Response Time: WebFlux 374 - VT 436 (- 62 per WebFlux)
WebFlux |
Virtual Threads |
Throughput: WebFlux 453.6/sec - VT 390.9/sec (+ 62.7 per WebFlux)
WebFlux |
Virtual Threads |
CPU/Heap:
WebFlux |
Virtual Threads |
Terzo test di carico: 400 utenti concorrenti, ripetuto 20 volte
- Number of Threads (users): 400.
- Loop Count: 20.
- Total requests: 8000.
Average Response Time: WebFlux 528 - VT 549 (- 21 per WebFlux)
WebFlux |
Virtual Threads |
Throughput: WebFlux 611.7/sec - VT 595.8/sec (+ 15.9 per WebFlux)
WebFlux |
Virtual Threads |
CPU and heap usage:
WebFlux |
Virtual Threads |
Quarto test di carico: 800 utenti concorrenti, ripetuto 20 volte
- Number of Threads (users): 800.
- Loop Count: 20.
- Total requests: 16000.
Average Response Time: WebFlux 999 - VT 998 (nessun vincitore)
WebFlux |
Virtual Threads |
Throughput: WebFlux 653.2/sec - VT 614.8/sec (+ 38.4 per WebFlux)
WebFlux |
Virtual Threads |
CPU and heap usage:
WebFlux |
Virtual Threads |
Conclusioni
Da questi test, si evince il fatto che più aumentano le richieste concorrenti, più WebFlux prende vantaggio sui Virtual Threads, in termini di Throughput. Sui tempi medi di risposta, anche qui vince di poco WebFlux, ma non ho trovato un pattern come per il Throughput.
Sulla CPU, sembra che WebFlux utilizzi più CPU a basso carico rispetto a VT. Aumentando il carico di richieste, i valori di WebFlux e VT sono equiparabili. Infine, per quanto riguarda l'utilizzo dell'Heap, sembra che VT utilizzi più memoria rispetto a WebFlux.
A queste condizioni, il vincitore di questa sfida è WebFlux!
Tuttavia, questo articolo non vuole esprimere un preferenza sull'uso di WebFlux a discapito dei virtual thread.
L'utilizzo dell'uno o dell'altro dipende da progetto a progetto, e da team a team (quanto siete confidenti nell'approccio reactive).
Altri articoli su Spring: Spring.
Articoli su Docker: Docker.
Libri consigliati su Spring, Docker e 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