Trois exemples de bugs qui ne se cachent à personne





J'écris beaucoup sur la recherche de bogues délicats - bogues de processeur, bogues de noyau, allocation de mémoire intermédiaire de 4 Go, mais la plupart des bogues ne sont pas si exotiques. Parfois, pour trouver un bogue, il suffit de regarder le tableau de bord du serveur, de passer quelques minutes dans le profileur ou de lire les avertissements du compilateur.



Dans cet article, je couvrirai trois bogues majeurs que j'ai trouvés et corrigés; tous ne se sont pas cachés du tout et ont simplement attendu que quelqu'un les remarque.



Surprise dans le processeur du serveur







Il y a plusieurs années, j'ai passé quelques semaines à étudier le comportement de la mémoire sur des serveurs de jeux en direct. Les serveurs exécutaient Linux dans des centres de données distants, donc la plupart du temps était passé à obtenir les autorisations nécessaires pour que je puisse accéder aux serveurs, ainsi qu'à apprendre à travailler efficacement avec perf et d'autres outils de diagnostic Linux. J'ai découvert une série de bogues qui entraînaient une consommation de mémoire trois fois plus élevée que nécessaire et les ai corrigés:



  • J'ai trouvé une incompatibilité dans l'ID de la carte, ce qui a amené chaque jeu à ne pas utiliser la même copie d'environ 20 Mo de données, mais à en charger une nouvelle.
  • J'ai trouvé une variable globale inutilisée (!) De 50 Mo (!!), qui était définie sur zéro memset (!!!), ce qui l'a amenée à consommer de la RAM physique dans chaque processus.
  • Divers bogues moins sérieux.


Mais notre histoire ne sera pas à ce sujet.



Après avoir pris le temps d'apprendre à profiler nos serveurs de jeu, j'ai réalisé que je pouvais enquêter un peu plus en profondeur. Par conséquent, j'ai couru perf sur les serveurs de l'un de nos jeux. Le premier processus serveur que j'ai profilé était ... bizarre. En regardant les données échantillonnées par le processeur en direct, j'ai vu qu'une seule fonction consommait 100% du temps CPU. Cependant, seules quatorze instructions ont été exécutées dans cette fonction. Cela n'avait aucun sens.



Au début, j'ai supposé que j'utilisais perf correctementou mal interpréter les données. J'ai regardé certains des autres processus du serveur et j'ai trouvé qu'environ la moitié d'entre eux étaient dans un état étrange. La seconde moitié avait un profil CPU plus normal.



La fonction qui nous intéresse passait par la liste chaînée des nœuds de navigation. J'ai demandé à mes collègues et j'ai trouvé un programmeur qui a dit que des problèmes de précision en virgule flottante pouvaient amener le jeu à générer des listes de navigation en boucle. Ils ont toujours voulu limiter le nombre maximum de nœuds pouvant être contournés, mais ils n'ont jamais réussi à le faire.



Alors le puzzle est résolu? L'instabilité des calculs en virgule flottante provoque des boucles dans les listes de navigation, ce qui oblige le jeu à les contourner à l'infini - c'est tout, le comportement est expliqué.



Mais ... une telle explication signifierait que lorsque cela se produit, le processus serveur entre dans une boucle infinie, tous les joueurs devront se déconnecter, et le processus serveur consommera à l'infini tout le cœur du processeur. Si tel était le cas, ne finirions-nous pas par manquer de ressources sur nos serveurs? Est-ce que personne n'aurait remarqué cela?



J'ai cherché des données de surveillance du serveur et j'ai trouvé quelque chose comme ceci:







Pendant toute la période de surveillance (un à deux ans), j'ai observé des fluctuations quotidiennes et hebdomadaires de la charge du serveur, qui se superposaient au modèle mensuel. Le niveau d'utilisation du processeur a augmenté progressivement, puis est tombé à zéro. Après avoir demandé un peu plus, j'ai découvert que les serveurs étaient redémarrés une fois par mois. Et enfin, la logique est apparue dans tout ça:



  • , .
  • , , .
  • CPU , 50%.
  • .


