Gevent ist eine Python-Netzwerkbibliothek, die libev und libuv für ihre Ereignisschleife und greenlet für asynchrone Aufgaben verwendet und wesentliche Abstraktionen für die Serverentwicklung bietet.
Historisch gesehen, Eve Online angenommen Stackless Python für seine Backend-Servern, unter Verwendung von Tasklets oder Microthreads. Diese Tasklets erlaubten es, Tausende von Anfragen parallel in einem einzigen Thread auszuführen und so die mit Threads verbundenen Leistungs- und Komplexitätsprobleme zu vermeiden. Stackless Python entwickelte sich später zu Eventlet und inspirierte Gevent, eine asynchrone Bibliothek, die sich ideal für die Entwicklung von Hochleistungs-Netzwerkanwendungen eignet und von der Lesbarkeit und schnellen Entwicklung von Python profitiert. Diese Faktoren haben wesentlich dazu beigetragen, dass Upsun Gevent für einige interne Dienste wie unsere Projekt-API gewählt hat.
Dieser Artikel geht davon aus, dass Sie, unser Leser, mit den Kernkonzepten von Python Gevent wie kooperatives Multitasking, Ereignisschleifen, Greenlets und gleichzeitiges Scheduling vertraut sind. Sie können einige gute Artikel zu diesen Themen hier und hier finden. Dies erlaubt uns, uns auf bewährte Praktiken und häufige Fallstricke in Gevent zu konzentrieren, die oft auch für andere asynchrone Bibliotheken gelten.
In Gevent ist Monkey-Patching eine optionale Technik, die die blockierenden Aufrufe der Standardbibliothek durch kooperative Alternativen ersetzt und in der Praxis weit verbreitet ist. Wenn Sie sich dafür entscheiden, Monkey-Patching nicht zu verwenden, sind Sie selbst für die Verwaltung von Greenlets verantwortlich, was je nach Anwendungsfall wünschenswert sein kann.
Gevent wurde zu einer Zeit entwickelt, als Python noch keine asynchrone Programmierung unterstützte - es gab keine async- oder await-Schlüsselwörter. Traditionelle Webserver-Anwendungen wurden entweder mit einem Pre-Fork-Modell erstellt oder waren auf mehrere Threads für die Gleichzeitigkeit angewiesen. Das Ziel von Gevent war es, vorhandenen Multithreading-Code konkurrierend zu machen, ohne auf Threads des Betriebssystems angewiesen zu sein. Manchmal konnte dies sogar ohne Änderung einer einzigen Codezeile in Ihrer Anwendung erreicht werden.
Wenn beispielsweise in einer Multithreading-Umgebung eine blockierende Funktion wie `socket.recv` aufgerufen wird, kümmert sich der Kernel um das Scheduling der Threads. Im Gegensatz dazu verwendet Gevent leichtgewichtige Threads, so genannte Greenlets, die gleichzeitig innerhalb eines einzigen Betriebssystem-Threads laufen. Um mehrere blockierende Funktionen innerhalb dieses einen Threads zu handhaben, patcht Gevent monkey die Python-Standardbibliothek. Dabei werden die blockierenden Funktionen der Bibliothek durch Versionen ersetzt, die die Ereignisschleife verwenden, um asynchron auf den Abschluss von Operationen zu warten.
Damit das Monkey-Patching effektiv funktioniert, müssen bestimmte Richtlinien strikt eingehalten werden:
Bei diesen Richtlinien handelt es sich nicht nur um Best Practices, sondern sie stammen direkt aus der offiziellen Dokumentation für das Monkey-Patching von Gevent. Die Nichtbeachtung dieser Regeln kann zu unvorhersehbaren, schwer zu diagnostizierenden Fehlern aufgrund von Konflikten zwischen gepatchtem und ungepatchtem Code führen.
Selbst wenn Sie das Monkey-Patching in Gevent sorgfältig implementiert haben, kann ein einziges Greenlet den gesamten Prozess blockieren. Dies gilt insbesondere für Lese- und Schreibvorgänge von Dateien. Trotz der Vorteile von Monkey Patching sind Datei-I/O-Operationen auf einigen Betriebssystemen immer noch 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, gleichzeitige Datei-E/A-Operationen unter Verwendung eines Thread-Pools durchzuführen. Dieser Ansatz ermöglicht es, die Dateioperationen so zu behandeln, dass der Rest der Anwendung nicht blockiert wird. Während Monkey-Patching also viele Probleme im Zusammenhang mit asynchroner Programmierung löst, ist es wichtig, sich seiner Grenzen bewusst zu sein und zu wissen, wie man sie umgeht, um eine wirklich blockierungsfreie Anwendung zu gewährleisten.
Trotz der Fähigkeiten von Gevent ist es wichtig zu wissen, dass es Bibliotheken von Drittanbietern nicht asynchron machen kann, wenn diese nicht die Standardbibliothek von Python für blockierende Aufrufe verwenden. Wenn Sie eine externe Bibliothek verwenden, die ihre eigenen blockierenden Aufrufe macht, entdecken Sie das Problem möglicherweise erst, wenn sich Ihre Anwendung in einer Produktionsumgebung befindet.
An dieser Stelle können Leistungsregressionstests oder Lasttests hilfreich sein. Wenn ein Greenlet bei der E/A blockiert ist, äußert sich das Problem in der Regel durch eine suboptimale CPU-Auslastung bei stagnierendem Durchsatz. Unter normalen Umständen sollten Sie nur dann an Durchsatzgrenzen stoßen, wenn die CPU-Auslastung bei oder über 100 % liegt. Wenn Sie also bei Lasttests eine niedrige CPU-Auslastung und keinen Anstieg des Durchsatzes feststellen, ist dies wahrscheinlich ein Zeichen für ein blockierendes Greenlet.
Um dies zu beheben, müssen Sie die problematische Bibliothek eines Drittanbieters und ihre spezifische blockierende Funktion identifizieren. Die Lösung kann darin bestehen, zu einer alternativen Bibliothek zu wechseln oder die aktuelle Bibliothek zu modifizieren, obwohl beides in der Regel nicht einfach ist.
Die Ausführung von CPU-intensivem Code in einem einzelnen Python-Greenlet kann die Ausführung anderer Greenlets blockieren, was zu einer so genannten Greenlet-Starvation führt. Die Fehlersuche in diesem Fall ist komplex, da man die Trends beim Kontextwechsel beobachten muss, um festzustellen, ob bestimmte Greenlets die CPU-Zeit für sich beanspruchen.
Um dieses Problem zu lösen, haben Sie mehrere Möglichkeiten:
Es ist wichtig zu beachten, dass Gevent nicht ideal für CPU-intensive Aufgaben 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 Multithreading-Welt. Es beinhaltet den Schutz gemeinsam genutzter Ressourcen vor dem gleichzeitigen Zugriff durch mehrere Threads. Während herkömmliche Threads jederzeit den Kontext wechseln können, was eine manuelle Konfliktlösung (unter Verwendung 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 die gleiche Komplexität wie herkömmliche präemptive Threads aufweisen, beinhalten sie dennoch gemeinsam genutzte Ressourcen, auf die verschiedene Greenlets zugreifen. Dies kann zu subtilen, manchmal schwer zu erkennenden Problemen führen.
Denken Sie an den folgenden Code:
def withdraw(self, amount): if self.balance >= amount: withdraw_from_db(amount) self.balance -= amount
Angenommen, der obige Code wird von mehreren Greenlets aufgerufen und withdraw_from_db() ist ein Aufruf, der zur Ausführung führt. Da self.balance eine gemeinsam genutzte Ressource ist, ist es gut möglich, dass die Abhebung mehrfach erfolgt, auch wenn der Saldo nicht ausreicht. Das Problem ist, dass der Kontostand gelesen wird und ein Kontextwechsel stattfinden kann, so dass der zweite self.balance, den wir lesen, möglicherweise nicht derselbe ist wie der erste.
Es gibt eine native Lock-Unterstützung in Gevent. Aber ich würde vorschlagen, dass Sie die Art und Weise, wie Sie auf Ihre gemeinsamen Ressourcen zugreifen, so gestalten, dass Sie dies so weit wie möglich minimieren, denn jedes Mal, wenn Sie eine Sperre verwenden, blockiert dies die Ausführung anderer Greenlets, was genau das Gegenteil von Gleichzeitigkeit ist. Außerdem ist es sehr wahrscheinlich, dass Sie die Sperre verwenden, wenn Sie einen blockierenden Aufruf haben, aber das könnte ein Rezept für eine Katastrophe sein. Vielleicht können Sie zum Beispiel die Abhebungslogik mithilfe einer Datenbanktransaktion implementieren und self.balance aus dem Code entfernen. Es könnte auch andere Möglichkeiten geben, dieses Problem zu umgehen, es muss nur sorgfältig durchdacht werden.
Wie jedes Framework hat auch Gevent seine Höhen und Tiefen. Es bietet zwar den Vorteil der asynchronen Programmierung mit weniger Komplexität als herkömmliches Multithreading, ist aber kein Allheilmittel für alle Gleichzeitigkeitsprobleme. Es ist wichtig, diese Einschränkungen und möglichen Fallstricke zu beachten, um Gevent in Ihren Anwendungen so effektiv wie möglich zu nutzen. Teilen Sie uns auf Discord mit, welche Fallstricke Sie bei Gevent erlebt haben.