Blog

Threads Virtuales: JMeter y Project Loom

Threads Virtuales: JMeter y Project Loom - Guía Introductoria

Motivación:

  • OpenJDK 19, que será publicada en setiembre de 2022, incorporará una nueva solución disruptiva: Threads Virtuales, aka Project Loom.
  • Queremos ver si es beneficioso usar Threads Virtuales en una aplicación como JMeter para mejorar su rendimiento.
  • Queremos experimentar con JMeter DSL como plataforma de creación de prototipos para la personalización de JMeter.

Para explorar dicha motivación, en este artículo vamos a tratar: una introducción general del estado actual de JMeter y otras alternativas, una visión general de alto nivel de Project Loom, cómo usar JMeter DSL para implementar un thread group personalizado que use Threads Virtuales, y los resultados y conclusiones de nuestros experimentos como también algunas ideas a futuro.

Introducción

Desde la primera liberación de jmeter-java-dsl, tenemos en mente el combinar todos los beneficios de JMeter con los que nos proporcionan otras herramientas de testing de performance como GatlingTaurusk6, etc. Satisfacer esta necesidad, que logramos comprobar luego de interactuar con la comunidad, resulta beneficioso para cualquier persona que quiera hacer pruebas de rendimiento.

Uno de los puntos que diferencia a JMeter de otras herramientas es su modelo de concurrencia (cómo ejecuta peticiones en paralelo).

JMeter usa Thread Pools, que son el estándar en Java (y muchos otros lenguajes) para realizar procesamiento en paralelo. Pero este modelo de concurrencia presenta un inconveniente significativo: es costoso en recursos computacionales. Para cada thread de JMeter, se crea uno para Java y por cada thread de Java se usa uno del SO (Sistema Operativo).

Los threads del SO son relativamente pesados, comparados con algunas alternativas, tanto en memoria como en CPU (mientras realizan cambio de contexto). También algunos SO imponen límites en el número de threads que se pueden crear, limitando efectivamente a JMeter.

Algunas opciones para evitar esta “ineficiencia” o limitación, implican el uso de alguna biblioteca alternativa o incluso un lenguaje diferente, para usar otro mecanismo como programación reactiva (como RxJava), modelo de actores (implementado por Akka y usado por Gatling mediante Scala), corutinas de kotlin, etc.

En la mayoría de los casos, al correr una prueba de performance, no se aprecian mayores diferencias entre las diferentes alternativas, ya que en la mayoría de ellos la memoria y CPU son acaparados por otros aspectos de la herramienta (por ejemplo; variables, extracción de respuestas y generación de pedidos para correlación, serialización y deserialización de pedidos y respuestas, etc) y la parte lenta de la ejecución está generalmente en el procesamiento de red o servidor. Aquí vas a encontrar una comparación de rendimiento entre JMeter y Gatling.

Pero, también existe una opción que no requiere de una biblioteca aparte o de un cambio de lenguaje, ¡bienvenidos a Project Loom!.

Project Loom: Threads Virtuales

No vamos a entrar en mucho detalle sobre Project Loom, puedes aprender más sobre el mismo aquí o acá. Pero en esencia te permite usar threads livianos (también llamados threads virtuales) que son dinámicamente asignados a threads estándar (también llamados threads de plataforma) de Java y a los threads del SO (múltiples threads virtuales por cada thread de plataforma).

Esto no solo reduce el consumo de recursos permitiendo generar más carga desde el mismo hardware, sino que también mantiene todos los beneficios del modelo existente de threads de Java (facilidad de debugeo, conocimiento previo del modelo, soporte de herramientas, etc)

