Seminario web destacado: Pruebas de API mejoradas con IA: un enfoque de prueba sin código | Vea ahora

Dominar las pruebas de rendimiento de aplicaciones Java con Parasoft

Foto de cabeza de Sergei Baranov
27 diciembre 2023
9 min leer

Una sola línea de código podría causar estragos en todo el rendimiento de su software si no se detecta y corrige a tiempo. Consulte esta publicación para obtener información sobre cómo monitorear subprocesos de Java y comprender las líneas específicas de código en su aplicación que podrían causar posibles errores en el rendimiento de su aplicación.

Los problemas relacionados con subprocesos pueden afectar negativamente el rendimiento de una aplicación API web de maneras que a menudo son difíciles de diagnosticar y de resolver. Mantener una imagen clara del comportamiento de un hilo es esencial para lograr un rendimiento óptimo.

En esta publicación, te mostraré cómo usar Prueba SOA de ParasoftMonitor de subprocesos de JVM de prueba de carga para ver la actividad de subprocesos de una JVM con gráficos de estadísticas vitales y volcados de subprocesos configurables que pueden señalar las líneas de código responsables de la pérdida de rendimiento causada por el uso ineficiente de subprocesos. Parasoft SOAtest's herramientas de prueba de rendimiento Le permite convertir cualquier prueba funcional en pruebas de carga y rendimiento.

Por qué son importantes las pruebas de rendimiento

Las aplicaciones de servidor, como los servidores web, están diseñadas para procesar múltiples solicitudes de clientes simultáneamente. La cantidad de solicitudes de clientes procesadas simultáneamente por un servidor generalmente se denomina "carga". Una carga creciente puede provocar tiempos de respuesta lentos, errores de aplicación y, finalmente, un bloqueo. Los problemas de simultaneidad y sincronización relacionados con subprocesos causados ​​por el procesamiento simultáneo de solicitudes pueden contribuir a todas estas categorías de comportamiento no deseado. Por esta razón, se deben probar exhaustivamente y eliminarlos antes de implementar la aplicación en producción.

Una de las mayores diferencias que separa pruebas de rendimiento A diferencia de otros tipos de pruebas de software, es su enfoque sistemático en cuestiones de concurrencia. El mecanismo de subprocesos múltiples que emplean las aplicaciones de servidor para procesar múltiples solicitudes de clientes simultáneamente puede hacer que el código de la aplicación ejecute errores e ineficiencias inducidas por la concurrencia que no ocurren en otros tipos de pruebas, típicamente de un solo subproceso, como las pruebas unitarias o de integración.

Desde la perspectiva de validar el código de la aplicación para problemas de sincronización y concurrencia relacionados con subprocesos, el objetivo de una prueba de rendimiento es doble.

  1. Para exponer estos problemas si existen.
  2. Para capturar detalles relevantes cuando ocurren estos problemas y proporcionar la máxima información de diagnóstico para ayudar a solucionarlos.

Lograr el primero de estos objetivos requiere poner la aplicación bajo prueba (AUT) bajo una carga comparable a la que experimentará en producción aplicando un flujo de solicitudes de clientes simuladas con simultaneidad de carga, intensidad de solicitud y duración de prueba configuradas correctamente. Para obtener más información, consulte el Guía de mejores prácticas de pruebas de rendimiento.

Lograr el segundo de estos objetivos es el tema de esta publicación de blog.

Mejores prácticas para probar el rendimiento de aplicaciones Java

Cuando se trata de detectar problemas relacionados con subprocesos y sus detalles, las preguntas principales son cómo detectarlos, qué hacer con los datos de diagnóstico y cuándo buscar dichos problemas. A continuación se encuentran las listas de respuestas clave a esas preguntas.

Cómo detectar problemas relacionados con subprocesos y utilizar los datos de diagnóstico

  • Supervise los subprocesos AUT en busca de interbloqueos, así como estados BLOQUEADOS o ESTACIONADOS que impidan que los subprocesos funcionen.
  • Cree controles de regresión de rendimiento basados ​​en monitores de subprocesos para detectar automáticamente dichos problemas.
  • Recopile detalles, como seguimientos de pila, de subprocesos AUT con problemas para diagnosticar el comportamiento no deseado de los subprocesos cuando ocurra.
  • Superponga gráficos de indicadores clave de rendimiento, como gráficos de tiempo de respuesta máximo y similares, con gráficos de monitorización de subprocesos para encontrar correlaciones entre los problemas de subprocesos y el rendimiento de la aplicación.

Pregunte a su proveedor de productos de prueba de rendimiento cómo su herramienta puede ayudarle a detectar y diagnosticar problemas de subprocesos AUT.

