Webinaire en vedette : MISRA C++ 2023 : tout ce que vous devez savoir | Voir le séminaire

Maîtriser les tests de performances des applications Java avec Parasoft

27 décembre 2023
9 min lire

Une seule ligne de code peut faire des ravages sur l'ensemble des performances de votre logiciel si elle n'est pas détectée et corrigée à temps. Consultez cet article pour savoir comment surveiller les threads Java et comprendre les lignes de code spécifiques de votre application qui pourraient entraîner des bogues potentiels dans les performances de votre application.

Les problèmes liés aux threads peuvent nuire aux performances d’une application Web API d’une manière qui est 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 outils de test de performances vous permettent de convertir tous les tests fonctionnels en tests de charge et de performances.

Pourquoi les tests de performances sont importants

Les applications serveur, comme les serveurs Web, sont conçues pour traiter plusieurs demandes client simultanément. Le nombre de requêtes client traitées simultanément par un serveur est généralement appelé « charge ». Une charge croissante peut entraîner des temps de réponse lents, des erreurs d'application et éventuellement un crash. Les problèmes de concurrence et de synchronisation liés aux threads causés par le traitement simultané des requêtes peuvent contribuer à toutes ces catégories de comportements indésirables. Pour cette raison, ils doivent être minutieusement testés et éliminés avant que l’application ne soit déployée en production.

L'une des plus grandes différences qui sépare Test de performance Parmi les autres types de tests logiciels, il y a l’accent systématique mis sur les problèmes de concurrence. Le mécanisme multithread utilisé par les applications serveur pour traiter simultanément plusieurs requêtes client peut entraîner des erreurs et des inefficacités induites par la concurrence dans le code d'application, qui ne se produisent pas dans d'autres types de tests, généralement monothread, tels que les tests unitaires ou d'intégration.

Du point de vue de la validation du code d’application pour les problèmes de concurrence et de synchronisation liés aux threads, l’objectif d’un test de performances est double.

  1. Pour exposer ces problèmes s’ils existent.
  2. Pour obtenir des détails pertinents lorsque ces problèmes surviennent et fournir un maximum d'informations de diagnostic pour aider à les résoudre.

Atteindre le premier de ces objectifs nécessite de soumettre l'application testée (AUT) à une charge comparable à celle qu'elle subira en production en appliquant un flux de requêtes client simulées avec une concurrence de charge, une intensité de requête et une durée de test correctement configurées. Pour en savoir plus, consultez le Guide des meilleures pratiques de test de performance.

Atteindre le deuxième de ces objectifs est le sujet de cet article de blog.

Meilleures pratiques pour les tests de performances des applications Java

Lorsqu'il s'agit de détecter les problèmes liés aux threads et leurs détails, les principales questions sont de savoir comment les détecter, que faire avec les données de diagnostic et quand rechercher de tels problèmes. Vous trouverez ci-dessous les listes de réponses clés à ces questions.

Comment détecter les problèmes liés aux threads et utiliser les données de diagnostic

  • Surveillez les threads AUT pour détecter les blocages ainsi que les états BLOCKED ou PARKED qui empêchent les threads de travailler.
  • Créez des contrôles de régression des performances basés sur le moniteur de thread pour détecter automatiquement ces problèmes.
  • Collectez des détails, tels que les traces de pile, des threads AUT en difficulté pour diagnostiquer le comportement indésirable des threads lorsqu'il se produit.
  • Superposez les graphiques des indicateurs de performances clés, tels que les graphiques du temps de réponse maximal, etc., avec les graphiques du moniteur de threads pour rechercher des corrélations entre les problèmes de threads et les performances des applications.

Demandez à votre fournisseur de produits de test de performances comment son outil peut vous aider à détecter et à diagnostiquer les problèmes de thread AUT.

Quand rechercher des problèmes liés aux threads

La configuration apparemment aléatoire de certains problèmes liés aux threads pose des problèmes supplémentaires. défis des tests de performances pour détecter et diagnostiquer les pannes rares. Corriger une erreur rare, une sur un million, peut devenir un cauchemar pour les équipes d'assurance qualité et de développement à la fin des étapes d'application du cycle de vie.

