RoadRunner: PHP ist nicht dafür gemacht, zu sterben, oder Golang eilt zur Hilfe

RoadRunner: PHP ist nicht dafür gemacht, zu sterben, oder Golang eilt zur Hilfe

Hallo, Habr! Wir bei Badoo arbeiten aktiv an der Performance von PHP, da wir ein ziemlich großes System in dieser Sprache haben und das Thema Performance – eine Frage der Kosten ist. Vor über zehn Jahren haben wir dafür PHP-FPM entwickelt, das zuerst eine Sammlung von Patches für PHP war und später in die offizielle Lieferung aufgenommen wurde.

In den letzten Jahren hat sich PHP stark weiterentwickelt: Der Garbage Collector hat sich verbessert, die Stabilität ist gestiegen – heute kann man mit PHP ohne größere Probleme Daemons und langlebige Skripte schreiben. Das hat es Spiral Scout ermöglicht, weiterzugehen: RoadRunner bereinigt im Gegensatz zu PHP-FPM den Speicher nicht zwischen den Anfragen, was einen zusätzlichen Leistungsvorteil bringt (obwohl dieser Ansatz den Entwicklungsprozess komplizierter macht). Wir experimentieren derzeit mit diesem Werkzeug, haben aber noch keine Ergebnisse, die wir teilen könnten. Um die Wartezeit etwas unterhaltsamer zu gestalten, veröffentlichen wir die Übersetzung der Ankündigung von RoadRunner von Spiral Scout.

Der Ansatz aus dem Artikel spricht uns an: Bei der Lösung unserer eigenen Aufgaben verwenden wir ebenfalls häufiger die Kombination aus PHP und Go und profitieren von den Vorteilen beider Sprachen, ohne auf die eine zugunsten der anderen verzichten zu müssen.

Viel Spaß!

In den letzten zehn Jahren haben wir Anwendungen sowohl für Unternehmen aus der Liste Fortune 500, als auch für Geschäfte mit einer Benutzerzahl von weniger als 500 entwickelt. Während dieser ganzen Zeit haben unsere Ingenieure den Backend hauptsächlich in PHP entwickelt. Vor zwei Jahren jedoch hat etwas nicht nur die Performance unserer Produkte, sondern auch deren Skalierbarkeit stark beeinflusst – wir haben Golang (Go) in unseren Technologie-Stack eingeführt.

Fast sofort haben wir festgestellt, dass Go es uns erlaubt, größere Anwendungen mit einer Leistungssteigerung von bis zu 40 Mal zu erstellen. Mit seiner Hilfe konnten wir bestehende Produkte, die in PHP geschrieben sind, erweitern und durch die Kombination der Vorteile beider Sprachen verbessern.

Wir werden erklären, wie die Kombination von Go und PHP hilft, reale Entwicklungsaufgaben zu lösen und wie sie für uns zu einem Werkzeug geworden ist, das einige Probleme im Zusammenhang mit der PHP 'Sterbens'-Modell.

Ihrer typischen PHP-Entwicklungsumgebung

Bevor wir erläutern, wie man mit Go das 'Sterbens'-Modell von PHP wiederbeleben kann, werfen wir einen Blick auf Ihre Standard-PHP-Entwicklungsumgebung.

In den meisten Fällen starten Sie die Anwendung mit einer Kombination aus dem Webserver nginx und dem PHP-FPM-Server. Der erste dient der Bereitstellung statischer Dateien und leitet spezifische Anfragen an PHP-FPM weiter, während PHP-FPM den PHP-Code ausführt. Möglicherweise verwenden Sie eine weniger beliebte Kombination aus Apache und mod_php. Auch wenn sie etwas anders funktioniert, sind die Prinzipien dieselben.

Betrachten wir, wie PHP-FPM den Code der Anwendung ausführt. Wenn eine Anfrage eingeht, initialisiert PHP-FPM einen untergeordneten PHP-Prozess und überträgt die Einzelheiten der Anfrage als Teil seines Zustands (_GET, _POST, _SERVER usw.).

Der Zustand kann während der Ausführung des PHP-Skripts nicht geändert werden, daher kann ein neuer Satz Eingabedaten nur auf eine Weise erlangt werden: durch das Leeren des Prozessspeichers und das Neuinitialisieren des Prozesses.