Cuándo buscar problemas relacionados con los hilos

El patrón aparentemente aleatorio de algunos problemas relacionados con los hilos plantea problemas adicionales. desafíos de las pruebas de rendimiento para detectar y diagnosticar fallas raras. Corregir un error raro, uno entre un millón, puede convertirse en una pesadilla para los equipos de control de calidad y de desarrollo en las últimas etapas del ciclo de vida de la aplicación.

Para aumentar las posibilidades de detectar dichos errores, la aplicación bajo prueba de carga debe monitorearse continuamente durante todas las etapas de la prueba de rendimiento. A continuación se muestra una lista de los principales tipos de pruebas de rendimiento y por qué debería utilizar monitores de subprocesos mientras los ejecuta.

  • Pruebas de humo o de referencia para detectar problemas relacionados con subprocesos de manera temprana.
  • Pruebas de carga validar que el AUT bajo la carga de producción esperada no sufre estos problemas.
  • Pruebas de estrés para comprobar el nivel de concurrencia. Puede ser mucho mayor en una prueba de estrés que en una prueba de carga normal, lo que aumenta la probabilidad de que se produzcan problemas relacionados con los subprocesos.
  • Pruebas de resistencia puede ejecutarse durante mucho más tiempo que las pruebas de carga normales, lo que aumenta la probabilidad de que se produzcan problemas raros relacionados con subprocesos.

Consulte las Guía de mejores prácticas de pruebas de rendimiento para obtener más detalles sobre los tipos de pruebas de rendimiento y su uso.

Como ejemplo de cómo se pueden aplicar estas prácticas, ahora seguiremos a un hipotético equipo de desarrollo de Java que se topa con algunos problemas de subprocesos comunes mientras crea una aplicación API web y diagnosticaremos algunos problemas de rendimiento relacionados con subprocesos comunes. Después de eso, veremos ejemplos más complejos de aplicaciones reales. Tenga en cuenta que parte del código subóptimo en los ejemplos siguientes se ha agregado intencionalmente para demostración.

El estudio de caso de la aplicación bancaria

Nuestro hipotético equipo de desarrollo de Java se embarcó en un nuevo proyecto: una aplicación bancaria REST API. El equipo configuró una infraestructura de integración continua (CI) para respaldar el nuevo proyecto, que incluye un trabajo de CI periódico con Parasoft. Prueba SOAes Prueba de carga módulo para probar continuamente el rendimiento de la nueva aplicación.

Obtenga instrucciones paso a paso sobre cómo configurar pruebas de rendimiento automatizadas.

Aplicación bancaria Versión 1: Condiciones de carrera en la implementación inicial

El código de la aplicación del Banco está empezando a crecer y las pruebas se están ejecutando. Sin embargo, el equipo notó que después de implementar un nuevo transfer operación, la aplicación del Banco comenzó a tener fallas esporádicas bajo mayor carga. La falla proviene del método de validación de la cuenta que ocasionalmente encuentra un saldo negativo en las cuentas protegidas contra sobregiros. El error en la validación de la cuenta provoca una excepción y una respuesta HTTP 500 de la API. Los desarrolladores sospechan que esto puede deberse a una condición de carrera en el IAccount.withdraw método cuando es llamado por diferentes subprocesos que procesan simultáneamente transfer operación en la misma cuenta:

13: public boolean transfer(IAccount from, IAccount to, int amount) {
14:    if (from.withdraw(amount)) {
15:       to.deposit(amount); 
16:       return true; 
17:    } 
18:    return false; 
19: }

Aplicación bancaria versión 2: agregar sincronización

Los desarrolladores deciden sincronizar el acceso a las cuentas dentro del transfer operación para prevenir la sospecha de condición de carrera:

14: public boolean transfer(IAccount from, IAccount to, int amount) {
15:    synchronized (to) {
16:       synchronized (from) {
17:          if (from.withdraw(amount)) {
18:             to.deposit(amount);
19:             return true; 
20:          } 
21:       } 
22:    } 
23:    return false; 
24: }

El equipo también agrega JVM Threads Monitor al proyecto de prueba de carga que se ejecuta en la aplicación API REST. El monitor proporcionará gráficos de subprocesos estancados, bloqueados, estacionados y totales y registrará volcados de subprocesos en estos estados.

