Découvrez comment intégrer facilement l'analyse statique, les tests unitaires et d'autres méthodes de test de logiciels C et C++ dans votre pipeline CI/CD. Inscrivez-vous pour la démo >>

Prenez le contrôle des problèmes de threading ayant un impact sur les performances de votre application API Web Java

Par Sergueï Baranov

11 April 2019

8  min lire

Dans cet article, découvrez comment surveiller les threads Java pour comprendre les lignes de code spécifiques de votre application qui causent des problèmes de performances.

Les problèmes liés aux threads peuvent affecter les performances d'une application API Web de manière souvent difficile à diagnostiquer et à résoudre. Garder une image claire du comportement d'un thread est essentiel pour obtenir des performances optimales. Dans cet article, je vais vous montrer comment utiliser Parasoft SOAtest's Load Test JVM Threads Monitor pour afficher l'activité de threading d'une JVM avec des graphiques de statistiques vitales et des vidages de thread configurables qui peuvent pointer vers les lignes de code responsables de la perte de performances causée par une utilisation inefficace des threads. Parasoft SOAtest test de charge module vous permet de convertir tous les tests fonctionnels en tests de charge et de performance.

Nous suivrons une équipe de développement Java hypothétique qui rencontre quelques problèmes de threads courants lors de la création d'une application API Web, et diagnostiquerons certains problèmes de performances courants liés aux threads. Après cela, nous examinerons des exemples plus complexes des applications réelles. (Notez que du code sous-optimal dans les exemples ci-dessous a été ajouté intentionnellement à des fins de démonstration.)

L'application Banque

Notre équipe de développement hypothétique Java s'est lancée dans un nouveau projet - une application bancaire API REST. L'équipe a mis en place une infrastructure d'intégration continue (CI) pour soutenir le nouveau projet, qui comprend un travail périodique de CI avec Parasoft SOAtest's test de charge module pour tester en permanence les performances de la nouvelle application. (Pour plus de détails sur la configuration des tests de performances automatisés, consultez mon article précédent, Test de charge et de performance dans un pipeline de livraison DevOps.)

Application bancaire version 1: Conditions de course dans l'implémentation initiale

Le code de l'application Bank commence à se développer et les tests sont en cours. Cependant, l'équipe a remarqué qu'après la mise en œuvre d'un nouveau transfer opération, l'application Bank a commencé à avoir des échecs sporadiques sous une charge plus élevée. L'échec provient de la méthode de validation de compte qui trouve parfois un solde négatif dans les comptes protégés contre les découverts. L'échec de la validation du compte provoque une exception et une réponse HTTP 500 de l'API. Les développeurs soupçonnent que cela peut être causé par une condition de concurrence dans le IAccount.withdraw méthode lorsqu'elle est appelée par différents threads traitant simultanément transfer opération sur le même compte:

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: }

Application bancaire version 2: Ajout de la synchronisation

Les développeurs décident de synchroniser l'accès aux comptes dans le transfer opération pour éviter la condition de course suspectée:

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: }

L'équipe ajoute également le moniteur de threads JVM au projet de test de charge qui s'exécute sur l'application API REST. Le moniteur fournira des graphiques des threads bloqués, bloqués, parqués et totaux et enregistrera les vidages de threads dans ces états.

Le changement de code est poussé vers le référentiel et récupéré par le processus de test des performances CI. Le lendemain, les développeurs constatent que le test de performance a échoué du jour au lendemain. L'application Bank a cessé de répondre peu de temps après le démarrage du test de performance de l'opération de transfert. L'inspection des graphiques du moniteur de threads JVM dans le rapport de test de charge montre rapidement qu'il y a des threads bloqués dans l'application Bank (voir Fig 1.a). Les détails de l'interblocage ont été enregistrés par le JVM Threads Monitor dans le cadre du rapport et montrent les lignes exactes de code responsables de l'interblocage (voir Listing 1.b).

Fig 1.a - Nombre de threads bloqués dans l'application sous test (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

Listing 1.b - Détails du blocage enregistrés par le moniteur de threads JVM

Application bancaire version 3: résolution des blocages

Les développeurs d'applications de la Banque décident de résoudre le blocage en se synchronisant sur un seul objet global et modifient le code de la méthode de transfert comme suit:

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: }