De hecho, es muy fácil cambiar en una aplicación existente Java de usar threads de plataforma a usar threads virtuales reemplazando invocaciones a new Thread por algo como Thread.ofVirtual(). No hay necesidad de cambiar operaciones de IO (entrada/salida), locks o esperas con sleep, el equipo de OpenJDK ya se encargó de adaptar todos estos métodos. Internamente un thread virtual asignado/montado a un thread de plataforma es suspendido/desmontado cuando una de estas operaciones es invocada, para luego ser reasignado cuando la operación finaliza. Además, excepciones, stacktraces, herramientas de debugeo y otras herramientas existentes funcionan sin mayores alteraciones, no necesitamos preocuparnos por callbacks, cadenas de flujo de invocaciones y observers, usar alguna biblioteca/API específica para IO o concurrencia, etc.

Hay artículos muy interesantes sobre el uso del Project Loom en diferentes escenarios como este experimento donde usan threads virtuales en Apache Tomcat.

Luego de nuestra primer liberación de JMeter DSL, hicimos algunas pruebas con el Project Loom y los thread groups de JMeter, pero en ese momento la tecnología estaba un poco inmadura y todavia habia algunas cosas por pulir: experimentamos algunos problemas aleatorios al ejecutar pruebas y algunos comportamientos inconsistentes, pero en general los resultados iniciales que obtuvimos fueron prometedores. Además, no era claro en aquel entonces cuándo la tecnología iba a estar disponible dentro de las distribuciones estándar de la JVM.

El equipo de JMeter también ha estado discutiendo algunas alternativas en el pasado. De hecho han explorado usar corutinas de Kotlin como se puede ver en esta cadena.

Esto ha cambiado significativamente desde entonces, ya que el equipo de OpenJDK ha anunciado la inclusión del primer preview de Project Loom en su próxima versión de la JDK, 19 (que se espera ser liberada en septiembre).

Con esto en mente, hemos decidido hacer pruebas nuevamente y publicar los resultados obtenidos.

Experimentando con JMeter y Loom

Para evaluar si Project Loom puede ser fácilmente usado en JMeter y ver las diferencias con el modelo de threads existente, decidimos implementar la solución más simple al alcance de nuestras manos: implementar un nuevo thread group con JMeter DSL que simplemente use threads virtuales en vez de los threads de plataforma, portando y adaptando parte de la lógica de los thread groups por defecto de JMeter.

Usar JMeter DSL nos permite enfocarnos en las partes de la lógica que realmente importan y cambian e iterar sobre la implementación, sin tener que preocuparnos por detalles que son requeridos para implementar plugins o modificaciones a JMeter, como detalles de interfaz gráfica, empaquetado, etc.

Antes de esto, debemos instalar la última versión “early access” de JDK 19. Esto lo podemos hacer de una manera muy sencilla con sdkman así:

sdk install java 19.ea.23-open

Nota: La última versión “early access” de la JDK cambia frecuentemente, así que seguramente tu tengas que cambiar la versión para que el comando te funcione.

Luego de esto creamos un proyecto Maven que implementa el nuevo thread group usando JMeter DSL como framework para extender y usar JMeter. Aquí puedes encontrar el código del proyecto.

Si revisas el código puedes ver que es bastante sencillo crear un nuevo thread group, extendiendo la clase BaseThreadGroup provista por el DSL que retorne en el método buildThreadGroup una subclase de la clase AbstractThreadGroup de JMeter. En este caso simplemente copiamos la lógica de la clase ThreadGroup de JMeter que queremos personalizar. En particular el método startNewThread, que es el encargado de crear los threads, donde cambiamos esta línea:

Thread newThread = new Thread(jmThread, jmThread.getThreadName());

Por esto:

Thread newThread = Thread.ofVirtual().name(jmThread.getThreadName()).unstarted(jmThread);

Adicionalmente también cambiamos el método addNewThread, reemplazando el uso de synchronized por ReentrantLock, ya que en la actual implementación de la JDK Project Loom bloquea los threads del SO cuando encuentra un bloque synchronized, en vez de suspender los threads virtuales.

No cambiamos cada ocurrencia de los bloques synchronized en JMeter (por ejemplo en ResultCollectors), para simplificar la implementación y porque dichos cambios no tendrían un impacto significativo ya que estos bloques no incluyen lógica pesada o lenta. Además, no detectamos “pinning” de threads cuando usamos la opción de JVM -Djdk.tracePinnedThreads (usado para detectar threads virtuales que se encuentran bloqueados en monitores).

También para simplificar y evitar mayores complejidades, el thread group implementado no tiene soporte para demoras iniciales o ramp up. Las pruebas usarán el sampler HTTP con la implementación Java para evitar tener que portar parte del manejo de threads de httpClient.

Para poder compilar y correr las pruebas, ya que los threads virtuales son una funcionalidad en modo “preview”, necesitamos especificar la opción de JVM — enable-preview.

Ya que obtenemos diferentes resultados con top (que en general reporta mayor uso de CPU y RAM) que cuando usamos VisualVM y Java Flight Recorder, optamos por monitorear los recursos con top, que reporta los datos del proceso desde la perspectiva del SO, evitando cualquier potencial funcionalidad “early access” que falte aun adaptar en las herramientas de la JVM.

El principal foco de las pruebas es evaluar si hay alguna diferencia en TPS, consumo de RAM, CPU, o resultados generales obtenidos entre threads de plataforma y threads virtuales. No vamos a experimentar con opciones de la JVM, “tunear” el código, o entrar en demasiados detalles con limitaciones concernientes al generador de carga o servicio bajo prueba, para mantener las pruebas simples y reducir el alcance del experimento.

Usaremos en cada prueba un thread group con 5 minutos de duración y cada configuración será ejecutada 3 veces para ignorar cualquier valor atípico.

Ahora solo tenemos que correr las pruebas con el thread group por defecto y el nuevo thread group con diferentes cargas y ver los resultados :).

Resultados

Vamos ahora a correr algunas pruebas experimentando con diferentes cargas y escenarios y ver cuales son las diferencias entre usar threads virtuales y de plataforma.

Pruebas locales

Primero vamos a intentar correr pruebas desde una máquina local (MacOS 11.5, Intel Core i7, 6 núcleos de 2.5 GHz y 16GB de RAM) contra un sitio remoto en http://opencart.abstracta.us, que es una versión de OpenCart hosteada por Abstracta.

Empezamos con este tipo de pruebas, desde local a un servicio remoto, ya que es una buena prueba en algunos casos para recolectar métricas de experiencia de usuario finales y es un escenario común cuando arrancamos a hacer pruebas de performance.

No vamos a correr más pruebas locales ya que no podemos seguir comparando con threads de plataforma. Además, el sistema bajo prueba está fallando mucho y ya identificamos una diferencia significativa entre los threads virtuales y de plataforma.

Aquí hay una gráfica para visualizar los datos rápidamente:

Prueba de servidor a servidor

Ya que correr pruebas desde una máquina local a un servidor remoto puede incluir fluctuaciones provocadas por la red o problemas de la máquina local (otras aplicaciones compitiendo por recursos, limitaciones del SO, etc), vamos ahora a probar con dos servidores en la misma red. Adicionalmente, ya que Opencart fue bastante lento y fácil de saturar, vamos a intentar esta vez con la página de bienvenida de nginx, que debería dar unos tiempos de respuesta realmente bajos.

Para estas ejecuciones vamos a usar 2 instancias de Amazon EC2 t2.medium (2 vCPUs, 4GB RAM) que se encontraran en la misma “availability zone” y corriendo Amazon Linux. Para administrar esta infraestructura usamos un proyecto pulumi que puedes encontrar en el repositorio de código aquí.

Una de las instancias va a correr el script de pruebas JMeter y el otro va a correr nginx usando un contenedor Docker (lo que facilita el setup).

No vamos a correr más pruebas ya que el servicio bajo prueba ya está sobrecargado y hemos identificado una diferencia importante entre threads virtuales y de plataforma.

Prueba ficticia

Como última prueba vamos a correr algunas pruebas usando el sampler dummy con 1 segundo de respuesta emulada para ver, solo por curiosidad, a cuanto llegamos sin tener una conexión en el medio y ver cómo evolucionan las métricas más allá de los 5000 virtual threads.