El cambio de código se envía al repositorio y el proceso de prueba de rendimiento de CI lo recoge. Al día siguiente, los desarrolladores descubren que la prueba de rendimiento falló durante la noche. La aplicación del Banco dejó de responder poco después del inicio de la prueba de rendimiento de la operación de transferencia. La inspección de los gráficos del Monitor de subprocesos JVM en el informe de prueba de carga muestra rápidamente que hay subprocesos estancados en la aplicación del Banco. Véase la figura 1.a. Los detalles del punto muerto fueron guardados por JVM Threads Monitor como parte del informe y muestran las líneas exactas de código responsables del punto muerto. Ver Listado 1.b.

Gráfico que muestra el número de subprocesos bloqueados en la aplicación bajo prueba (AUT).
Fig 1.a: Número de subprocesos bloqueados en la aplicación bajo prueba (AUT).
DEADLOCKED thread: http-nio-8080-exec-20     
com.parasoft.demo.bank.v2.ATM_2.transfer:15     
com.parasoft.demo.bank.ATM.transfer:21    
...    
Blocked by:        
DEADLOCKED thread: http-nio-8080-exec-7
com.parasoft.demo.bank.v2.ATM_2.transfer:16
com.parasoft.demo.bank.ATM.transfer:21            
com.parasoft.demo.bank.v2.RestController_2.transfer:29
sun.reflect.GeneratedMethodAccessor58.invoke:-1
sun.reflect.DelegatingMethodAccessorImpl.invoke:-1   
java.lang.reflect.Method.invoke:-1            
org.springframework.web.method.support.InvocableHandlerMethod.doInvoke:209

Listado 1.b: Detalles del interbloqueo guardados por el monitor JVM Threads.

Aplicación bancaria versión 3: resolución de bloqueos

Los desarrolladores de la aplicación bancaria deciden resolver el punto muerto sincronizando en un único objeto global y modificando el código del método de transferencia de la siguiente manera:

14: public boolean transfer(IAccount from, IAccount to, int amount) {
15:     synchronized (Account.class) {
16:       if (from.withdraw(amount)) {
17:          to.deposit(amount); 
18:             return true; 
19:       } 
20:    }
21:    return false; 
22: }

El cambio resuelve el problema de interbloqueo de la Versión 2 y la condición de carrera de la Versión 1, pero el promedio transfer El tiempo de respuesta de la operación aumenta más de cinco veces, de 30 a más de 150 milisegundos. Véase la figura 2.a. El gráfico BlockedRatio de JVM Threads Monitor muestra que del 60 al 75 por ciento de los subprocesos de la aplicación están en estado BLOQUEADO durante la ejecución de la prueba de carga. Véase la figura 2.b. Los detalles guardados por el monitor indican que los subprocesos de la aplicación están bloqueados al intentar ingresar a la sección sincronizada globalmente en la línea 15. Consulte el Listado 2.c.

Dos gráficos uno al lado del otro. La figura 2a muestra los tiempos de respuesta de la versión 3 de la aplicación bancaria con una línea azul que salta al 160 por ciento y se mantiene estable en comparación con los tiempos de respuesta de la versión 1 con una línea verde que alcanza el 40 por ciento con ligeras fluctuaciones a la baja. A la derecha está la Figura 2.b que muestra el porcentaje de subprocesos bloqueados en el AUT que alcanzan 70 y luego caen a 50 a los 100 segundos de tiempo de finalización.

BLOCKED thread: http-nio-8080-exec-4    
com.parasoft.demo.bank.v3.ATM_3.transfer:15    
com.parasoft.demo.bank.ATM.transfer:21    
com.parasoft.demo.bank.v3.RestController_3.transfer:29    
...    
Blocked by:        
SLEEPING thread: http-nio-8080-exec-8            
java.lang.Thread.sleep:-2            
com.parasoft.demo.bank.Account.doWithdraw:64           
com.parasoft.demo.bank.Account.withdraw:31

Listado 2.c: Detalles de subprocesos bloqueados guardados por JVM Threads Monitor.

Aplicación bancaria versión 4: mejora del rendimiento de la sincronización

El equipo de desarrollo busca una solución que resuelva la condición de carrera sin introducir interbloqueos y comprometer la capacidad de respuesta de la aplicación y, después de algunas investigaciones, encuentra una solución prometedora con el uso de java.util.concurrent.locks.ReentrantLock clase:

19: private boolean doTransfer(Account from, Account to, int amount) {           
20:    try
21:        acquireLocks(from.getReentrantLock(), to.getReentrantLock()); 
22:        if (from.withdraw(amount)) {
23:            to.deposit(amount); 
24:            return true; 
25:        } 
26:        return false; 
27:    } finally {
28:         releaseLocks(from.getReentrantLock(), to.getReentrantLock()); 
29:    } 
30: } 

