Comment débugger un programme stuck en production
Apprenez à trouver pourquoi votre service Python est bloqué sans avoir besoin de le redémarrer !
C’est une belle journée d'été, tout est calme; un peu trop calme peut-être.
Notre système de monitoring ne signale aucune erreur, alors que d'ordinaire, nous recevons quelques alertes Sentry par jour. Mais là, c'est silence radio, et c’est suspect.
En examinant nos données, nous nous rendons compte qu'il ne se passe rien depuis plusieurs heures. Pourtant, notre programme est configuré pour redémarrer automatiquement en cas de crash.
Nous nous précipitons sur la machine et exécutons un docker ps
pour vérifier si les conteneurs fonctionnent toujours. Oui, ils sont opérationnels.
Alors, que se passe-t-il ? Pourquoi l'activité semble-t-elle arrêtée ?
Let’s debug this sh*t.
Welcome to py-spy
py-spy is a sampling profiler for Python programs. It lets you visualize what your Python program is spending time on without restarting the program or modifying the code in any way. py-spy is extremely low overhead: it is written in Rust for speed and doesn't run in the same process as the profiled Python program. This means py-spy is safe to use against production Python code.
Les points fort de py-spy
La librairie permet de réaliser plusieurs actions intéressantes :
Profiling de code : C'est un statistical profiler.
Capacité à se brancher directement sur un processus en cours d'exécution sans nécessiter de modification du code, ou de redémarrage de l'instance.
Prise en charge du multiprocessing : Gestion efficace des environnements utilisant le multiprocessing, les workers gunicorn, etc…
Notre premier script à debug
Voici notre premier coupable : un script se retrouve stuck dans une boucle infinie et qui exécute time.sleep
sans cesse.
Nous allons exécuter ce script sur notre host, et observer quelles informations émergent.
Après l'avoir lancé, nous récupérons le PID de notre processus, une information cruciale pour établir la connexion avec py-spy
.
py-spy offre actuellement trois commandes principales pour recueillir les informations sur notre processus.
Explorons-les !
1) py-spy dump
py-spy
offre la possibilité d'afficher la call stack actuelle de chaque thread Python avec la commande dump
.
Nous avons également la faculté d’ajouter un flag —locals
afin de voir les variables et leurs valeurs actuelles.
py-spy dump --pid 70300
Dans cet exemple, nous observons que le blocage se situe à la ligne 7 et correspond effectivement à notre instruction time.sleep
.
2) py-spy record
Nous avons aussi la possibilité de nous connecter au processus, et de réaliser un enregistrement (record) sur une durée plus étendue.
Lorsque nous estimons avoir collecté suffisamment de données, un simple appui sur Ctrl + C
interrompt le profiling. Cette action génère un flamegraph, un visuel qui illustre où le code consacre la majorité de son temps d'exécution.
Exemple de commande pour lancer le record :
py-spy record —pid 8773
Le flamegraph se lit de haut en bas.
Dans notre cas, il confirme effectivement que la ligne 7 ( notre fameux time.sleep
) monopolise le temps d'exécution. Sans surprise !
Si vous le souhaitez ,vous pouvez visualiser ces résultats autrement; avec un speedscope par exemple, le lien est ici.
Si vous appréciez la newsletter StuffAndCode, vous pouvez vous inscrire afin de ne manquer aucun article !
🐍 Et recevez en PLUS, un article exclusif sur comment profiler n’importe quel type de code Python.
3) py-spy top
py-spy propose la commande top
afin de suivre le déroulement de notre programme. Nous pouvons lire les fonctions appelées, le temps d’execution etc …
Et c’est en temps réel ! Donc nul besoin d’être à l’aveugle comme sur la commande record
, ou d’avoir juste un point dans le temps avec la commande dump
. Ici nous visualisons directement ce qu’il se passe dans notre programme.
Cependant, nous pouvons constater que nous ne voyons pas exactement le numéro de la ligne executée par le programme.
Un cas concret
Nous sommes en 2k24, nos services tournent dans des containers et le code est légèrement plus compliqué qu’une boucle while avec un sleep. On va essayer de randomiser tout cela afin de simuler une anomalie, sans qu’elle produise un blocage complet.
Nous allons exécuter ce code dans un conteneur Docker utilisant Python 3.11, et rechercher où notre code consacre le plus de temps.
1) Debugger depuis l’host
Notre code tourne dans un container Docker, mais rien ne nous empêche de le debugger depuis notre host.
Pour cela rien de plus simple. Nous cherchons le PID de notre processus Python qui tourne dans Docker, et nous lançons les mêmes commandes que nous avons vues précedemment.
Il est fort probable que vous deviez exécuter la commande avec les droits root pour mener les observations.
De la même manière, nous pouvons faire un record de programme, toujours pour mettre en évidence les actions, et analyser le flamegraph.
2) Debugger depuis Docker
Envisageons que vous ne vouliez/puissiez installer py-spy sur votre machine host de production. Peut-être n’avez-vous pas de version Python compatible avec ?
Pas de soucis ! Nous pouvons debugger à l’aide de Docker.
Scénario 1 : se servir du container de notre programme
Py-spy est reconnu pour être low-overhead. Donc il aura un faible impact sur les performances de notre programme, et prendra peu de ressources de notre container.
Ainsi, nous pouvons imaginer ce scénario :
Entrer dans le container avec la commande
docker exec -ti <container_name> bash
Installer py-spy :
pip install py-spy
Trouver le PID de notre programme
Lancer le debug avec la commande
py-spy top
par exemple
Nous avons un plan, testons le :
WTF ? Nous avons une “Operation not permitted” alors que nous sommes root ?
py-spy offre de la documentation à ce sujet que vous pouvez retrouver ici .
On nous dit :
Running py-spy inside of a docker container will also usually bring up a permissions denied error even when running as root.
This error is caused by docker restricting the process_vm_readv system call we are using. This can be overridden by setting
--cap-add SYS_PTRACE
when starting the docker container.Alternatively you can edit the docker-compose yaml file
your_service: cap_add: - SYS_PTRACE
Note that you'll need to restart the docker container in order for this setting to take effect.
Donc, il est explicitement écrit dit que si l’on désire debugger notre programme dans Docker, il faut ajouter des capabilities et redémarrer notre container.
Ce n’est vraiment pas incroyable, vu que potentiellement le bug se produit trés rarement, et ignorons quand il va se reproduire.
Aussi, devoir faire des modifications de docker-compose juste pour çela, ce n’est pas fou.
✨Soyez serein, nous avons une autre solution ! ✨
Scénario 2: Debug depuis un autre container Docker
Pourquoi pas démarrer un autre container en lui ajoutant la capability nécessaire pour profiler le code ?
Dans ce cas, nous devons partager le namespace du container que nous voulons debugger dans la commande afin que notre container de debug puisse cibler le processus à debugger.
Nous pouvons utiliser la commande suivante :
docker run --cap-add=SYS_PTRACE --pid=container:<container_name> -it python:3.11 /bin/bash
Une fois a l’intérieur du container, il suffit d’installer py-spy et de lancer nos commandes.
py-spy top --pid 1
Bingo ! Nous voyons l’activité de notre autre container !
Choses à noter :
Limitation de Version Python : Actuellement, il est impossible de debug des programmes Python avec une version supérieure à 3.11 en utilisant
py-spy
, car il ne supporte pas encore ces versions.Fonctionnalités Étendues de py-spy : Les capacités de
py-spy
vont bien au-delà de ce qui a été présenté ici. L'outil offre une multitude d'options pour le debug, notamment la prise en charge du multiprocessing, la possibilité de trier les instructions qui retiennent le Global Interpreter Lock (GIL), ainsi que l'observation des processus IDLE.Interprétation des Résultats : en plus de flamegraph,
py-spy
propose également d'autres visualisations et outils d'analyse.
Si vous appréciez la newsletter StuffAndCode, vous pouvez vous inscrire afin de ne manquer aucun article !
🐍 Et recevez en PLUS, un article exclusif sur comment profiler n’importe quel type de code Python.