Cookies

Esta web utiliza cookies para obtener datos estadísticos de la navegación de sus usuarios. Si continúas navegando consideramos que aceptas su uso.

Midiendo el rendimiento de un servicio Java 21 con virtual threads.

Vuelvo por aquí para compartir los resultados de una comparativa que tenía curiosidad por realizar, comparar la escalabilidad de un microservicio simple desarrollado con Spring Boot. La idea de este ejercicio es comprobar como afecta a la disponibilidad del servicio utilizar la flamante funcionalidad incorporada en Java 21, los Virtual Threads.

El servicio se va a encargar de ofrecer una API Rest sencilla para almacenar, leer, modificar y borrar ubicaciones en una base de datos PostgreSQL. Podemos consultar el código en el repositorio de Github spring-boot-location-api.

Para desarrollar este servicio he utilizado el stack tradicional de Spring Boot, con los starters de Spring Web y Spring Data JDBC. Para los más curiosos, en la rama webflux-r2dbc del repositorio GIT he añadido también una implementación equivalente reactiva, usando Spring Webflux y Spring Data R2DBC.

Como lanzar los tests de carga

Como última pieza del puzzle, para poder medir la disponibilidad con o sin virtual threads usaré el programa de línea de comandos siege que ofrece una interfaz sencilla para poder lanzar tests de carga. A continuación veremos un ejemplo:

siege -b -c 1200 -t 60s http://127.0.0.1:8080/location/1

Este comando lanzará 1.200 peticiones HTTP concurrentes (opción -c) contra el endpoint especificado durante 60 segundos (opción -t). Con él podremos medir la disponibilidad del endpoint que responde a las peticiones GET con las diferentes configuraciones del microservicio.

Virtual Threads y microservicios bloqueantes

Vamos a comprobar cuál es el beneficio de utilizar los Virtual Threads en una aplicación bloqueante tradicional. Las aplicaciones bloqueantes tradicionales suelen utilizar un hilo para atender cada una de las peticiones que reciben. La JVM por defecto utiliza un hilo del sistema operativo para atender cada una de estas peticiones. Activando los Virtual Threads, la máquina Java escalará mejor ante un escenario en el que varios hilos estén bloqueados por la Entrada/Salida, ya que los Virtual Threads están específicamente optimizados para estas cargas de trabajo y no necesitan los mismos recursos que un hilo del sistema operativo.

En aplicaciones Spring Boot podemos activar los Virtual Threads simplemente añadiendo la property:

spring.threads.virtual.enabled=true

Comparativa del método GET

Veamos cuales son los resultados de nuestra API de ubicaciones con y sin Virtual Threads enviando una petición de consulta a nuestra base de datos.

Test sin Virtual Threads

{
    "transactions":                        53754,
    "availability":                        98.75,
    "data_transferred":                     8.61,
    "response_time":                        1.10,
    "transaction_rate":                   901.61,
    "throughput":                           0.14,
    "concurrency":                        989.58,
    "successful_transactions":             53764,
    "failed_transactions":                   681,
    "longest_transaction":                 31.17,
    "shortest_transaction":                 0.00
}

Test con Virtual Threads

{
    "transactions":                        60490,
    "availability":                        99.24,
    "data_transferred":                     9.69,
    "response_time":                        0.99,
    "transaction_rate":                  1011.20,
    "concurrency":                        998.96,
    "successful_transactions":             60509,
    "failed_transactions":                   465,
    "longest_transaction":                 12.33,
    "shortest_transaction":                 0.00 
}                                                    

La disponibilidad ofrecida por la solución con virtual threads como podemos ver es superior. Además, es capaz de responder a un 13% más de peticiones en el mismo intervalo de tiempo por lo que sin ningún cambio en nuestro código podemos obtener una escalabilidad de este método mucho mayor.

Comparativa del método POST

Ahora veamos el rendimiento del endpoint de almacenamiento de ubicaciones. Este test siempre se ha hecho con la tabla vacía para dar unas condiciones iguales entre la ejecución con virtual threads y sin virtual threads.

Test sin Virtual Threads

{   
    "transactions":                        45858,
    "availability":                        98.29,
    "data_transferred":                     5.51,
    "response_time":                        1.28,
    "transaction_rate":                   772.28,
    "concurrency":                        988.10,
    "successful_transactions":             45885,
    "failed_transactions":                   800,
    "longest_transaction":                 11.32,
    "shortest_transaction":                 0.00
}

Test con Virtual Threads

{
    "transactions":                        53267,
    "availability":                        99.28,
    "data_transferred":                     6.40,
    "response_time":                        1.11,
    "transaction_rate":                   894.79,
    "concurrency":                        996.25,
    "successful_transactions":             53277,
    "failed_transactions":                   385,
    "longest_transaction":                 35.83,
    "shortest_transaction":                 0.00
}

En este caso la disponibilidad ofrecida por la solución con virtual threads también es superior, siendo capaz de atender un 16% más de peticiones.

Conclusiones

Definitivamente, los virtual threads suponen una ventaja para nuestros servicios Java bloqueantes. Si en tus proyectos se está utilizando ya Java 21 o migrar aquellos que más carga reciben a esta versión no supone un gran esfuerzo, podría ser una buena idea valorar empezar a utilizar los virtual threads en tus servicios Java.