Programmeurs plombiers, ou l'histoire d'une fuite et les difficultés à y faire face

C'était le mardi 25 février. La sortie difficile de la version le samedi 22 février était déjà dans le passé. Il semblait que tout le pire était derrière, et rien n'annonçait de problème. Mais tout a changé à un moment donné lorsqu'une erreur de surveillance a provoqué une fuite de mémoire sur le processus de coordination du service de contrôle d'accès.

D'où? Les derniers changements majeurs dans la base de code du coordinateur étaient dans la version précédente il y a plus de deux mois, et après cela, rien de remarquable n'est arrivé à la mémoire. Mais, malheureusement, les horaires de surveillance étaient inflexibles - la mémoire du coordinateur a manifestement commencé à couler quelque part, une grande flaque d'eau apparaissant sur le plancher de service, ce qui signifie que l'équipe de plomberie avait beaucoup de travail à faire.



D'abord, nous faisons une petite digression. Entre autres choses, VLSI vous permet de suivre les heures de travail et de surveiller l'accès par modèle de visage, empreinte digitale ou cartes d'accès. Dans le même temps, VLSI communique avec les contrôleurs de terminaux (serrures, tourniquets, terminaux d'accès, etc.). Un service distinct communique avec les appareils. Il est passif, interagit avec les dispositifs de contrôle d'accès en fonction de leurs propres protocoles implémentés via HTTP (S). Il est écrit sur la base de la pile standard des services de notre entreprise: la base de données PostgreSQL, Python 3 est utilisé pour la logique métier, étendu avec les méthodes C / C ++ de notre plateforme.

Un nœud de service Web typique comprend les processus suivants:

  • Monitor est le processus racine.
  • Un coordinateur est un processus enfant d'un moniteur.
  • Processus de travail.

Monitor est le premier processus de service Web. Sa tâche consiste à appeler fork (), à démarrer le processus du coordinateur enfant et à surveiller son travail. Le coordinateur est le principal processus du service Web, c'est lui qui reçoit les demandes des appareils externes, envoie les réponses et équilibre la charge. Le coordinateur envoie la demande aux processus de travail pour exécution, ils l'exécutent, transfèrent la réponse à la mémoire partagée et informent le coordinateur que la tâche est terminée et que vous pouvez récupérer le résultat.

Qui est à blâmer et que faire?


Ainsi, le coordinateur du service de contrôle d'accès se distingue des coordinateurs des autres services de notre entreprise par la présence d'un serveur web. Les coordinateurs des autres services fonctionnaient sans fuites, le problème a donc dû être recherché dans notre configuration. Pire encore, sur les bancs d'essai, la nouvelle version existe depuis longtemps et personne n'a remarqué de problèmes de mémoire. Ils ont commencé à regarder de plus près et ont constaté que la mémoire ne circule que sur l'un des stands, et même avec un succès variable - ce qui signifie que le problème n'est toujours pas si facilement reproduit.



Que faire? Comment trouver une raison? Tout d'abord, nous avons pris un vidage de mémoire et l'avons envoyé à des spécialistes de la plate-forme pour analyse. Le résultat - il n'y a rien dans le dépotoir: aucune raison, aucun indice dans quelle direction regarder plus loin. Nous avons vérifié les changements dans le code du coordinateur par rapport à la version précédente - tout d'un coup, nous avons fait de terribles modifications, mais vous n'avez tout simplement pas compris tout de suite? Mais non - seuls quelques commentaires ont été ajoutés au code du coordinateur, mais quelques méthodes ont été déplacées vers de nouveaux fichiers - en général, rien de criminel.

Ils ont commencé à se tourner vers nos collègues, les développeurs du cœur de notre service. Ils ont nié avec confiance la possibilité même de participer à notre malheur, mais ont proposé d'implanter la surveillance tracemalloc dans le service. Aussitôt dit, aussitôt fait, lors du prochain hotfix, nous finalisons le service, testons rapidement, avant de lancer la bataille.

Et que voyons-nous? Maintenant, notre mémoire s'écoule non seulement rapidement, mais très rapidement - la croissance est devenue exponentielle. Nous attribuons le premier pic aux forces du mal et aux facteurs qui les accompagnent, mais le deuxième pic, plusieurs heures après le premier, indique clairement qu'il faut trop de temps pour attendre la publication du prochain correctif avec un tel comportement d'urgence du service. Par conséquent, nous prenons les résultats de tracemalloc et corrigeons le service, annulant les modifications avec surveillance afin de revenir au moins à une croissance linéaire.