Pour augmenter les chances de détecter de telles erreurs, l'application soumise au test de charge doit être surveillée en permanence pendant toutes les étapes des tests de performances. Vous trouverez ci-dessous une liste des principaux types de tests de performances et pourquoi vous devez utiliser des moniteurs de threads lorsque vous les exécutez.

  • Tests de fumée ou de base pour détecter rapidement les problèmes liés aux threads.
  • Tests de charge valider que l'AUT sous la charge de production attendue ne souffre pas de ces problèmes.
  • Tests de résistance pour vérifier le niveau de concurrence. Il peut être beaucoup plus élevé dans un test de contrainte que dans un test de charge classique, ce qui augmente la probabilité d'apparition de problèmes liés aux threads.
  • Tests d'endurance peut s'exécuter beaucoup plus longtemps que les tests de charge réguliers, ce qui augmente la probabilité d'apparition de problèmes rares liés aux threads.

Voir le Guide des meilleures pratiques de test de performance pour plus de détails sur les types de tests de performances et leur utilisation.

À titre d'exemple de la façon dont ces pratiques peuvent être appliquées, nous allons maintenant suivre une hypothétique équipe de développement Java qui rencontre quelques problèmes de thread courants lors de la création d'une application API Web et diagnostiquer certains problèmes de performances courants liés aux threads. Après cela, nous examinerons des exemples plus complexes d’applications réelles. Notez que certains codes sous-optimaux dans les exemples ci-dessous ont été ajoutés intentionnellement à des fins de démonstration.

L'étude de cas de l'application bancaire

Notre hypothétique équipe de développement 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.

Obtenez des instructions étape par étape sur la façon de configurer des tests de performances automatisés.

Application bancaire version 1 : Conditions de concurrence dans la mise en œuvre 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 JVM Threads Monitor au projet Load Test 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.

La modification du code est transmise au référentiel et est récupérée par le processus de test de performances CI. Le lendemain, les développeurs constatent que le test de performances a échoué du jour au lendemain. L'application de la Banque a cessé de répondre peu après le début du test de performance de l'opération de transfert. L'inspection des graphiques JVM Threads Monitor dans le rapport de test de charge montre rapidement qu'il existe des threads bloqués dans l'application Bank. Voir la figure 1.a. Les détails du blocage ont été enregistrés par JVM Threads Monitor dans le cadre du rapport et affichent les lignes exactes de code responsables du blocage. Voir le listing 1.b.

Graphique montrant le nombre de threads bloqués dans l'application testée (AUT).
Fig 1.a : Nombre de threads bloqués dans l'application testée (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 JVM Threads.

Application bancaire version 3 : résolution des blocages

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

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

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 l'opération est multiplié par plus de cinq, passant de 30 à plus de 150 millisecondes. Voir la figure 2.a. Le graphique JVM Threads Monitor BlockedRatio montre que de 60 à 75 % des threads d'application sont à l'état BLOQUÉ pendant l'exécution du test de charge. Voir la figure 2.b. Les détails enregistrés par le moniteur indiquent que les threads d'application sont bloqués lors de la tentative d'accès à la section globalement synchronisée à la ligne 15. Voir le listing 2.c.

Deux graphiques côte à côte. La figure 2a montre les temps de réponse de l'application bancaire version 3 avec une ligne bleue passant à 160 % restant stables par rapport aux temps de réponse de la version 1 avec une ligne verte atteignant 40 % avec de légères fluctuations à la baisse. Sur la droite, la figure 2.b montre le pourcentage de threads bloqués dans l'AUT atteignant 70 puis tombant à 50 après 100 secondes de temps d'achèvement.

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 JVM Threads Monitor.

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é) en graphe rouge, de la version 3 (synchronisation globale des objets) en graphe bleu et de la version 1 (opération de transfert non synchronisé) en 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.

Graphique montrant le temps de réponse aux opérations de transfert de l'application bancaire version 4 (rouge), version 3 (bleu) et version 1 (vert).
Fig 3.a : Temps de réponse aux opérations de transfert de l'application bancaire version 4 (rouge), version 3 (bleu) et version 1 (vert).

Défis de performance réels

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

