
Gevent ist eine Python-Netzwerkbibliothek, die libev und libuv für ihre Ereignisschleife sowie Greenlet für asynchrone Aufgaben nutzt und damit wesentliche Abstraktionen für die Serverentwicklung bietet.
Früher setzte „Eve Online“ für seine Backend-Server „Stackless Python“ ein und nutzte dabei Tasklets oder Mikrothreads. Diese Tasklets ermöglichten es, Tausende von Anfragen parallel innerhalb eines einzigen Threads auszuführen, wodurch die mit Threads verbundenen Probleme hinsichtlich performance und Komplexität vermieden wurden. Stackless Python entwickelte sich später zu Eventlet weiter und inspirierte Gevent – eine asynchrone Bibliothek, die sich ideal für die Entwicklung leistungsstarker Netzwerk-Anwendungen eignet und dabei von der Lesbarkeit und der schnellen Entwicklungsgeschwindigkeit von Python profitiert. Diese Faktoren haben maßgeblich dazu beigetragen, dass sich Upsun Cloud bei einigen internen Diensten wie unserer Project-API für Gevent entschieden hat.
Dieser Artikel setzt voraus, dass du, unser Leser, mit den Kernkonzepten von Python Gevent wie kooperativem Multitasking, Event-Loops, Greenlets und paralleler Ablaufplanung vertraut bist. Einige gute Artikel zu diesen Themen findest du hier und hier. So können wir uns auf Best Practices und häufige Fallstricke bei Gevent konzentrieren, die oft auch auf andere asynchrone Bibliotheken zutreffen.
In Gevent ist Monkey-Patching eine optionale Technik, bei der die blockierenden Aufrufe in der Standardbibliothek durch kooperative Alternativen ersetzt werden; sie wird in der Praxis häufig eingesetzt. Wenn du dich gegen Monkey-Patching entscheidest, musst du die Greenlets selbst verwalten – was je nach Anwendungsfall durchaus wünschenswert sein kann.
Gevent entstand zu einer Zeit, als Python asynchrone Programmierung noch nicht nativ unterstützte – es gab keine Schlüsselwörter wie `async` oder `await`. Herkömmliche Webserver-Anwendungen wurden entweder nach dem Pre-Fork-Modell aufgebaut oder nutzten mehrere Threads für die Parallelität. Gevent hatte zum Ziel, bestehenden Multithread-Code parallel zu machen, ohne auf tatsächliche Betriebssystem-Threads zurückzugreifen. Manchmal war dies sogar möglich, ohne auch nur eine einzige Zeile Code in deiner Anwendung zu ändern.
Wenn beispielsweise in einer Multithread-Umgebung eine blockierende Funktion wie `socket.recv` aufgerufen wird, übernimmt der Kernel die Planung der Threads. Im Gegensatz dazu setzt Gevent auf leichtgewichtige Threads, sogenannte „Greenlets“, die parallel innerhalb eines einzigen Betriebssystem-Threads laufen. Um mehrere blockierende Funktionen innerhalb dieses einzigen Threads zu verarbeiten, führt Gevent Monkey-Patches an der Python-Standardbibliothek durch. Dabei werden die blockierenden Funktionen der Bibliothek durch Versionen ersetzt, die die Ereignisschleife nutzen, um asynchron auf den Abschluss von Operationen zu warten.
Damit das „Monkey Patching“ effektiv funktioniert, müssen bestimmte Richtlinien strikt befolgt werden:
Diese Richtlinien sind nicht nur Best Practices; sie stammen direkt aus der offiziellen Dokumentation zum Monkey-Patching von Gevent. Die Nichtbeachtung dieser Regeln könnte aufgrund von Konflikten zwischen gepatchtem und ungepatchtem Code zu unvorhersehbaren, schwer zu diagnostizierenden Fehlern führen.
Selbst wenn du das Monkey-Patching in Gevent sorgfältig implementiert hast, kann ein einziges Greenlet immer noch den gesamten Prozess blockieren. Das gilt insbesondere für Datei-Lese- und -Schreibvorgänge. Trotz der Vorteile des Monkey-Patchings sind Datei-I/O-Vorgänge auf einigen Betriebssystemen nach wie vor synchron, was bedeutet, dass sie zu einem Engpass werden können.
Es gibt jedoch eine Strategie, um diese Einschränkung zu umgehen. Python Gevent bietet die Möglichkeit, parallele Datei-I/O-Operationen mithilfe eines Thread-Pools durchzuführen. Dieser Ansatz ermöglicht es, die Dateioperationen so abzuwickeln, dass der Rest der Anwendung nicht blockiert wird. Während Monkey-Patching auch viele Probleme im Zusammenhang mit asynchroner Programmierung löst, ist es unerlässlich, sich seiner Einschränkungen bewusst zu sein und zu wissen, wie man sie umgeht, um eine wirklich nicht blockierende Anwendung zu gewährleisten.
Selbst mit den Funktionen von Gevent ist es wichtig zu beachten, dass es Bibliotheken von Drittanbietern nicht asynchron machen kann, wenn diese für blockierende Aufrufe nicht die Python-Standardbibliothek verwenden. Wenn du eine externe Bibliothek nutzt, die eigene blockierende Aufrufe durchführt, wirst du das Problem möglicherweise erst entdecken, wenn sich deine Anwendung bereits in der Produktivumgebung befindet.
Hier können Performance-Tests oder Lasttests hilfreich sein. Wenn ein Greenlet bei I/O blockiert wird, äußert sich das Problem in der Regel durch eine suboptimale CPU-Auslastung bei gleichzeitig stagnierendem Durchsatz. Unter normalen Umständen solltest du Durchsatzgrenzen erst dann erreichen, wenn die CPU-Auslastung bei 100 % oder darüber liegt. Wenn du also bei Lasttests eine niedrige CPU-Auslastung und keinen Anstieg des Durchsatzes beobachtest, ist das wahrscheinlich ein Anzeichen für ein blockierendes Greenlet.
Um das zu beheben, musst du die problematische Bibliothek eines Drittanbieters und deren spezifische blockierende Funktion identifizieren. Mögliche Lösungen könnten der Wechsel zu einer alternativen Bibliothek oder die Anpassung der aktuellen Bibliothek sein, wobei beides in der Regel nicht ganz einfach ist.
Das Ausführen von CPU-intensivem Code in einem einzelnen Python-Greenlet kann die Ausführung anderer Greenlets blockieren, was zu einem sogenannten „Greenlet-Starvation“ führt. Das Debuggen dieses Problems ist komplex, da du die Trends beim Kontextwechsel beobachten musst, um festzustellen, ob bestimmte Greenlets die CPU-Zeit monopolisieren.
Um das zu beheben, hast du mehrere Möglichkeiten:
Es ist wichtig zu beachten, dass Gevent für CPU-intensive Aufgaben nicht ideal ist. Diese Einschränkung gilt nicht nur für Gevent; auch andere asynchrone Bibliotheken wie Asyncio und Node.js stehen vor ähnlichen Herausforderungen.
Thread-Sicherheit ist ein bekanntes Konzept in der Welt der Multithreading-Anwendungen. Dabei geht es darum, gemeinsam genutzte Ressourcen vor dem gleichzeitigen Zugriff durch mehrere Threads zu schützen. Während herkömmliche Threads jederzeit einen Kontextwechsel vornehmen können, was eine manuelle Konfliktlösung (mithilfe von Mutexen) erfordert, könnte man annehmen, dass asynchrone Bibliotheken wie Python Gevent solche Probleme automatisch lösen würden. Das ist jedoch nicht der Fall.
Auch wenn asynchrone Bibliotheken wie Gevent nicht denselben Komplexitätsgrad wie herkömmliche präemptive Threads aufweisen, beinhalten sie dennoch gemeinsam genutzte Ressourcen, auf die verschiedene Greenlets zugreifen. Das kann zu subtilen, manchmal schwer zu erkennenden Problemen führen.
Betrachte das folgende Programmieren:
def withdraw(self, amount):
if self.balance >= amount:
withdraw_from_db(amount)
self.balance -= amountAngenommen, das obige Programm wird von mehreren Greenlets aufgerufen und `withdraw_from_db()` ist ein Aufruf, der die Ausführung unterbricht. Da `self.balance` eine gemeinsam genutzte Ressource ist, ist es sehr wahrscheinlich, dass `withdraw` mehrfach ausgeführt wird, selbst wenn das Guthaben nicht ausreicht. Das Problem ist, dass das Guthaben gelesen wird und dabei ein Kontextwechsel stattfinden kann, sodass der zweite Wert von `self.balance`, den wir lesen, möglicherweise nicht mehr mit dem ersten übereinstimmt.
Gevent bietet native Unterstützung für Sperren. Ich würde dir jedoch empfehlen, den Zugriff auf deine gemeinsam genutzten Ressourcen so zu gestalten, dass du dieses Problem so weit wie möglich minimierst, denn immer wenn du eine Sperre verwendest, blockierst du die Ausführung anderer Greenlets – was genau das Gegenteil von Parallelität ist. Außerdem ist es sehr wahrscheinlich, dass du die Sperre bei einem blockierenden Aufruf einsetzt, was jedoch eine Katastrophe auslösen könnte. Vielleicht kannst du die Abhebungslogik beispielsweise mithilfe einer Datenbanktransaktion implementieren und „self.balance“ aus dem Programmieren entfernen. Es gibt sicher auch andere Wege, dieses Problem zu umgehen – man muss es nur sorgfältig durchdenken.
Wie jedes Framework hat auch Gevent seine Vor- und Nachteile. Es bietet zwar den Vorteil der asynchronen Programmierung mit geringerer Komplexität als herkömmliches Multithreading, ist aber kein Allheilmittel für alle Herausforderungen im Bereich der Parallelität. Es ist unerlässlich, diese Einschränkungen und möglichen Fallstricke im Auge zu behalten, um Gevent in euren Anwendungen möglichst effektiv zu nutzen. Teilt uns in unserer Community mit, welche Fallstricke ihr bei Gevent erlebt habt.