Le changement résout le problème de blocage de la version 2 et la condition de concurrence de la version 1, mais la moyenne transfer le temps de réponse de fonctionnement augmente de plus de cinq fois de 30 à plus de 150 millisecondes (voir Fig. 2.a). Le graphique JVM Threads Monitor BlockedRatio montre que de 60 à 75 pour cent des threads d'application sont à l'état BLOQUÉ pendant l'exécution du test de charge (voir Fig. 2.b). Les détails enregistrés par le moniteur indiquent que les threads d'application sont bloqués lors de la tentative d'entrer dans la section globalement synchronisée sur la ligne 15 (voir le Listing 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

Listing 2.c - Détails des threads bloqués enregistrés par le moniteur de threads JVM

Application bancaire version 4: Amélioration des performances de synchronisation

L'équipe de développement recherche un correctif qui résoudrait la condition de concurrence sans introduire de blocages et compromettre la réactivité de l'application et après quelques recherches trouve une solution prometteuse avec l'utilisation de java.util.concurrent.locks.ReentrantLock classe:

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: 

Les graphiques de la figure 3a montrent les temps de réponse de l'application bancaire transfer fonctionnement de la version 4 (verrouillage optimisé) dans le graphe rouge, la version 3 (synchronisation globale des objets) dans le graphe bleu et la version 1 (opération de transfert non synchronisé) dans le graphe vert. Les graphiques indiquent que le transfer les performances de fonctionnement se sont considérablement améliorées grâce à l'optimisation du verrouillage. La légère différence entre le synchronisé (graphique rouge) et non synchronisé (graphique vert) transfer l'opération est un prix acceptable pour éviter les conditions de course.

 

Fig 3.a - transfer temps de réponse de fonctionnement de l'application Bank Version 4 (rouge), Version 3 (bleu) et Version 1 (vert).

Immobilier Exemples Monde

Exemple 1: Augmentation du temps de réponse de l'application

Les exemples d'application bancaire ci-dessus servent à montrer comment résoudre les cas isolés typiques de dégradation des performances causés par des problèmes de thread. Les cas du monde réel peuvent être plus compliqués - les graphiques de la figure 4 montrent un exemple d'application d'API REST de production dont le temps de réponse n'a cessé d'augmenter au fur et à mesure que le test de performance progressait. Le temps de réponse de l'application a augmenté à une vitesse plus faible pour la première moitié du test et à une vitesse plus élevée dans la seconde moitié (voir figure 4.a). Dans la première moitié du test, la croissance du temps de réponse était en corrélation avec le temps total des threads d'application passé dans l'état BLOQUÉ (voir la figure 4.b). Dans la seconde moitié du test, la croissance du temps de réponse était en corrélation avec le nombre de threads d'application à l'état PARKED. Les traces de pile capturées par le moniteur de threads JVM de test de charge fournissaient les détails: l'un pointait sur un synchronized bloc, qui était responsable du temps excessif passé dans l'état BLOQUÉ. L'autre pointait les lignes de code qui utilisaient java.util.concurrent.locks classes pour la synchronisation, qui était responsable de garder les threads dans l'état PARKED. Une fois ces zones de code optimisées, les deux problèmes de performances ont été résolus.

Exemple 2: pics occasionnels du temps de réponse de l'application

Le moniteur de threads JVM de test de charge peut être très utile pour capturer les détails de problèmes rares liés aux threads, en particulier si vos tests de performances sont automatisés et exécutés régulièrement *. Les graphiques de la figure 5 montrent une application API REST de production qui présentait des pics intermittents dans les temps de réponse moyen et maximum (voir figure 5.a).

Ces pics dans le temps de réponse des applications peuvent souvent être causés par une configuration sous-optimale du garbage collector JVM, mais dans ce cas, un pic de corrélation dans le moniteur BlockedTime (voir Fig.5.b) indique que la synchronisation des threads est la source du problème. Le moniteur BlockedThreads aide encore plus ici en capturant les traces de pile des threads bloqués et bloquants. Il est important de comprendre la différence entre les moniteurs BlockedTime et BlockedThreads.

