Eine Million Anfragen pro Sekunde mit Python

Ist es möglich, mit Python eine Million Anfragen pro Sekunde zu erfüllen? Wahrscheinlich erst vor kurzem.

Viele Unternehmen migrieren von Python zu anderen Programmiersprachen, um ihre Betriebsleistung zu steigern und Serverpreise zu sparen. Dies ist jedoch nicht unbedingt erforderlich. Python kann das richtige Werkzeug für den Job sein.

Die Python-Community macht in letzter Zeit viel mit Leistung. CPython 3.6 steigerte die Gesamtleistung der Interpreter mit der neuen Wörterbuchimplementierung. CPython 3.7 wird dank der Einführung schnellerer Anrufkonventionen und Wörterbuch-Suchcaches noch schneller.

Für Aufgaben zur Eingabe von Zahlen können Sie PyPy mit seiner Just-in-Time-Code-Kompilierung verwenden. Sie können auch die Testsuite von NumPy ausführen, die jetzt die allgemeine Kompatibilität mit C-Erweiterungen verbessert hat. Später in diesem Jahr wird erwartet, dass PyPy die Python 3.5-Konformität erreicht.

All diese großartige Arbeit hat mich zu Innovationen in einem der Bereiche inspiriert, in denen Python in großem Umfang eingesetzt wird: der Entwicklung von Web- und Mikrodiensten.

Japronto betreten!

Japronto ist ein brandneues Mikro-Framework, das auf Ihre Anforderungen an Mikrodienste zugeschnitten ist. Zu den Hauptzielen gehört es, schnell , skalierbar und leicht zu sein . Dank asyncio können Sie sowohl synchron als auch asynchron programmieren . Und es ist schamlos schnell . Noch schneller als NodeJS und Go.

Errata: Wie Benutzer @heppu betont, kann der stdlib-HTTP-Server von Go 12% schneller sein als in dieser Grafik dargestellt, wenn er genauer geschrieben wird. Außerdem gibt es einen fantastischen Fasthttp- Server für Go, der in diesem speziellen Benchmark anscheinend nur 18% langsamer ist als Japronto. Genial! Einzelheiten finden Sie unter //github.com/squeaky-pl/japronto/pull/12 und //github.com/squeaky-pl/japronto/pull/14.

Wir können auch sehen, dass Meinheld WSGI-Server NodeJS und Go fast ebenbürtig ist. Trotz seines inhärent blockierenden Designs ist es im Vergleich zu den vorherigen vier, bei denen es sich um asynchrone Python-Lösungen handelt, eine hervorragende Leistung. Vertrauen Sie also niemals jemandem, der sagt, dass asynchrone Systeme immer schneller sind. Sie sind fast immer gleichzeitig, aber es steckt noch viel mehr dahinter.

Ich habe diesen Mikro-Benchmark mit einer „Hallo Welt!“ Durchgeführt. Anwendung, aber es zeigt deutlich Server-Framework-Overhead für eine Reihe von Lösungen.

Diese Ergebnisse wurden auf einer AWS c4.2xlarge-Instanz mit 8 VCPUs erzielt, die in der Region São Paulo mit standardmäßiger gemeinsamer Mandanten- und HVM-Virtualisierung sowie magnetischem Speicher gestartet wurde. Auf dem Computer wurde Ubuntu 16.04.1 LTS (Xenial Xerus) mit dem generischen x86_64-Kernel unter Linux 4.4.0–53 ausgeführt. Das Betriebssystem meldete Xeon® CPU E5–2666 v3 bei 2,90 GHz CPU. Ich habe Python 3.6 verwendet, das ich frisch aus dem Quellcode kompiliert habe.

Um fair zu sein, führten alle Teilnehmer (einschließlich Go) einen Einzelarbeiterprozess durch. Die Server wurden unter Verwendung von wrk mit 1 Thread, 100 Verbindungen und 24 gleichzeitigen (Pipeline-) Anforderungen pro Verbindung (kumulative Parallelität von 2400 Anforderungen) auf Last getestet.

HTTP-Pipelining ist hier von entscheidender Bedeutung, da es eine der Optimierungen ist, die Japronto bei der Ausführung von Anforderungen berücksichtigt.

Die meisten Server führen Anforderungen von Pipelining-Clients auf dieselbe Weise aus wie von Nicht-Pipelining-Clients. Sie versuchen nicht, es zu optimieren. (Tatsächlich werden Sanic und Meinheld auch Anfragen von Pipelining-Clients stillschweigend löschen, was eine Verletzung des HTTP 1.1-Protokolls darstellt.)

Mit einfachen Worten, Pipelining ist eine Technik, bei der der Client nicht auf die Antwort warten muss, bevor er nachfolgende Anforderungen über dieselbe TCP-Verbindung sendet. Um die Integrität der Kommunikation sicherzustellen, sendet der Server mehrere Antworten in derselben Reihenfolge zurück, in der Anforderungen empfangen werden.

Die blutigen Details von Optimierungen

Wenn viele kleine GET - Anfragen pipelined werden durch die Kunden, gibt es eine hohe Wahrscheinlichkeit , dass sie in einem TCP - Paket (dank Nagle-Algorithmus) auf der Serverseite kommen werden, dann wird gelesen zurück von einem Systemaufruf .

Das Ausführen eines Systemaufrufs und das Verschieben von Daten vom Kernel-Space in den User-Space ist eine sehr teure Operation im Vergleich zum Verschieben von Speicher innerhalb des Prozessraums. Aus diesem Grund ist es wichtig, so wenige Systemaufrufe wie nötig durchzuführen (aber nicht weniger).

