Tous droits réservés

Le jour où j'ai cassé Python

Chasse aux deadlocks jusqu'en enfer

Petit rapport d’une expérience professionnelle rageante.

Le titre est un peu mensonger, quoique. L’un des projets auxquels je participe dans ma vie professionnelle consiste en la création de super-calculateurs destinés au traitement massif de données pour divers clients. Bien évidemment, vu l’ampleur de la tâche, il y a beaucoup d’équipes différentes sur des parties également très différentes. En l’occurrence, je ne touche pas du tout à la conception de la partie hardware pure.

Il se trouve que je travaille sur un logiciel installé sur l’ensemble des noeuds composants les différents racks de la bestiole finale et que ce logiciel, en Python, effectue beaucoup d’actions et de calculs en parallèle. Comme beaucoup de logiciels sur le cluster, celui-ci requiert une parfaite synchronisation des noeuds, et ce comme condition critique (des noeuds non-synchronisés et toute la machine s’arrête pour des raisons de cohérence de données par exemple).

Pour garantir cela, NTP est installé sur chaque noeud. Cependant, j’ai développé une petite surcouche pour surveiller manuellement NTP et l’heure afin de détecter des changements soudains et abruptes pour lesquels NTP serait trop lent pour réagir. C’est le cas par exemple si NTP était stable pendant un long moment et recontactera le serveur qu’une fois toute les X secondes où X peut valoir des milliers de secondes ! Imaginez si je force l’heure sur un noeud (date -s), change celui-ci de plusieurs heures et doit attendre plusieurs minutes pour voir une resynchronisation ! Inacceptable et la machine crashera avant quelque part.

Bref, mon petit module Python semble fonctionner individuellement, mais une fois intégrée à la machine, lorsque je change l’heure dans le futur de quelques heures, bien que celle-ci est ramenée correctement dans le passé, la machine semble figée. Plus rien ne marche. De surcroit aucun log n’est disponible à aucun niveau que ce soit, et aucun problème ne semble réellement survenir à part un mystérieux arrêt total du fonctionnement.

Erreur de conception ? Dans l’OS ? Dans la stack software que l’on rajoute ? Panique à bord. Et bien il se trouve que le problème provient de… Python. En effet, la stack logicielle qui gère la machine est, comme on peut s’en douter, fortement asynchrone, distribuée et parallèle (ce qui était déjà en soit la galère pour tracer le problème) et donc beaucoup de procédures sont appelées avec un timeout et gérées par un système maison qui repose sur des queues en Python.

Manque de bol, la méthode get avec timeout et appel bloquant bloque sur la ligne suivante car dans le module threading, la classe Condition n’attend pas durant $\delta$ la durée du timeout mais calcul le temps restant à partir du… temps système. Ainsi, lorsque je change manuellement le temps de $t$ à $t+T$ via date -s par exemple, il y a un laps de temps durant lequel le logiciel appelle get avec un timeout $\delta$. Le processus va bloquer, mais pendant ce temps, j’ai détecté le changement de temps et ramené la machine à $t+w$. Comme Python se base sur le temps système, le temps à atteindre devient $t+T-t + \delta= T + \delta$. Autrement dit, le timeout considéré n’est plus le même: tout est bloqué jusqu’à $T+T+\delta$ (et évidemment comme j’ai changé par 3 heures dans le futur, je n’ai pas eu la patience d’attendre et de me rendre compte que tout allait bien après cette durée :D).

Vous pouvez faire le test chez vous en ouvrant deux terminaux. Dans le premier, démarrez Python en CLI:

1
2
3
4
import Queue #queue pour Python 3

qe = Queue.Queue()
qe.get(block=True, timeout=15)

Et pendant les 15 prochaines secondes, dans le second terminal date -s <nouvelle_date_dans_le_passé> (il faudra les droits pour cela). En temps normal, vous devriez avoir une exception Empty indiquant que la queue est vide, ce qui n’est plus le cas après le changement de date.