Le moniteur BlockedTime affiche le temps accumulé que les threads JVM ont passé à l'état BLOQUÉ entre les appels du moniteur, tandis que le moniteur BlockedThreads prend des instantanés périodiques des threads JVM et recherche les threads bloqués dans ces instantanés. Pour cette raison, le moniteur BlockedTime est plus fiable pour détecter le blocage des threads, mais il vous avertit simplement que des problèmes de blocage des threads existent. Le moniteur BlockedThreads, car il prend des instantanés de threads réguliers, peut manquer certains événements de blocage de thread, mais du côté positif, lorsqu'il capture de tels événements, il fournit des informations détaillées sur les causes du blocage. Pour cette raison, le fait qu'un moniteur BlockedThreads capture ou non les détails liés au code d'un état bloqué est une question de statistiques, mais si vos tests de performances s'exécutent régulièrement, vous obtiendrez bientôt un pic dans le graphique BlockedThreads (voir Fig. . 5.c), ce qui signifie que les détails des threads bloqués et bloquants ont été capturés. Ces détails vous indiqueront les lignes de code responsables des rares pics de temps de réponse de l'application.

 

Création de contrôles de régression des performances

Le moniteur de threads JVM de test de charge en plus d'être un outil de diagnostic efficace, peut également être utilisé pour créer des contrôles de régression des performances pour les problèmes liés aux threads. Après avoir découvert et résolu ce problème de performances, créez un test de régression des performances pour celui-ci. Le test comprendra un test de performance existant ou nouveau et un nouveau contrôle de régression. En cas de test de charge Parasoft, il s'agirait d'une métrique de surveillance QoS pour un canal de surveillance de threads JVM pertinent. Par exemple, pour le problème décrit dans l'exemple 1 (figure 4), créez une métrique Load Test QoS Monitor qui vérifie le temps passé par les threads d'application dans l'état BLOQUÉ et une autre métrique qui vérifie le nombre de threads dans l'état PARKED. C'est toujours une bonne idée de créer des threads nommés dans votre application Java, cela vous permettra d'appliquer des contrôles de régression des performances à un ensemble de threads filtrés par nom.

Utilisation du moniteur Java Threads dans les tests de performances automatisés

Le tableau suivant fournit un résumé des canaux de Threads Monitor à utiliser et à quel moment:

Canal de surveillance des threads Quand utiliser
Threads bloqués
MoniteurDeadlockedThreads
Toujours. Les blocages sont sans doute le plus grave des problèmes liés aux threads qui pourraient interrompre complètement les fonctionnalités de l'application.
Fils bloqués
Temps Bloqué
Rapport Bloqué
Nombre Bloqué
Toujours. Un temps excessif passé à l'état BLOQUÉ ou au nombre de threads BLOQUÉS entraînera généralement une perte de performances. Surveillez au moins un de ces paramètres. Également utilisé pour les contrôles de régression des performances.
Fils garés Toujours. Un nombre excessif de threads à l'état PARKED peut indiquer une mauvaise utilisation des classes java.util.concurrent.locks et d'autres problèmes de thread. Également utilisé pour les contrôles de régression des performances.
Nombre total de threads Souvent. Utilisez pour comparer le nombre de threads dans les états BLOQUÉ, PARKÉ ou d'autres états au nombre total de threads. Également utilisé pour les contrôles de régression des performances.
Dormir
En attente
Temps d'attente
AttenteRatio
Compte d'attente
Parfois. À utiliser pour les contrôles de régression des performances liés à ces états et pour les tests exploratoires.
NouveauThreads
Fils inconnus
Rarement. À utiliser pour les contrôles de régression des performances liés à ces états de thread.

Conclusion

Le moniteur de threads JVM de Parasoft est un outil de diagnostic efficace pour détecter les problèmes de performances JVM liés aux threads ainsi que pour créer des contrôles de régression de performances avancés. Lorsqu'il est combiné avec SOAtest's Load Test Continuum, le moniteur de threads JVM aide à éliminer l'étape de reproduction des problèmes de performances en enregistrant les détails des threads pertinents qui pointent vers les lignes de code responsables des performances médiocres, et vous aide à améliorer les performances des applications ainsi que la productivité des développeurs et du contrôle qualité.

 

Nouvel appel à l'action

Par Sergueï Baranov

Sergei est un ingénieur logiciel principal chez Parasoft, se concentrant sur les tests de charge et de performance au sein de Parasoft SOAtest.

Recevez les dernières nouvelles et ressources sur les tests de logiciels dans votre boîte de réception.