Los gráficos de la Fig. 3a muestran los tiempos de respuesta de la aplicación bancaria. transfer operación de la versión 4 (bloqueo optimizado) en el gráfico rojo, versión 3 (sincronización global de objetos) en el gráfico azul y versión 1 (operación de transferencia no sincronizada) en el gráfico verde. Los gráficos indican que la transfer el rendimiento de la operación ha mejorado dramáticamente como resultado de la optimización del bloqueo. La ligera diferencia entre el sincronizado (gráfico rojo) y el no sincronizado (gráfico verde) transfer La operación es un precio aceptable para prevenir las condiciones de carrera.

Gráfico que muestra el tiempo de respuesta de la operación de transferencia de la aplicación del Banco Versión 4 (rojo), Versión 3 (azul) y Versión 1 (verde).
Fig 3.a: Tiempo de respuesta de la operación de transferencia de la aplicación bancaria Versión 4 (rojo), Versión 3 (azul) y Versión 1 (verde).

Desafíos de rendimiento del mundo real

Ejemplo 1: aumento del tiempo de respuesta de la aplicación

Los ejemplos de aplicaciones bancarias anteriores demuestran cómo resolver casos aislados típicos de degradación del rendimiento causada por problemas de subprocesos. Los casos del mundo real pueden ser más complicados. Los gráficos de la Fig. 4 muestran un ejemplo de una aplicación API REST de producción cuyo tiempo de respuesta siguió creciendo a medida que avanzaba la prueba de rendimiento. El tiempo de respuesta de la aplicación creció a un ritmo menor durante la primera mitad de la prueba y a un ritmo mayor en la segunda mitad. Véase la figura 4.a.

En la primera mitad de la prueba, el crecimiento del tiempo de respuesta se correlacionó con el tiempo total que los subprocesos de la aplicación pasaron en el estado BLOQUEADO. Véase la figura 4.b.

En la segunda mitad de la prueba, el crecimiento del tiempo de respuesta se correlacionó con la cantidad de subprocesos de la aplicación en estado ESTACIONADO. Véase la figura 4.c.

Los seguimientos de pila capturados por Load Test JVM Threads Monitor proporcionaron los detalles: uno apuntaba a un synchronized block, que fue responsable del tiempo excesivo pasado en estado BLOQUEADO. El otro señaló las líneas de código que usaban java.util.concurrent.locks clases para la sincronización, que era responsable de mantener los subprocesos en el estado PARKED. Después de optimizar estas áreas de código, se resolvieron ambos problemas de rendimiento.

Se muestran tres gráficos. Figura 4.a. muestra que el tiempo de respuesta de la aplicación aumenta a 10,000 1500 a partir de los 4 segundos de finalización de la prueba. Figura 4.b. muestra el tiempo que los subprocesos de la aplicación pasaron en un estado bloqueado. La figura XNUMX.c muestra la cantidad de subprocesos de aplicación en estado estacionado.

Ejemplo 2: picos ocasionales en el tiempo de respuesta de la aplicación

El Monitor de subprocesos JVM de prueba de carga puede resultar muy útil para capturar detalles de problemas raros relacionados con subprocesos, especialmente si sus pruebas de rendimiento están automatizadas y se ejecutan con regularidad*. Los gráficos de la Fig. 5 muestran una aplicación API REST de producción que tuvo picos intermitentes en los tiempos de respuesta promedio y máximo. Véase la figura 5.a.

Estos picos en el tiempo de respuesta de la aplicación a menudo pueden deberse a una configuración subóptima del recolector de basura JVM, pero en este caso un pico correlacionado en el monitor BlockedTime, Fig. 5.b, apunta a la sincronización de subprocesos como el origen del problema. El monitor BlockedThreads ayuda aún más aquí al capturar los rastros de la pila de los subprocesos bloqueados y bloqueados. Es importante comprender la diferencia entre los monitores BlockedTime y BlockedThreads.

El monitor BlockedTime muestra el tiempo acumulado que los subprocesos JVM pasaron en estado BLOQUEADO entre invocaciones del monitor, mientras que el monitor BlockedThreads toma instantáneas periódicas de los subprocesos JVM y busca subprocesos bloqueados en esas instantáneas. Por esta razón, el monitor BlockedTime es más confiable para detectar bloqueos de subprocesos, pero solo le alerta que existen problemas de bloqueo de subprocesos.