Wenn Japronto Daten empfängt und parst erfolgreich mehrere Anfragen aus ihn heraus, versucht es , alle Anfragen so schnell wie möglich auszuführen, Leim Antworten in der richtigen Reihenfolge zurück, dann schreibt in wieder einem Systemaufruf . Tatsächlich kann der Kernel dank der Streu- / Sammel-E / A-Systemaufrufe, die Japronto noch nicht verwendet, beim Kleben helfen.

Beachten Sie, dass dies nicht immer möglich ist, da einige der Anforderungen zu lange dauern können und das Warten auf sie die Latenz unnötig erhöhen würde.

Seien Sie vorsichtig, wenn Sie die Heuristik optimieren, und berücksichtigen Sie die Kosten für Systemaufrufe und die erwartete Bearbeitungszeit für Anforderungen.

Neben der Verzögerung von Schreibvorgängen für Pipeline-Clients gibt es mehrere andere Techniken, die der Code verwendet.

Japronto ist fast vollständig in C geschrieben. Die Objekte Parser, Protokoll, Verbindungs-Reaper, Router, Anfrage und Antwort werden als C-Erweiterungen geschrieben.

Japronto ist bemüht, die Erstellung von Python-Gegenstücken seiner internen Strukturen zu verzögern, bis dies explizit angefordert wird. Beispielsweise wird ein Header-Wörterbuch erst erstellt, wenn es in einer Ansicht angefordert wird. Alle Token-Grenzen sind bereits zuvor markiert, aber die Normalisierung der Header-Schlüssel und die Erstellung mehrerer str-Objekte erfolgt beim erstmaligen Zugriff.

Japronto stützt sich auf die hervorragende Picohttpparser C-Bibliothek, um Statuszeilen, Header und einen HTTP-Nachrichtentext zu analysieren. Picohttpparser verwendet direkt Textverarbeitungsanweisungen, die in modernen CPUs mit SSE4.2-Erweiterungen enthalten sind (fast jede 10 Jahre alte x86_64-CPU verfügt über diese), um die Grenzen von HTTP-Token schnell anzupassen. Die E / A wird von dem super tollen Uvloop verwaltet, der selbst ein Wrapper um libuv ist. Auf der untersten Ebene ist dies eine Brücke zum Epoll-Systemaufruf, der asynchrone Benachrichtigungen zur Lese- / Schreibbereitschaft bereitstellt.

Python ist eine Sprache, in der Müll gesammelt wird. Daher muss beim Entwurf von Hochleistungssystemen vorsichtig vorgegangen werden, um den Druck auf den Garbage Collector nicht unnötig zu erhöhen. Das interne Design von Japronto versucht, Referenzzyklen zu vermeiden und so wenig Zuweisungen / Freigaben wie nötig vorzunehmen. Dazu werden einige Objekte in sogenannte Arenen vorbelegt. Es wird auch versucht, Python-Objekte für zukünftige Anforderungen wiederzuverwenden, wenn nicht mehr auf sie verwiesen wird, anstatt sie wegzuwerfen.

Alle Zuweisungen erfolgen als Vielfaches von 4 KB. Interne Strukturen sind sorgfältig angeordnet, sodass häufig zusammen verwendete Daten nahe genug im Speicher sind, wodurch die Möglichkeit von Cache-Fehlern minimiert wird.

Japronto versucht, nicht unnötig zwischen Puffern zu kopieren, und führt viele Vorgänge direkt aus. Beispielsweise wird der Pfad in Prozent dekodiert, bevor er im Router-Prozess abgeglichen wird.

Open Source-Mitwirkende, ich könnte Ihre Hilfe gebrauchen.

Ich arbeite seit 3 ​​Monaten ununterbrochen an Japronto - oft an Wochenenden sowie an normalen Arbeitstagen. Dies war nur möglich, weil ich eine Pause von meinem regulären Programmiererjob machte und meine ganze Anstrengung in dieses Projekt steckte.

Ich denke, es ist Zeit, die Früchte meiner Arbeit mit der Gemeinde zu teilen.

Derzeit implementiert Japronto einen ziemlich soliden Funktionsumfang:

  • HTTP 1.x-Implementierung mit Unterstützung für Chunk-Uploads
  • Volle Unterstützung für HTTP-Pipelining
  • Keep-Alive-Verbindungen mit konfigurierbarem Reaper
  • Unterstützung für synchrone und asynchrone Ansichten
  • Master-Multiworker-Modell basierend auf Gabelung
  • Unterstützung für das Neuladen von Code bei Änderungen
  • Einfaches Routing

Als nächstes möchte ich Websockets untersuchen und HTTP-Antworten asynchron streamen.

Es gibt viel zu tun, um zu dokumentieren und zu testen. Wenn Sie helfen möchten, kontaktieren Sie mich bitte direkt auf Twitter. Hier ist das GitHub-Projekt-Repository von Japronto.

Wenn Ihr Unternehmen nach einem Python-Entwickler sucht, der ein Leistungsfreak ist und auch DevOps ausführt, bin ich offen dafür, davon zu hören. Ich werde Positionen weltweit in Betracht ziehen.

Letzte Worte

Alle Techniken, die ich hier erwähnt habe, sind nicht wirklich spezifisch für Python. Sie könnten wahrscheinlich in anderen Sprachen wie Ruby, JavaScript oder sogar PHP eingesetzt werden. Ich wäre auch daran interessiert, solche Arbeiten zu machen, aber dies wird leider nur passieren, wenn jemand sie finanzieren kann.

Ich möchte der Python-Community für ihre kontinuierlichen Investitionen in das Performance Engineering danken. Nämlich Victor Stinner @ VictorStinner, INADA Naoki @ Methane und Yury Selivanov @ 1st1 und das gesamte PyPy-Team.

Aus Liebe zu Python.