A medida que la cantidad de threads incrementa podemos ver un esperado incremento en la desviación de TPS del ideal, debido a una carga mayor impuesta sobre la máquina.

Conclusiones y próximos pasos

La primer conclusión que podemos obtener de este experimento es que es muy sencillo implementar nuevos elementos de JMeter y probar nuevas ideas usando JMeter DSL. Parece ser una buena solución para prototipar ideas y luego contribuirlas al código de JMeter o implementar un nuevo plugin de JMeter.

Con respecto a los resultados obtenidos de las pruebas, en general no hay demasiada diferencia en consumo de CPU o RAM para los principales escenarios usando threads virtuales y de plataforma. A pesar que los threads virtuales podrían ser más livianos que los de plataforma, esto no es nada comparado con los recursos requeridos por las variables de threads de JMeter, los árboles de test plan de los threads de JMeter (JMeter clona un árbol por thread), los Sample Results, etc. Hemos visto un TPS menor y consumo de CPU mayor con los threads virtuales cuando probamos servicios realmente rápidos, pero eso puede deberse a una diferencia “temporal” ya que los threads virtuales todavía tienen margen de mejora según las palabras del equipo de OpenJDK. Una diferencia apreciable aparece cuando usamos el sampler dummy, pero este escenario es bastante ficticio.

No hay tampoco demasiada diferencia en velocidad, ya que la mayoría del tiempo es acaparado por operaciones que son mucho más costosas que el cambio de contexto de los threads: como networking, serialización y deserialización, etc.

La principal diferencia se puede ver cuando intentamos generar más concurrencia que la impuesta por los límites del SO. Con threads virtuales el límite en la cantidad de procesos no interfiere en las pruebas.

Algo que debes tener en cuenta a la hroa de usar threads virtuales es que son óptimos para tareas que requieren alguna espera (IO, lock, sleep), así que cuanto más procesamiento tienes en el thread, menor sentido, en términos de performance, tiene transicionar a usar threads virtuales. Los puede usar para organizar mejor el trabajo paralelo o para evitar limitaciones a nivel del SO sobre la cantidad de threads, pero no deberías considerarlos como una manera de mejorar la performance de un proceso que sea computacionalmente intensivo.

Sería interesante en el futuro evaluar y experimentar con una implementación de httpClient que use threads virtuales y correr unas pruebas rehusando conexiones. Esto ayudaría a cubrir más casos de los que típicamente tenemos cuando hacemos pruebas de performance que habitualmente tienen esta necesidad (rehusar conexiones). Además, esto seguramente haga mejor uso de los threads virtuales, ya que habría menos procesamiento requerido por la lógica de conexión y desconexión (la creación de de objetos asociadas a ella) y más de esperas por IO.

En tanto a Project Loom y su futuro, pensamos que los threads virtuales nos traen una nueva solución a la JVM que evitan problemas asociados al uso de threads de plataforma y también los problemas asociados a las alternativas existentes hoy día. Estamos ansiosos por ver cómo esta nueva tecnología evoluciona, mejora y es incorporada en diferentes herramientas.

En particular, esperamos que JMeter lo integre pronto, después que haya sido liberada, ya que provee una opción muy útil cuando estamos luchando contra las limitantes del SO.

Te alentamos a que le prestes atención, lo pruebes y pruebes JMeter DSL si todavía no lo has hecho. Y que nos des una estrella aquí.

¿Qué piensas? ¿Fueron los resultados los que esperabas? ¿Ya conocías Project Loom? Nos gustaría conocer tus ideas y comentarios, realmente apreciamos el intercambio.

¡Síguenos en LinkedinTwitterFacebook, Instagram y Youtube para ser parte de nuestra comunidad y enterarte de nuestros contenidos!


Otros contenidos relacionados

Cómo ejecutar Pruebas de Performance de streaming de video con el Plugin HLS para JMeter

3 métricas clave de Pruebas de Performance que todo tester debe conocer

¿Cómo diseñar un Plan de Pruebas de Performance?

132 / 153