Les exemples d'applications bancaires ci-dessus montrent comment résoudre des cas isolés typiques de dégradation des performances provoqués par des problèmes de thread. Les cas réels peuvent être plus compliqués. Les graphiques de la figure 4 montrent un exemple d'application API REST de production dont le temps de réponse n'a cessé d'augmenter à mesure que le test de performances progressait. Le temps de réponse des applications a augmenté à un rythme plus faible pendant la première moitié du test et à un rythme plus élevé dans la seconde moitié. Voir la figure 4.a.

Dans la première moitié du test, la croissance du temps de réponse était corrélée au temps total passé par les threads d’application dans l’état BLOQUÉ. Voir la figure 4.b.

Dans la seconde moitié du test, la croissance du temps de réponse était corrélée au nombre de threads d'application à l'état PARKED. Voir la figure 4.c.

Les traces de pile capturées par le moniteur de threads JVM de test de charge ont fourni les détails : l'une pointait vers 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.

Trois graphiques montrent. Figure 4.a. montre que le temps de réponse de l'application augmente jusqu'à 10,000 1500 à partir de 4 4 secondes de fin de test. Figure XNUMX.b. affiche le temps passé par les threads d'application dans un état bloqué. La figure XNUMX.c montre le nombre de threads d'application dans un état parqué.

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 des 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 présentant des pics intermittents de temps de réponse moyens et maximaux. Voir la figure 5.a.

De tels 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 corrélé dans le moniteur BlockedTime, 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 cumulé que les threads JVM ont passé dans l'état BLOCKED entre les invocations 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 de l'existence de problèmes de blocage des threads.

Le moniteur BlockedThreads, parce qu'il prend des instantanés de threads réguliers, peut manquer certains événements bloquant les threads, mais du côté positif, lorsqu'il capture de tels événements, il fournit des informations détaillées sur la cause 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 sont exécutés 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 des applications.

Trois graphiques présentés côte à côte. Figure 5.a. montre un pic dans les temps de réponse maximum (jaune) et moyen (gris) de l'application. Figure 5.b. montre un pic de corrélation dans le moniteur BlockedTime. Figure 5.c. montre un pic de corrélation dans le moniteur des threads bloqués.

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

Le moniteur Load Test JVM Threads, 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 un tel problème de performances, créez un test de régression des performances pour celui-ci. Le test consistera en une exécution de test de performance existante ou nouvelle et un nouveau contrôle de régression. Dans le cas de Parasoft Load Test, il s'agirait d'une métrique de moniteur QoS pour un canal JVM Threads Monitor pertinent. Par exemple, pour le problème décrit dans l'exemple 1, Fig. 4, créez une métrique Load Test QoS Monitor qui vérifie le temps que les threads d'application ont passé dans l'état BLOCKED 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.

Le rôle de l'automatisation dans les tests de performances des applications Java

Une fois que les contrôles de régression du moniteur JVM Threads ont été créés, ils peuvent être utilisés efficacement dans les tests de performances automatisés. Les contrôles de régression des performances sont un outil indispensable pour l’automatisation des tests, qui constitue à son tour un élément majeur du contrôle continu. tests de performances dans un pipeline DevOps. Des contrôles de régression des performances doivent être créés non seulement en réponse aux régressions de performances passées, mais également comme défense contre des problèmes potentiels.

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

Canal de surveillance des threadsQuand utiliser
Threads bloquésToujours. 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.
MoniteurDeadlockedThreads
Fils bloquésToujours. 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.
Temps Bloqué
Rapport Bloqué
Nombre Bloqué
Fils garésToujours. 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 threadsSouvent. 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.
DormirParfois. À utiliser pour les contrôles de régression des performances liés à ces états et pour les tests exploratoires.
En attente
Temps d'attente
AttenteRatio
Compte d'attente
NouveauThreadsRarement. À utiliser pour les contrôles de régression des performances liés à ces états de thread.
Fils inconnus

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 SOAtestLoad Test Continuum, le JVM Threads Monitor 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 mauvaises performances et vous aide à améliorer à la fois les performances des applications ainsi que la productivité des développeurs et de l'assurance qualité.

Guide des meilleures pratiques de test de performance