2 points par GN⁺ 2025-03-01 | 1 commentaires | Partager sur WhatsApp
  • Par le pass�e9, l'utilisation CPU de mon syst�e8me a atteint 300 %, saturant enti�e8rement les 32 c53urs
  • J'utilisais le runtime Java 17 et, en v�e9rifiant le temps CPU dans le thread dump puis en triant par temps CPU, j'ai trouv�e9 de nombreux threads similaires
  • Analyse du code en cause
    • La stack trace pointait vers la ligne 29 de la classe BusinessLogic
    • Le code en question parcourait la liste unrelatedObjects tout en ins�e9rant les valeurs de relatedObject dans treeMap
    • Il s'agissait donc d'un code inefficace, car unrelatedObject n'�e9tait pas utilis�e9 �e0 l'int�e9rieur de la boucle

Correction du code et tests

  • La boucle inutile a �e9t�e9 supprim�e9e et remplac�e9e par une seule ligne�a0: treeMap.put(relatedObject.a(), relatedObject.b());
  • Des tests unitaires ont �e9t�e9 ex�e9cut�e9s avant et apr�e8s la modification, mais le probl�e8me n'a pas pu �eatre reproduit
  • M�eame lorsque treeMap et unrelatedObjects contenaient chacun plus d'un million d'�e9l�e9ments, le probl�e8me ne survenait pas

D�e9couverte de la cause

  • treeMap �e9tait acc�e9d�e9 simultan�e9ment par plusieurs threads, sans synchronisation
  • Le probl�e8me provenait donc de modifications concurrentes de TreeMap par plusieurs threads

Reproduction du probl�e8me par l'exp�e9rimentation

  • Une exp�e9rience a �e9t�e9 men�e9e dans laquelle plusieurs threads mettaient �e0 jour al�e9atoirement un TreeMap partag�e9
  • Un bloc try-catch a �e9t�e9 utilis�e9 pour ignorer les NullPointerException
  • Les r�e9sultats ont montr�e9 que l'utilisation CPU pouvait monter jusqu'�e0 500�a0%

Conclusion

  • Des modifications concurrentes non synchronis�e9es d'un TreeMap peuvent provoquer de graves probl�e8mes de performances
  • Pour �e9viter ce type de probl�e8me, il est recommand�e9 de synchroniser TreeMap ou d'utiliser une collection thread-safe comme ConcurrentMap

1 commentaires

 
GN⁺ 2025-03-01
Commentaires Hacker News
  • Je pensais qu’une race condition provoquait une corruption de données ou un deadlock, mais je n’avais pas envisagé qu’elle puisse aussi causer des problèmes de performance. Les données peuvent être corrompues d’une manière qui crée une boucle infinie

    • Dans un projet, je pense qu’il faut en principe corriger les erreurs, comportements anormaux et avertissements, car ils peuvent provoquer des problèmes sans rapport apparent
    • Il est bien connu que les collections centrales de Java ne sont pas thread-safe par conception. L’OP devrait vérifier si plusieurs threads manipulent aussi des collections dans d’autres parties du code
    • La solution la plus simple consiste à encapsuler TreeMap avec Collections.synchronizedMap, ou à passer à ConcurrentHashMap puis à trier lorsque c’est nécessaire
    • On peut rendre thread-safe des opérations individuelles sur la map, mais rien ne garantit qu’une séquence d’opérations le soit. On ne peut pas non plus être certain que l’objet propriétaire de TreeMap soit thread-safe
    • Comme solution discutable, suivre les nœuds déjà visités n’est pas une bonne approche. La collection reste non thread-safe et peut encore échouer d’autres façons plus subtiles
    • Un développeur attentif aux détails pourrait remarquer l’association entre threads et TreeMap, ou suggérer de ne pas utiliser TreeMap si des éléments triés ne sont pas nécessaires. Mais ici, cela n’a pas été le cas
    • Le problème vient d’une violation du contrat de la collection, et remplacer TreeMap par HashMap resterait malgré tout incorrect
  • Dans du code où plusieurs threads travaillent, la seule stratégie vraiment sûre est de rendre tous les objets immuables, et de limiter ceux qui ne peuvent pas l’être à de petites sections autonomes et strictement contrôlées

    • J’ai réécrit un module central en suivant ces principes, et il est passé d’une source permanente de problèmes à l’une des parties les plus robustes de la base de code
    • Avec ces règles en place, les revues de code sont devenues bien plus simples
  • La remarque « on pouvait à peine se connecter en ssh » m’a rappelé l’époque du master, quand j’utilisais un Sun UltraSparc 170

    • Un nouvel utilisateur ou un étudiant avait voulu exécuter du travail en parallèle, avait découpé un gros fichier texte en plusieurs sections par numéros de ligne, puis avait traité chaque section en parallèle
    • Une grande quantité de RAM était utilisée, et les tentatives de swap faisaient des allers-retours frénétiques pour lire différentes sections du même fichier
    • Il était impossible d’obtenir une invite de connexion sur la console, mais il y avait déjà une session ouverte, ce qui a permis d’obtenir une session root et de corriger le problème
    • Le vrai problème venait d’une mauvaise compréhension des limites du système
  • Le code pourrait simplement être réduit à ceci

    • Le code d’origine n’effectue treeMap.put que lorsque <i>unrelatedObjects</i> n’est pas vide. C’est peut-être un bug
    • Il faut vérifier si <i>a</i> et <i>b</i> renvoient bien la même valeur à chaque fois, et si <i>treeMap</i> se comporte réellement comme une map
  • Une autre façon d’obtenir une boucle infinie consiste à utiliser une implémentation de <i>Comparator</i> ou <i>Comparable</i> qui n’assure pas un ordre total cohérent

    • Cela n’a rien à voir avec la concurrence et peut survenir selon les données et l’ordre de traitement
  • On pourrait envisager de détecter les cycles avec un compteur croissant, puis de lever une exception si on dépasse la profondeur de l’arbre ou la taille de la collection

    • Cela ne demande pratiquement ni mémoire supplémentaire ni surcharge CPU, et a davantage de chances d’être accepté
  • En Java, exécuter des opérations concurrentes sur des objets non thread-safe produit les bugs les plus intéressants

  • Quelqu’un demande si un TreeMap non protégé peut vraiment provoquer une utilisation à 3 200 %

    • J’ai déjà vu un problème similaire vers 2009, et cela peut manifestement encore arriver
    • C’est décevant pour ceux qui pensent qu’une data race n’est « qu’un petit problème »
  • L’auteur a découvert une sorte de Poison Pill. C’est plus courant dans les systèmes d’event sourcing : un message qui tue tout ce qu’il rencontre

    • Quand une structure de données atteint un état illégal, tous les threads suivants se retrouvent piégés dans la même bombe logique
  • Les exceptions dans les threads sont un problème absolu

    • Il y a une histoire de chasse au bug terrifiante avec C++, select() et des threads qui lançaient des exceptions