• Docs
  • Talk to an expert
Blog
Blog
BlogProduitÉtudes de casNouvellesPerspectives
Blog

Python Gevent en pratique : les pièges courants à éviter

Pythonperformance
22 février 2024
Sümer Cip
Sümer Cip
Ingénieur principal en informatique dématérialisée
Partager
Cette page a été rédigée en anglais par nos experts, puis traduite par une IA pour vous y donner accès rapidement! Pour la version originale, c’est par ici.

Gevent est une bibliothèque réseau Python qui utilise libev et libuv pour sa boucle d'événements et greenlet pour les tâches asynchrones, offrant ainsi des abstractions essentielles pour le développement de serveurs.

À l’origine, Eve Online avait adopté Stackless Python pour ses serveurs backend, en utilisant des « tasklets » ou micro-threads. Ces tasklets permettaient d’exécuter des milliers de requêtes en parallèle au sein d’un seul thread, ce qui évitait les problèmes de performances et de complexité liés aux threads. Stackless Python a ensuite évolué vers Eventlet, inspirant Gevent — une bibliothèque asynchrone idéale pour développer des applications réseau hautes performances, qui tire parti de la lisibilité et de la rapidité de développement de Python. Ces facteurs ont largement contribué au choix de Gevent par Upsun Cloud pour certains services internes, comme notre API Project.

Cet article part du principe que tu, cher lecteur, connais les concepts clés de Gevent pour Python, comme le multitâche coopératif, les boucles d’événements, les greenlets et la planification concurrente. Tu trouveras de bons articles sur ces sujets ici et ici. Ça nous permet de nous concentrer sur les bonnes pratiques et les pièges courants de Gevent, qui s’appliquent souvent à d’autres bibliothèques asynchrones.

Pièges courants

Le « monkey patching »

Dans Gevent, le « monkey patching » est une technique optionnelle qui remplace les appels bloquants de la bibliothèque standard par des alternatives coopératives ; elle est largement utilisée dans la pratique. Si tu choisis de ne pas recourir au « monkey patching », tu devras gérer toi-même les greenlets, ce qui peut s’avérer souhaitable selon ton cas d’utilisation.

Gevent a vu le jour à une époque où Python ne prenait pas en charge nativement la programmation asynchrone — il n’y avait ni mot-clé « async » ni « await ». Les applications de serveurs web traditionnelles étaient soit construites selon un modèle « pre-fork », soit reposaient sur plusieurs threads pour la concurrence. Gevent visait à rendre le code multithread existant concurrent sans s’appuyer sur les threads réels du système d’exploitation (OS). Parfois, cela pouvait même se faire sans modifier une seule ligne de code dans ton application.

Par exemple, dans un environnement multithread, lorsqu’une fonction bloquante comme `socket.recv` est appelée, c’est le noyau qui gère l’ordonnancement des threads. À l’inverse, Gevent utilise des threads légers, appelés « greenlets », qui s’exécutent en parallèle au sein d’un seul thread du système d’exploitation. Pour gérer plusieurs fonctions bloquantes au sein de ce thread unique, Gevent applique des « monkey patches » à la bibliothèque standard de Python. Ça consiste à remplacer les fonctions bloquantes de la bibliothèque par des versions qui utilisent la boucle d’événements pour attendre de manière asynchrone que les opérations se terminent.

Pour que le « monkey patching » fonctionne efficacement, certaines règles doivent être strictement respectées :

  • Le « monkey patching » doit être effectué le plus tôt possible dans le code. Si tu attends trop longtemps, tu risques qu’une autre partie de ton code importe un module utilisant les fonctions d’origine, non modifiées, ce qui entraînerait des conflits.
  • Le « monkey patching » doit être effectué sur le thread principal. Ça garantit que les modifications s’appliquent à tous les « greenlets ».
  • Le processus de patching doit avoir lieu alors que l’application fonctionne encore en monothread. C’est crucial, car le « monkey patching » de Gevent modifie également la bibliothèque de gestion des threads. Si tu as déjà lancé plusieurs threads à l’aide du module de gestion des threads non patché, des conflits pourraient survenir.

Ces consignes ne sont pas seulement des bonnes pratiques ; elles proviennent directement de la documentation officielle sur le « monkey patching » de Gevent. Ne pas respecter ces règles pourrait entraîner des erreurs imprévisibles et difficiles à diagnostiquer, dues à des conflits entre le code modifié et le code non modifié.

Appels bloquants

Même si tu as soigneusement mis en œuvre le « monkey patching » dans Gevent, un seul « greenlet » peut encore bloquer tout le processus. C’est particulièrement vrai pour les opérations de lecture/écriture de fichiers. Malgré les avantages du « monkey patching », les opérations d’E/S sur les fichiers restent synchrones sur certains systèmes d’exploitation, ce qui signifie qu’elles peuvent devenir un goulot d’étranglement.

Il existe toutefois une stratégie pour contourner cette limitation. Python Gevent offre la possibilité d’effectuer des E/S de fichiers en parallèle en utilisant un pool de threads. Cette approche permet de gérer les opérations sur les fichiers sans bloquer le reste de l’application. Ainsi, même si le « monkey patching » résout de nombreux problèmes liés à la programmation asynchrone, il est essentiel d’être conscient de ses limites et de savoir comment les contourner pour garantir une application véritablement non bloquante.

Bibliothèques tierces

Même avec les capacités de Gevent, il est crucial de noter qu’il ne peut pas rendre asynchrones les bibliothèques tierces si celles-ci n’utilisent pas la bibliothèque standard de Python pour les appels bloquants. Si tu utilises une bibliothèque externe qui effectue ses propres appels bloquants, tu risques de ne pas découvrir le problème avant que ton application ne soit en environnement de production.

C’est là que les tests de régression des performances ou les tests de charge peuvent s’avérer utiles. Lorsqu’un greenlet est bloqué sur une opération d’E/S, le problème se manifeste généralement par une utilisation sous-optimale du processeur et un débit stagnant. Dans des circonstances normales, tu ne devrais atteindre les limites de débit que lorsque l’utilisation du processeur est égale ou supérieure à 100 %. Par conséquent, si tu observes une faible utilisation du processeur et aucune augmentation du débit lors des tests de charge, c’est probablement le signe d’un greenlet bloquant.

Pour résoudre ce problème, tu devras identifier la bibliothèque tierce problématique et la fonction spécifique qui provoque le blocage. Les solutions peuvent consister à passer à une autre bibliothèque ou à modifier celle que tu utilises actuellement, même si aucune de ces deux options n’est généralement simple à mettre en œuvre.

Code gourmand en ressources CPU

L'exécution de code gourmand en ressources CPU dans un seul greenlet Python peut empêcher d'autres greenlets de s'exécuter, ce qui entraîne ce qu'on appelle la « famine de greenlets ». Le débogage de ce problème est complexe, car tu dois observer les tendances de changement de contexte pour déterminer si certains greenlets monopolisent le temps CPU.

Pour y remédier, plusieurs options s’offrent à toi :

  1. Délester les tâches gourmandes en CPU vers un processus ou un thread distinct, idéalement en utilisant un pool. Ça permet de séparer les tâches liées aux E/S de celles liées au CPU, ce qui les empêche de se gêner mutuellement.
  2. Si la séparation des charges de travail n’est pas possible — peut-être parce que tu dois effectuer une tâche gourmande en CPU et renvoyer immédiatement le résultat —, cède explicitement le greenlet pour permettre à d’autres greenlets de s’exécuter.

Il est important de noter que Gevent n’est pas idéal pour les tâches gourmandes en CPU. Cette limitation n’est pas propre à Gevent ; d’autres bibliothèques asynchrones comme Asyncio et Node.js sont également confrontées à des défis similaires.

Sécurité des greenlets

La sécurité des threads est un concept bien connu dans l’univers multithread. Elle consiste à protéger les ressources partagées contre les accès simultanés de plusieurs threads. Alors que les threads traditionnels peuvent changer de contexte à tout moment, ce qui nécessite une résolution manuelle des conflits (à l’aide de mutex), on pourrait supposer que des bibliothèques asynchrones comme Gevent en Python résoudraient automatiquement ce genre de problèmes. Mais ce n’est pas le cas.

Même si les bibliothèques asynchrones comme Gevent ne présentent pas le même niveau de complexité que les threads préemptifs traditionnels, elles impliquent tout de même des ressources partagées auxquelles accèdent différents greenlets. Ça peut entraîner des problèmes subtils, parfois difficiles à détecter.

Pense au code suivant :

def withdraw(self, amount):
    if self.balance >= amount:
        withdraw_from_db(amount)
        self.balance -= amount

Supposons que le code ci-dessus soit appelé par plusieurs greenlets et que `withdraw_from_db()` soit un appel qui cède l’exécution. Comme `self.balance` est une ressource partagée, il est fort probable que le retrait puisse se produire plusieurs fois, même si le solde est insuffisant. Le problème, c’est que le solde est lu et qu’un changement de contexte peut se produire ; la deuxième valeur de `self.balance` que tu lis pourrait ne pas être la même que celle que tu as lue en premier.

Gevent prend en charge les verrous de manière native. Mais je te conseille de concevoir l’accès à tes ressources partagées de manière à minimiser autant que possible ce risque, car chaque fois que tu utilises un verrou, cela empêche les autres greenlets de s’exécuter, ce qui va à l’encontre même de la concurrence. De plus, il y a de fortes chances que tu utilises un verrou quand tu as un appel bloquant, mais ça pourrait être la recette du désastre. Par exemple, tu pourrais peut-être implémenter la logique de retrait à l’aide d’une transaction de base de données et supprimer « self.balance » du code. Il existe sans doute d’autres moyens de contourner ce problème, il suffit d’y réfléchir attentivement.

Conclusion

Comme tout framework, Gevent a ses avantages et ses inconvénients. S’il offre l’avantage d’une programmation asynchrone moins complexe que le multithreading traditionnel, ce n’est pas une solution miracle pour tous les défis liés à la concurrence. Il est essentiel d’être vigilant face à ces limites et aux pièges potentiels pour tirer le meilleur parti de Gevent dans tes applications. Raconte-nous tes expériences avec Gevent sur notre communauté.

Restez informé

Abonnez-vous à notre newsletter mensuelle pour les dernières mises à jour et nouvelles.

Déployez en toute liberté.
Essayez Upsun gratuitement.

Développez avec DispatchDéployez avec Cloud