Dieses Ausführungsmodell bietet viele Vorteile. Sie müssen sich nicht übermäßig um den Speicherverbrauch kümmern, alle Prozesse sind vollständig isoliert, und wenn einer davon "stirbt", wird er automatisch neu erstellt, ohne die anderen Prozesse zu beeinträchtigen. Aber dieser Ansatz hat auch Nachteile, die sich beim Versuch zeigen, die Anwendung zu skalieren.

Nachteile und Ineffizienz der üblichen PHP-Umgebung

Wenn Sie professionell mit PHP entwickeln, wissen Sie, wie wichtig es ist, ein neues Projekt mit der Auswahl des Frameworks zu beginnen. Es besteht aus Bibliotheken für Dependency Injection, ORMs, Übersetzungen und Templates. Und natürlich können alle eingehenden Benutzerdaten bequem in einem Objekt zusammengefasst werden (Symfony/HttpFoundation oder PSR-7). Frameworks sind großartig!

Aber alles hat seinen Preis. In jedem Framework auf Unternehmensebene müssen für die Verarbeitung einer einfachen Benutzeranfrage oder eines Datenbankzugriffs mindestens Dutzende von Dateien geladen, zahlreiche Klassen erstellt und mehrere Konfigurationen analysiert werden. Noch schlimmer ist, dass nach jeder Aufgabe alles zurückgesetzt werden muss, und man von vorne anfangen muss: Der gesamte Code, den Sie gerade initiiert haben, wird nutzlos, da Sie mit ihm kein weiteres Anfrage mehr bearbeiten können. Sagen Sie das irgendeinem Programmierer, der in einer anderen Sprache schreibt, und Sie werden das Unverständnis auf seinem Gesicht sehen.

PHP-Entwickler haben jahrelang nach Lösungen für dieses Problem gesucht und durchdachte Methoden des "Lazy Loading", Mikroframeworks, optimierte Bibliotheken, Caching usw. verwendet. Aber letztendlich müssen sie die gesamte Anwendung trotzdem immer wieder zurücksetzen und von vorne beginnen. (Hinweis des Übersetzers: Teilweise wird dieses Problem mit dem Erscheinen preload in PHP 7.4)

Kann PHP mit Go mehr als eine Anfrage gleichzeitig verarbeiten?

Es ist möglich, PHP-Skripte zu schreiben, die länger als einige Minuten (von Stunden bis Tagen) leben: zum Beispiel Cron-Jobs, CSV-Parser oder Warteschlangen-Analyzer. Alle arbeiten nach demselben Schema: Sie holen die Aufgabe ab, führen sie aus und warten auf die nächste. Der Code bleibt ständig im Speicher, wodurch wertvolle Millisekunden gespart werden, da beim Laden von Framework und Anwendung zahlreiche zusätzliche Schritte erforderlich sind.

Einen langlebigen Skript zu entwickeln, ist jedoch nicht so einfach. Jeder Fehler kann den Prozess vollständig stoppen, die Diagnose von Speicherlecks kann frustrierend sein, und Debugging durch F5 kann nicht mehr verwendet werden.

Die Situation hat sich mit der Einführung von PHP 7 verbessert: Ein zuverlässiger Garbage Collector wurde eingeführt, die Fehlerbehandlung ist einfacher geworden, und die Kern-Erweiterungen sind nun gegen Speicherlecks geschützt. Allerdings müssen Ingenieure weiterhin vorsichtig mit dem Speicher umgehen und auf Probleme im Code achten (gibt es eine Sprache, in der man diesen Aspekt ignorieren kann?). Dennoch gibt es in PHP 7 weniger unerwartete Überraschungen.

Kann man das Modell für langlebige PHP-Skripte nehmen, es für trivialere Aufgaben wie die Verarbeitung von HTTP-Anfragen anpassen und damit die Notwendigkeit beseitigen, bei jeder Anfrage alles neu zu laden?

Um diese Aufgabe zu lösen, musste zuerst eine serverseitige Anwendung implementiert werden, die in der Lage ist, HTTP-Anfragen zu empfangen und diese nacheinander an einen PHP-Worker weiterzuleiten, ohne ihn jedes Mal zu beenden.

Wir wussten, dass wir einen Webserver in reinem PHP (PHP-PM) oder mit der C-Erweiterung (Swoole) schreiben konnten. Obwohl jeder Ansatz seine Vorteile hat, waren beide Optionen für uns unzureichend – wir wollten mehr. Wir benötigten nicht nur einen Webserver – wir strebten eine Lösung an, die uns von den Problemen des "schweren Starts" in PHP befreit, die gleichzeitig leicht anpassbar und erweiterbar für spezifische Anwendungen ist. Mit anderen Worten, wir benötigten einen Anwendungsserver.