Il semblait que nous avions les résultats de tracemalloc travaillant sur la mémoire allouée en Python, maintenant nous allons les regarder et trouver le coupable de la fuite, mais ce n'était pas là - les données collectées n'ont pas les pics de 5,5 Go que nous avons vus sur les graphiques de surveillance. La mémoire maximale utilisée n'est que de 250 Mo, et même traceMalloc en mange 130 Mo. Ceci est en partie explicable - tracemalloc vous permet de voir la dynamique de la mémoire en Python, mais il ne connaît pas l'allocation de mémoire dans les packages C et C ++ qui sont implémentés par notre plateforme. Il n'a pas été possible de trouver quelque chose d'intéressant dans les données obtenues, la mémoire est allouée en volumes acceptables à des objets ordinaires tels que des flux, des chaînes et des dictionnaires - en général, rien de suspect. Ensuite, nous avons décidé de supprimer tout ce qui était superflu des données, ne laissant que la consommation totale de mémoire et de temps, et de visualiser.Bien que la visualisation n'ait pas aidé à répondre aux questions «ce qui se passe» et «pourquoi», avec son aide, nous avons vu une corrélation avec les données de la surveillance - ce qui signifie que nous avons certainement un problème quelque part et que nous devons le rechercher.



À ce moment-là, notre équipe de plombiers était à court d'idées pour savoir où chercher une fuite. Heureusement, l'oiseau nous a chanté qu'un changement majeur de la plate-forme s'est produit - la version Python est passée de 3.4 à 3.7, et c'est un immense champ de recherche.

Nous avons décidé de rechercher les problèmes liés aux fuites de mémoire dans Python 3.7 sur Internet, car il est certain que quelqu'un a déjà rencontré ce problème. Pourtant, Python 3.7 a été publié il y a longtemps, nous n'y sommes passés qu'avec la mise à jour actuelle. Heureusement, la réponse à notre question a été trouvée rapidement, et il y avait un problème et une demande de tirage pour résoudre le problème, et elle-même était dans les modifications apportées par les développeurs Python.
Qu'est-il arrivé?

Depuis la version 3.7, le comportement de la classe ThreadingMixIn a changé, dont nous héritons de notre serveur Web pour traiter chaque demande dans un thread séparé. Dans la classe ThreadingMixIn, ils ont ajouté l'entrée de tous les threads créés dans un tableau. En raison de ces modifications, les instances de classe qui gèrent les connexions de périphérique ne sont pas libérées après la fin et le garbage collector en Python ne peut pas effacer la mémoire des threads épuisés. C'est ce qui a conduit à la croissance linéaire de la mémoire allouée en proportion directe avec le nombre de requêtes à notre serveur.

Le voici, le code insidieux du module Python avec un gros trou (le code en Python 3.5 est affiché à gauche avant les changements, à droite - en 3.7, après):



En découvrant la raison, nous avons facilement éliminé la fuite: dans notre classe d'héritiers, nous avons changé la valeur du drapeau qui renvoyait l'ancien comportement, et c'est tout - une victoire! Les flux sont créés comme auparavant, sans écrire dans la variable de classe, mais nous observons une image agréable sur les graphiques de surveillance - la fuite a été corrigée!



C'est agréable d'écrire à ce sujet après une victoire. Nous ne sommes probablement pas les premiers à rencontrer ce problème après le passage à Python 3.7, mais probablement pas le dernier. Pour nous, nous avons conclu que nous avions besoin de:

  • Adoptez une approche plus sérieuse pour évaluer les conséquences possibles de changements majeurs, surtout si d'autres décisions appliquées dépendent de nous.
  • En cas de changements globaux dans la plate-forme, tels que, par exemple, la modification de la version de Python, vérifiez votre code pour d'éventuels problèmes.
  • Répondez à tout changement suspect dans les calendriers de surveillance non seulement des services de combat, mais aussi des services de test. Malgré le garbage collector actuel, il y a aussi des fuites de mémoire dans Python.
  • Il faut être prudent avec les outils d'analyse de la mémoire comme tracemalloc, car leur mauvaise utilisation peut aggraver les choses.
  • Vous devez être préparé au fait que la détection des fuites de mémoire nécessitera de la patience, de la persévérance et un peu de travail de détective.

Eh bien, je tiens à exprimer ma gratitude à tous ceux qui ont aidé à faire face aux travaux de plomberie d'urgence et à revenir une fois de plus à son ancienne capacité de travail à notre service!

All Articles