Tome el control de los problemas de subprocesos que afectan el rendimiento de su aplicación Java Web API

Por Serguéi Baranov

11 de abril de 2019

8  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 de API web de formas que a menudo son difíciles de diagnosticar y difíciles de resolver. Mantener una imagen clara del comportamiento de un subproceso 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 Prueba de carga El módulo le permite convertir cualquier prueba funcional en pruebas de carga y rendimiento.

Seguiremos a un hipotético equipo de desarrollo de Java que se encuentra con algunos problemas comunes de subprocesos al crear una aplicación de API web y diagnosticaremos algunos problemas comunes de rendimiento relacionados con subprocesos. Después de eso, veremos ejemplos más complejos de las aplicaciones reales. (Tenga en cuenta que algunos códigos subóptimos en los ejemplos a continuación se han agregado intencionalmente con fines de demostración).

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. (Para obtener más detalles sobre cómo configurar las pruebas de rendimiento automatizadas, consulte mi publicación anterior, Pruebas de carga y rendimiento en un canal de entrega de DevOps.)

Versión 1 de la aplicación bancaria: 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: }

Versión 2 de la aplicación bancaria: adición de 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 el Monitor de subprocesos de JVM al proyecto de prueba de carga que se ejecuta en la aplicación API REST. El monitor proporcionará gráficos de subprocesos bloqueados, bloqueados, estacionados y totales y registrará volcados de subprocesos en estos estados.

El cambio de código se envía al repositorio y es recogido por el proceso de prueba de rendimiento de CI. 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 de JVM en el informe de prueba de carga muestra rápidamente que hay subprocesos bloqueados en la aplicación del banco (consulte la Fig. 1.a). Los detalles del interbloqueo fueron guardados por JVM Threads Monitor como parte del informe y muestran las líneas exactas de código responsables del interbloqueo (consulte el Listado 1.b).

Fig 1.a - Número de subprocesos interbloqueados 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 de interbloqueo guardados por el monitor de subprocesos de JVM

Versión 3 de la aplicación bancaria: resolución de interbloqueos

Los desarrolladores de aplicaciones bancarias deciden resolver el interbloqueo sincronizando en un único objeto global y modifican 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) { 17:         if (from.withdraw(amount)) { 18:             to.deposit(amount); 19:             return true; 20:         } 21:     } 22:     return false; 23: }

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 (ver Fig. 2.a). El gráfico BlockedRatio del Monitor de subprocesos de JVM 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 (consulte la Fig. 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 (ver Listado 2.c).

   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 el Monitor de subprocesos de JVM

Versión 4 de la aplicación bancaria: 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 funcionamiento de la versión 4 (bloqueo optimizado) en gráfico rojo, versión 3 (sincronización global de objetos) en gráfico azul y versión 1 (operación de transferencia no sincronizada) en gráfico verde. Los gráficos indican que el 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.

 

Figura 3.a - transfer tiempo de respuesta de operación de la aplicación del Banco Versión 4 (rojo), Versión 3 (azul) y Versión 1 (verde).

Los ejemplos del mundo real

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

Los ejemplos de aplicaciones bancarias anteriores sirven para demostrar cómo resolver casos aislados típicos de degradación del rendimiento causados ​​por problemas de subprocesos. Los casos del mundo real pueden ser más complicados: los gráficos de la figura 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 una tasa menor durante la primera mitad de la prueba y a una tasa mayor en la segunda mitad (ver Fig 4.a). En la primera mitad de la prueba, el crecimiento del tiempo de respuesta se correlacionó con el tiempo total de subprocesos de aplicación que pasaron en el estado BLOQUEADO (ver Fig 4.b). En la segunda mitad de la prueba, el crecimiento del tiempo de respuesta se correlacionó con el número de subprocesos de aplicación en el estado PARKED. 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.

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

El Monitor de subprocesos de JVM de prueba de carga puede ser 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 de API REST de producción que tenía picos intermitentes en los tiempos de respuesta promedio y máximo (ver Fig. 5.a).

Tales picos en el tiempo de respuesta de la aplicación a menudo pueden ser causados ​​por una configuración subóptima del recolector de basura JVM, pero en este caso un pico correlativo en el monitor BlockedTime (ver Fig. 5.b) apunta a la sincronización de subprocesos como la fuente del problema. El monitor BlockedThreads ayuda aún más aquí al capturar los rastros de 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 las invocaciones del monitor, mientras que el monitor BlockedThreads toma instantáneas periódicas de subprocesos JVM y busca subprocesos bloqueados en esas instantáneas. Por esta razón, el monitor BlockedTime es más confiable para detectar el bloqueo de subprocesos, pero solo le advierte 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 de 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 la Fig. . 5.c), lo que significa que se han capturado los detalles de los hilos bloqueados y bloqueados. 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.

 

Creación de controles de regresión de rendimiento

El monitor de subprocesos JVM de prueba de carga, 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 existente o nueva y un nuevo control de regresión. En el caso de la prueba de carga de Parasoft, sería una métrica de supervisión de QoS para un canal de supervisión de subprocesos de JVM relevante. Por ejemplo, para el problema descrito en el ejemplo 1 (Fig. 4), cree una métrica de Monitoreo 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.

Uso de Java Threads Monitor en pruebas de rendimiento automatizadas

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

Canal de monitorización de subprocesos Cuándo usarlos
Hilos interbloqueados
Supervisar subprocesos bloqueados
Siempre. 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.
Hilos bloqueados
TiempoBloqueado
Relación bloqueada
BlockedCount
Siempre. 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.
subprocesos estacionados Siempre. 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 totales A 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.
DormirHilos
Esperando subprocesos
Tiempo de espera
EsperandoRatio
WaitingCount
De vez en cuando. Úselo para controles de regresión de rendimiento relacionados con estos estados y para pruebas exploratorias.
hilos nuevos
Temas desconocidos
Casi nunca. Úselo para los controles de regresión de rendimiento relacionados con estos estados de subprocesos.

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.

 

Nueva llamada a la acción

Por Serguéi Baranov

Sergei es un ingeniero de software principal en Parasoft, y se centra en las pruebas de carga y rendimiento dentro de Parasoft SOAtest.

Reciba las últimas noticias y recursos sobre pruebas de software en su bandeja de entrada.