El monitor BlockedThreads, debido a que toma instantáneas de subprocesos regulares, puede perder algunos eventos de bloqueo de subprocesos, pero en el lado positivo, cuando captura dichos eventos, proporciona información detallada de las causas del bloqueo. Por esta razón, si un monitor BlockedThreads capturará o no los detalles relacionados con el código de un estado bloqueado es una cuestión de estadísticas, pero si sus pruebas de rendimiento se ejecutan de forma regular, pronto obtendrá un pico en el gráfico de BlockedThreads (consulte Fig. 5.c), lo que significa que se han capturado los detalles del hilo bloqueado y bloqueado. Estos detalles le indicarán las líneas de código responsables de los raros picos en el tiempo de respuesta de la aplicación.

Tres gráficos mostrados uno al lado del otro. Figura 5.a. muestra un aumento en los tiempos de respuesta máximos (amarillo) y promedio (gris) de la aplicación. Figura 5.b. muestra un pico correlacionado en el monitor BlockedTime. Figura 5.c. muestra un pico correlacionado en el monitor de subprocesos bloqueados.

Creación de controles de regresión de rendimiento

El monitor Load Test JVM Threads, además de ser una herramienta de diagnóstico eficaz, también se puede utilizar para crear controles de regresión de rendimiento para problemas relacionados con subprocesos. Una vez que haya descubierto y solucionado dicho problema de rendimiento, cree una prueba de regresión de rendimiento para ello. La prueba consistirá en una ejecución de prueba de rendimiento nueva o existente y un nuevo control de regresión. En el caso de Parasoft Load Test, sería una métrica de monitor de QoS para un canal de monitor de subprocesos JVM relevante. Por ejemplo, para el problema descrito en el ejemplo 1, Fig. 4, cree una métrica de Monitor de QoS de prueba de carga que verifique el tiempo que los subprocesos de la aplicación pasaron en estado BLOQUEADO y otra métrica que verifique la cantidad de subprocesos en estado ESTACIONADO. Siempre es una buena idea crear subprocesos con nombre en su aplicación Java, esto le permitirá aplicar controles de regresión de rendimiento a un conjunto de subprocesos filtrados por nombre.

El papel de la automatización en las pruebas de rendimiento de aplicaciones Java

Una vez que se han creado los controles de regresión del monitor JVM Threads, se pueden utilizar de manera efectiva en pruebas de rendimiento automatizadas. Los controles de regresión de rendimiento son una herramienta indispensable para la automatización de pruebas, que es, a su vez, un elemento importante de la evaluación continua. pruebas de rendimiento dentro de una canalización de DevOps. Los controles de regresión del desempeño deben crearse no sólo en respuesta a regresiones de desempeño pasadas sino también como una defensa contra problemas potenciales.

La siguiente tabla proporciona un resumen de qué canales de Threads Monitor usar y cuándo:

Canal de monitorización de subprocesosCuándo usar
Hilos interbloqueadosSiempre. Los interbloqueos son posiblemente los problemas más graves relacionados con los subprocesos que podrían romper la funcionalidad de la aplicación por completo.
Supervisar subprocesos bloqueados
Hilos bloqueadosSiempre. El tiempo excesivo en el estado BLOQUEADO o el número de subprocesos BLOQUEADOS normalmente dará como resultado una pérdida de rendimiento. Supervise al menos uno de estos parámetros. También se utiliza para controles de regresión de rendimiento.
TiempoBloqueado
Relación bloqueada
Recuento bloqueado
subprocesos estacionadosSiempre. Un número excesivo de subprocesos en el estado PARKED puede indicar un uso inadecuado de las clases java.util.concurrent.locks y otros problemas de subprocesos. También se utiliza para controles de regresión de rendimiento.
Subprocesos totalesA menudo. Úselo para comparar el número de subprocesos en BLOQUEADO, ESTACIONADO u otros estados con el número total de subprocesos. También se utiliza para controles de regresión de rendimiento.
DormirHilosDe vez en cuando. Úselo para controles de regresión de rendimiento relacionados con estos estados y para pruebas exploratorias.
Esperando subprocesos
Tiempo de espera
EsperandoRatio
EsperandoContar
hilos nuevosCasi nunca. Úselo para los controles de regresión de rendimiento relacionados con estos estados de subprocesos.
Temas desconocidos

Conclusión

El Monitor de subprocesos de JVM de Parasoft es una herramienta de diagnóstico eficaz para detectar problemas de rendimiento de JVM relacionados con subprocesos, así como para crear controles de regresión de rendimiento avanzados. Cuando se combina con Prueba SOALoad Test Continuum, JVM Threads Monitor ayuda a eliminar el paso de reproducir problemas de rendimiento mediante el registro de detalles de subprocesos relevantes que apuntan a las líneas de código responsables del bajo rendimiento y lo ayuda a mejorar tanto el rendimiento de la aplicación como la productividad del desarrollador y el control de calidad.

Guía de mejores prácticas de pruebas de rendimiento