Le bogue a été corrigé en ajoutant quelques lignes de code qui ont cessé de parcourir la liste après vingt nœuds de navigation, ce qui a probablement permis d'économiser plusieurs millions de dollars en coûts de serveur et d'alimentation. Je n'ai pas trouvé ce bogue en regardant les graphiques de surveillance, mais quiconque les a consultés pouvait le faire.



J'aime le fait que la fréquence du bug coïncide parfaitement avec la maximisation du coût de celui-ci; en même temps, il n'a jamais causé de problèmes assez graves. Ceci est similaire à l'action d'un virus qui évolue pour faire éternuer les gens, pas pour les tuer.



Chargement lent







La productivité du développeur de logiciels est étroitement liée à la vitesse du cycle d'édition / compilation / liaison / débogage. En d'autres termes, cela dépend du temps qu'il faut après avoir apporté une modification au fichier source pour exécuter le nouveau binaire avec la modification effectuée. J'ai fait un excellent travail au fil des ans pour réduire les temps de compilation / liaison, mais les temps de chargement sont également importants. Certains jeux font énormément de travail à chaque fois qu'ils commencent. Je suis impatient et donc souvent le premier à passer des heures ou des jours à faire charger le jeu quelques secondes plus vite.



Dans ce cas, j'ai exécuté mon profileur préféré et j'ai regardé le graphique d'utilisation du processeur pendant la phase de chargement initiale du jeu. Une étape semblait la plus prometteuse: il a fallu environ dix secondes pour initialiser certaines données d'éclairage. J'espérais qu'un moyen pourrait être trouvé pour accélérer ces calculs en économisant environ cinq secondes au démarrage. Avant de plonger dans l'étude, j'ai consulté un graphiste. Il a déclaré:



«Nous n'utilisons pas ces données d'éclairage dans le jeu. Supprimez simplement ce défi. "



Oh génial. C'était facile.



En passant une demi-heure à profiler et à changer une ligne, j'ai pu réduire de moitié le temps de chargement du menu principal, et cela n'a pas demandé d'efforts extraordinaires.



Départ prématuré



En raison du nombre arbitraire d'arguments dans le formatage, il est printftrès facile d'obtenir une erreur d'incompatibilité de type. En pratique, les résultats peuvent varier considérablement:



  1. printf («0x% 08lx», p); // Affiche le pointeur comme int - tronqué ou pire sur 64 bits
  2. printf («% d,% f», f, i); // Changer les emplacements de float et int - peut afficher des absurdités, ou cela peut fonctionner (!)
  3. printf («% s% d», i, s); // Changement de l'ordre de string et int - cela entraînera probablement un plantage


La norme dit que de telles incompatibilités de type sont des comportements non définis, et certains compilateurs génèrent du code qui se bloque délibérément avec l'une de ces incompatibilités, mais ce qui précède répertorie les résultats les plus probables (note: la question de savoir pourquoi le deuxième paragraphe produit souvent les résultats souhaités est bonne Puzzle de connaissances ABI ).



De telles erreurs sont très faciles à faire, de sorte que tous les compilateurs modernes ont la capacité d'avertir les développeurs qu'une discordance s'est produite. Gcc et clang ont tous deux des annotations de fonction de style printf, et ils peuvent avertir des non-concordances (cependant, malheureusement, les annotations ne fonctionnent pas avec les fonctions de style wprintf). VC ++ a des annotations (malheureusement d'autres) que / analyser peut utiliser pour avertir des discordances, mais si vous n'utilisez pas / analysez, il avertira uniquement des fonctions de style CRT de style printf / wprintf, pas de vos fonctions personnalisées ...



La société pour laquelle j'ai travaillé a annoté ses fonctions dans le style printf afin que gcc / clang émette des avertissements, mais a ensuite décidé d'ignorer les avertissements. C'est une décision étrange, car de tels avertissements sont des indicateurs parfaitement précis de bogues - le rapport signal sur bruit est infini.



J'ai décidé de commencer à nettoyer ces bogues en utilisant VC ++ et / d'analyser les annotations pour trouver exactement tous les bogues. J'ai résolu la plupart des erreurs et effectué un gros changement en attendant que le code soit vérifié avant de le soumettre.







Il y a eu une panne de courant dans le centre de données ce week-end et tous nos serveurs sont tombés en panne (probablement en raison d'erreurs de configuration de l'alimentation). Le personnel d'urgence s'est précipité pour tout restaurer et tout réparer avant que trop d'argent ne soit perdu.



L'aspect amusant des bogues printf est qu'ils se comportent mal 100% du temps. Autrement dit, s'ils vont afficher des données incorrectes ou provoquer le blocage du programme, cela se produit à chaque fois. Par conséquent, ils peuvent rester dans le programme uniquement s'ils se trouvent dans un code de journalisation qui n'est jamais lu ou dans un code de gestion d'erreurs rarement exécuté.



Il s'est avéré que l'événement "redémarrage simultané de tous les serveurs" provoquait le déplacement du code sur des chemins qui ne seraient normalement pas exécutés. Les serveurs de démarrage ont commencé à rechercher d'autres serveurs, ne les ont pas trouvés et ont affiché quelque chose comme ce message:



fprintf (journal, "Impossible de trouver le serveur% s. Code d'erreur% d. \ n", err, nom_serveur);


Oups. Incompatibilité de type pour un nombre arbitraire d'arguments. Et le départ.



Les secouristes ont un problème supplémentaire. Les serveurs devaient être redémarrés, mais cela n'a pas pu être fait avant que les vidages sur incident ne soient examinés, qu'un bogue ait été découvert, les binaires du serveur n'ont pas été reconstruits et une nouvelle version a été publiée. C'était un processus assez rapide - semble-t-il, pas plus de quelques heures, mais cela aurait bien pu être évité.



Je pensais que cette histoire démontrait parfaitement pourquoi nous devrions passer du temps à dépanner les causes de ces avertissements - pourquoi ignorer les avertissements qui leur disent que le code va définitivement planter ou se comporter mal lorsqu'il est exécuté? Cependant, personne ne s'est inquiété du fait que l'élimination de cette catégorie d'avertissements pourrait nous faire économiser plusieurs heures de temps d'arrêt. En fait, la culture de l'entreprise ne semblait intéressée par aucun de ces correctifs. Mais c'est ce dernier bug qui m'a fait comprendre qu'il était temps de passer à une autre entreprise.



Quelles leçons peut-on en tirer?



Si toutes les personnes impliquées travaillent dur sur les fonctionnalités du produit et corrigent des bogues bien connus, il y a probablement des bogues très simples qui sont sur l'affichage public. Passez un peu de temps à étudier les journaux, à nettoyer les avertissements du compilateur (bien qu'en fait, si vous avez des avertissements du compilateur, cela vaut probablement la peine de repenser les décisions que vous avez prises dans la vie), exécutez le profileur pendant quelques minutes. Vous obtenez des points supplémentaires si vous ajoutez votre propre système de journalisation, activez de nouveaux avertissements ou utilisez un profileur que personne d'autre n'utilise que vous.



Si vous faites d'excellents correctifs qui améliorent l'utilisation ou la stabilité de la mémoire / du processeur et que personne ne s'en soucie, trouvez une entreprise qui l'apprécie.



Discussion sur Hacker News ici , discussion Reddit ici , discussion Twitter ici .






La publicité



Un serveur fiable à louer et le bon choix d'un plan tarifaire vous permettront d'être moins distrait par des notifications de surveillance désagréables - tout fonctionnera en douceur et avec un temps de disponibilité très élevé!









All Articles