Kann Go dabei helfen? Wir wussten, dass es helfen kann, denn diese Sprache kompiliert Anwendungen in eigenständige Binärdateien; sie ist plattformübergreifend; nutzt ein sehr elegantes Modell der parallelen Verarbeitung (Concurrency) und eine Bibliothek für die HTTP-Verarbeitung; und schließlich stehen uns Tausende von Open-Source-Bibliotheken und -Integrationen zur Verfügung.

Die Schwierigkeiten bei der Kombination von zwei Programmiersprachen

Zunächst musste festgelegt werden, wie zwei oder mehr Anwendungen miteinander kommunizieren würden.

Zum Beispiel über eine ausgezeichnete Bibliothek von Alex Palästras könnte die gemeinsame Nutzung von Speicher zwischen PHP- und Go-Prozessen ermöglicht werden (ähnlich wie mod_php in Apache). Aber diese Bibliothek hat Eigenschaften, die ihre Verwendung für unser Anliegen einschränken.

Wir entschieden uns, einen anderen, verbreitetere Ansatz zu wählen: die Interaktion zwischen Prozessen über Sockets/Pipelines zu erstellen. Dieser Ansatz hat sich in den letzten Jahrzehnten als zuverlässig erwiesen und wurde auf Betriebssystemebene gut optimiert.

Zu Beginn erstellten wir ein einfaches binäres Protokoll für den Datenaustausch zwischen Prozessen und die Behandlung von Übertragungsfehlern. In seiner einfachsten Form ähnelt ein Protokoll dieses Typs netstring c mit einem Header von fester Größe (in unserem Fall 17 Byte), der Informationen über den Pakettyp, seine Größe und eine binäre Maske zur Überprüfung der Datenintegrität enthält.

Auf der PHP-Seite verwendeten wir die Funktion pack, und auf der Go-Seite die Bibliothek encoding/binary.

Ein Protokoll erschien uns nicht ausreichend – und wir fügten die Möglichkeit hinzu, Go-Dienste net/rpc direkt aus PHP. Später half uns das sehr in der Entwicklung, da wir Go-Bibliotheken leicht in PHP-Anwendungen integrieren konnten. Das Ergebnis dieser Arbeit sehen Sie zum Beispiel in einem unserer anderen Open-Source-Produkte Goridge.

Aufteilung von Aufgaben auf mehrere PHP-Worker

Nach der Implementierung des Interaktionsmechanismus haben wir darüber nachgedacht, wie wir Aufgaben am effizientesten an die PHP-Prozesse übergeben können. Wenn eine Aufgabe eintrifft, muss der Anwendungsserver einen freien Worker für deren Ausführung auswählen. Wenn ein Worker/Prozess mit einem Fehler beendet wurde oder "gestorben" ist, entfernen wir ihn und erstellen einen neuen Ersatz. Wenn jedoch der Worker/Prozess erfolgreich abgeschlossen wurde, senden wir ihn zurück in den Pool der verfügbaren Worker zur Ausführung von Aufgaben.

RoadRunner: PHP ist nicht dafür gemacht, zu sterben, oder Golang eilt zur Hilfe

Für die Speicherung des Pools aktiver Worker haben wir einen gepufferten Kanal, um unerwartet "gestorbene" Worker aus dem Pool zu entfernen, haben wir einen Mechanismus zur Fehler- und Statusüberwachung der Worker hinzugefügt.

So erhielten wir einen funktionierenden PHP-Server, der in der Lage ist, beliebige Anfragen, die in binärer Form vorliegen, zu verarbeiten.

Damit unsere Anwendung als Webserver fungieren kann, mussten wir einen zuverlässigen PHP-Standard auswählen, um alle eingehenden HTTP-Anfragen darzustellen. In unserem Fall wandeln wir net/http-Anfragen aus Go in das Format PSR-7, damit es mit den meisten der heute verfügbaren PHP-Frameworks kompatibel ist.

Da PSR-7 als unveränderlich gilt (manche werden sagen, dass das technisch nicht so ist), müssen Entwickler Anwendungen schreiben, die im Grunde nicht mit der Anfrage als globalem Objekt umgehen. Dies passt hervorragend zum Konzept von langlebigen PHP-Prozessen. Unsere endgültige Implementierung, die noch keinen Namen hatte, sah folgendermaßen aus:

RoadRunner: PHP ist nicht dafür gemacht, zu sterben, oder Golang eilt zur Hilfe

Wir präsentieren RoadRunner — ein hochleistungsfähiger Server für PHP-Anwendungen

Unsere erste Testaufgabe war ein API-Backend, bei dem es zeitweise unvorhersehbare Anstiege bei den Anfragen gab (deutlich häufiger als gewöhnlich). Obwohl in den meisten Fällen die Möglichkeiten von nginx ausreichten, stießen wir regelmäßig auf den Fehler 502, da wir das System nicht schnell genug an die erwartete Laststeigerung anpassen konnten.

Zur Lösung dieses Problems setzten wir Anfang 2018 unseren ersten PHP/Go-Anwendungsserver in Betrieb. Und sofort erzielten wir unglaubliche Ergebnisse! Wir haben nicht nur den Fehler 502 vollständig eliminiert, sondern konnten auch die Anzahl der Server um zwei Drittel reduzieren und eine Menge Geld sowie Kopfschmerztabletten für Ingenieure und Produktmanager sparen.

Bis zur Mitte des Jahres haben wir unsere Lösung verbessert, sie unter der MIT-Lizenz auf GitHub veröffentlicht und sie genannt RoadRunner, wodurch ihre unglaubliche Geschwindigkeit und Effizienz hervorgehoben wird.

Wie RoadRunner Ihren Entwicklungs-Stack verbessern kann

Anwendung RoadRunner wurde es uns ermöglicht, Middleware net/http auf der Go-Seite zu verwenden, um die JWT-Überprüfung bereits bevor die Anfrage in PHP eintrifft, durchzuführen, sowie WebSockets zu verarbeiten und globalen Zustand in Prometheus zu aggregieren.

Dank des eingebauten RPC können APIs beliebiger Go-Bibliotheken für PHP ohne das Schreiben von Wrapper-Erweiterungen geöffnet werden. Noch wichtiger ist, dass man mit RoadRunner neue Server bereitstellen kann, die sich von HTTP unterscheiden. Beispiele dafür sind das Starten von PHP-Handlern AWS Lambda, das Erstellen zuverlässiger Queue-Parser und sogar das Hinzufügen von , würden Sie den Port zu unseren Anwendungen.

Mit Hilfe der Communities von PHP und Go haben wir die Stabilität der Lösung erhöht, in einigen Tests die Anwendungsleistung um bis zu das 40-Fache gesteigert, die Debugging-Tools verbessert, die Integration mit dem Symfony-Framework umgesetzt und Unterstützung für HTTPS, HTTP/2, Plugins und PSR-17 hinzugefügt.

Fazit

Einige sind immer noch gefangen in der veralteten Vorstellung, dass PHP eine langsame und schwerfällige Sprache ist, die nur für die Entwicklung von Plugins für WordPress geeignet ist. Diese Leute könnten sogar sagen, dass PHP ein solches Limit hat: Wenn eine Anwendung groß genug wird, muss man eine „reifere“ Sprache wählen und den über viele Jahre angesammelten Code neu schreiben.

Darauf möchten wir antworten: Denken Sie noch einmal nach. Wir glauben, dass nur Sie selbst irgendwelche Einschränkungen für PHP setzen. Sie können Ihr ganzes Leben damit verbringen, von einer Sprache zur anderen zu wechseln, während Sie versuchen, die ideale Kombination für Ihre Bedürfnisse zu finden, oder Sie können beginnen, Sprachen als Werkzeuge wahrzunehmen. Die scheinbaren Nachteile einer Sprache wie PHP können in Wirklichkeit die Gründe für ihren Erfolg sein. Und wenn man sie mit einer anderen Sprache wie Go kombiniert, wird man viel leistungsstärkere Produkte schaffen, als wenn man sich auf eine einzige Sprache beschränkt.

Nachdem wir mit der Kombination aus Go und PHP gearbeitet haben, können wir sagen, dass wir sie lieben gelernt haben. Wir planen nicht, eines zugunsten des anderen aufzugeben – im Gegenteil, wir werden nach Wegen suchen, noch mehr Nutzen aus diesem dualen Stack zu ziehen.

UPD: Wir begrüßen den Schöpfer von RoadRunner und Mitautor des ursprünglichen Artikels – Lachezis

Quelle: habr.com

60GB SSD 8Gb DDR4