Alors qu’ai-je fait pour résoudre le problème ? Patché Queue (même si en fait c’est threading qui pose problème) en créant une classe perso qui hérite de Queue et surcharge getavec un algorithme qui prend en compte le temps écoulé plutôt que le temps restant (avec quelques subtilités).

Et vous ? Quelles ont été les chasses aux bugs et les choses les plus étranges que vous ayez-vu (en Python ou autre) ?



10 commentaires

Ah oui, c’est pas mal !

Je me souviens d’un problème similaire dans Minecraft, il y a quelques années. Il faut savoir que dans le jeu, les blocs ont une certaine solidité, et que celle-ci détermine le temps nécessaire pour les casser en frappant dessus. Il suffisait de reculer un peu le temps système pour faire foirer cette protection, et pouvoir casser instantanément tous types de blocs.

Et comme cette vérification du temps écoulé ne se faisait que côté client, c’est valable aussi en multijoueur…

Ça pourrait peut être mériter une remontée upstream ?

Kje

J’y ai pensé, mais comme c’est tout frais que c’était en fin d’itération ET une itération avant GA date, ce n’était pas vraiment la priorité. D’autant que la solution adoptée n’est pas forcément la meilleure. En testant le temps écoulé, tu es obligé d’avoir une boucle avec un sleep. Pour éviter de surcharger le CPU j’utilise un schedule exponentiel (je multiplie par deux le temps d’attente à chaque vérification de boucle à partir d’une valeur assez faible pour mes besoins). Cela colle à mes besoins mais c’est loin d’être générique:

  • Déjà on est moins précis et réactif
  • Surcharge CPU possible

Du coup je voulais réfléchir à une solution plus pratique avant de faire remonter, voire résoudre le problème dans threading directement, mais il faudrait investiguer les conséquences…

Et comme cette vérification du temps écoulé ne se faisait que côté client, c’est valable aussi en multijoueur…

entwanne

Tu sais comment ça a été résolu ?

Peut-etre parce qu’il n’y a pas cette contrainte de synchronization et un acces possible depuis l’exterieur ? Le scenario decrit est extremement peu probable parce que NTP detectera des petites anomalies regulierement. Mais j’ai voulu traiter le probleme parce qu’on peut imaginer des attaques basees sur un changement de l’horologe interne (normalement il faut des droits mais on ne peut pas exclure un moyen detourne de changer l’horologe sans l’elevation directe des privileges).

Un moyen de concilier les deux ce serait d’utiliser le temps absolu, et de passer sur le temps ecoulee quand on detecte un changement dans l’heure (typiquement le remaining devient plus grand que le precedent et donc on peut le corriger. D’ailleurs il y a peut etre meme pas besoin d’utiliser le temps d’attente en procedant comme cela.

Avec un temps absolu, tu peux potentiellement avoir une grosse différence entre ce que tu demandait et ce que tu voulais. Il suffit que l’ordonnanceur qui gère ton programme/le matériel(interruption) t’enlève la main juste après que tu ais calculé le temps d’attente (donc dans ton cas où le timeout est fixe, juste avant que tu ne déclenches l’attente) et te le rend plus tard. C’est notamment nécessaire si tu as besoin de faire du realtime.

La solution ne serait pas plutot d’utiliser CLOCK_MONOTONIC plutot que CLOCK_REALTIME dans le lock sous-jacent pour ce cas particulier ? Le problème ensuite étant la portabilité du code :/

+0 -0

Après ce que je pensais c’était surtout remonter le problème en ouvrant un ticket. Ils vont peut être le fermer pour de bonnes raisons mais tu n’es pas obligé d’arriver avec le patch, surtout sans savoir toutes les raisons qui ont mené à ce choix

Un nœud désynchronisé et c’est l’ensemble des nœuds qui s’arrêtent (enfin pas tout à fait mais cela pourrait), potentiellement des centaines. Redémarrer un nœud, c’est 10 à 15 minutes, c’est compliqué et cela va venir complexifier les scénarios de D&R et affaiblir la politique de HA pour pas grand chose.

Donc malheureusement, il semblerait que non, ce ne soit pas possible. Après la solution appliquée fonctionne parfaitement pour ce scénario.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte