RoadRunner: PHP ist nicht dafür gemacht, zu sterben, und Golang hilft nicht

RoadRunner: PHP ist nicht dafür gemacht, zu sterben, und Golang hilft nicht

Hey Habr! Wir sind bei Badoo aktiv Ich arbeite an der PHP-Leistung, da wir ein ziemlich großes System in dieser Sprache haben und das Leistungsproblem eine Kosteneinsparungsfrage ist. Vor mehr als zehn Jahren haben wir dafür PHP-FPM erstellt, das zunächst eine Reihe von Patches für PHP war und später in die offizielle Distribution gelangte.

In den letzten Jahren hat PHP große Fortschritte gemacht: Der Garbage Collector hat sich verbessert, die Stabilität ist gestiegen – heute kann man problemlos Daemons und langlebige Skripte in PHP schreiben. Dies ermöglichte es Spiral Scout, noch weiter zu gehen: RoadRunner räumt im Gegensatz zu PHP-FPM den Speicher zwischen Anfragen nicht auf, was zu einem zusätzlichen Leistungsgewinn führt (obwohl dieser Ansatz den Entwicklungsprozess verkompliziert). Wir experimentieren derzeit mit diesem Tool, haben aber noch keine Ergebnisse, die wir mitteilen können. Damit das Warten auf sie noch mehr Spaß macht, Wir veröffentlichen die Übersetzung der RoadRunner-Ankündigung von Spiral Scout.

Der Ansatz aus dem Artikel liegt uns am Herzen: Bei der Lösung unserer Probleme verwenden wir meistens auch eine Reihe von PHP und Go, um die Vorteile beider Sprachen zu nutzen und nicht die eine zugunsten der anderen aufzugeben.

Viel Spaß damit!

In den letzten zehn Jahren haben wir Anwendungen für Unternehmen aus der Liste erstellt Fortune-500und für Unternehmen mit einem Publikum von nicht mehr als 500 Benutzern. Während dieser Zeit haben unsere Ingenieure das Backend hauptsächlich in PHP entwickelt. Doch vor zwei Jahren hatte etwas einen großen Einfluss nicht nur auf die Leistung unserer Produkte, sondern auch auf deren Skalierbarkeit – wir führten Golang (Go) in unseren Technologie-Stack ein.

Fast sofort stellten wir fest, dass wir mit Go größere Anwendungen mit bis zu 40-fachen Leistungssteigerungen erstellen konnten. Damit konnten wir unsere bestehenden PHP-Produkte erweitern und verbessern, indem wir die Vorteile beider Sprachen kombinierten.

Wir verraten Ihnen, wie die Kombination von Go und PHP dabei hilft, echte Entwicklungsprobleme zu lösen und wie sie für uns zu einem Tool geworden ist, das einige der damit verbundenen Probleme beseitigen kann Aussterbendes PHP-Modell.

Ihre tägliche PHP-Entwicklungsumgebung

Bevor wir darüber sprechen, wie Sie mit Go das sterbende PHP-Modell wiederbeleben können, werfen wir einen Blick auf Ihre Standard-PHP-Entwicklungsumgebung.

In den meisten Fällen führen Sie Ihre Anwendung mit einer Kombination aus dem Nginx-Webserver und dem PHP-FPM-Server aus. Ersteres bedient statische Dateien und leitet spezifische Anfragen an PHP-FPM weiter, während PHP-FPM selbst PHP-Code ausführt. Möglicherweise verwenden Sie die weniger beliebte Kombination aus Apache und mod_php. Obwohl es etwas anders funktioniert, sind die Prinzipien dieselben.

Werfen wir einen Blick darauf, wie PHP-FPM Anwendungscode ausführt. Wenn eine Anfrage eingeht, initialisiert PHP-FPM einen untergeordneten PHP-Prozess und übergibt die Details der Anfrage als Teil ihres Status (_GET, _POST, _SERVER usw.).

Der Status kann sich während der Ausführung des PHP-Skripts nicht ändern, daher gibt es nur eine Möglichkeit, einen neuen Satz Eingabedaten zu erhalten: durch Löschen des Prozessspeichers und erneutes Initialisieren.

Dieses Ausführungsmodell hat viele Vorteile. Sie müssen sich nicht zu viele Gedanken über den Speicherverbrauch machen, alle Prozesse sind vollständig isoliert, und wenn einer von ihnen „stirbt“, wird er automatisch neu erstellt und hat keine Auswirkungen auf die übrigen Prozesse. Allerdings weist dieser Ansatz auch Nachteile auf, die bei der Skalierung der Anwendung auftreten.

Nachteile und Ineffizienzen einer regulären PHP-Umgebung

Wenn Sie ein professioneller PHP-Entwickler sind, wissen Sie, wo Sie ein neues Projekt starten müssen – mit der Wahl eines Frameworks. Es besteht aus Abhängigkeitsinjektionsbibliotheken, ORMs, Übersetzungen und Vorlagen. Und natürlich können alle Benutzereingaben bequem in einem Objekt (Symfony/HttpFoundation oder PSR-7) zusammengefasst werden. Frameworks sind cool!

Aber alles hat seinen Preis. In jedem Framework auf Unternehmensebene müssen Sie zur Verarbeitung einer einfachen Benutzeranfrage oder eines Zugriffs auf eine Datenbank mindestens Dutzende Dateien laden, zahlreiche Klassen erstellen und mehrere Konfigurationen analysieren. Aber das Schlimmste ist, dass Sie nach Abschluss jeder Aufgabe alles zurücksetzen und von vorne beginnen müssen: Der gesamte Code, den Sie gerade initiiert haben, wird unbrauchbar und Sie können mit seiner Hilfe keine weitere Anfrage mehr bearbeiten. Erzählen Sie dies jedem Programmierer, der in einer anderen Sprache schreibt, und Sie werden die Verwirrung auf seinem Gesicht sehen.

PHP-Ingenieure suchen seit Jahren nach Möglichkeiten, dieses Problem zu lösen, indem sie clevere Lazy-Loading-Techniken, Mikroframeworks, optimierte Bibliotheken, Cache usw. verwenden. Aber am Ende muss man immer noch die gesamte Anwendung zurücksetzen und von vorne beginnen . (Anmerkung des Übersetzers: Dieses Problem wird mit der Einführung von teilweise gelöst Vorspannung in PHP 7.4)

Kann PHP mit Go mehr als eine Anfrage überstehen?

Es ist möglich, PHP-Skripte zu schreiben, die länger als ein paar Minuten (bis zu Stunden oder Tage) leben: zum Beispiel Cron-Tasks, CSV-Parser, Warteschlangenbrecher. Sie arbeiten alle nach dem gleichen Szenario: Sie rufen eine Aufgabe ab, führen sie aus und warten auf die nächste. Der Code bleibt ständig im Speicher und spart wertvolle Millisekunden, da zum Laden des Frameworks und der Anwendung viele zusätzliche Schritte erforderlich sind.

Aber langlebige Skripte zu entwickeln ist nicht einfach. Jeder Fehler beendet den Prozess vollständig, die Diagnose von Speicherlecks ist ärgerlich und ein F5-Debugging ist nicht mehr möglich.

Mit der Veröffentlichung von PHP 7 hat sich die Situation verbessert: Ein zuverlässiger Garbage Collector ist erschienen, der Umgang mit Fehlern ist einfacher geworden und Kernel-Erweiterungen sind jetzt auslaufsicher. Zwar müssen Ingenieure immer noch vorsichtig mit dem Speicher umgehen und sich der Zustandsprobleme im Code bewusst sein (gibt es eine Sprache, die diese Dinge ignorieren kann?). Dennoch hält PHP 7 weniger Überraschungen für uns bereit.

Ist es möglich, das Modell der Arbeit mit langlebigen PHP-Skripten zu übernehmen und es an trivialere Aufgaben wie die Verarbeitung von HTTP-Anfragen anzupassen und dadurch die Notwendigkeit zu beseitigen, bei jeder Anfrage alles von Grund auf neu zu laden?

Um dieses Problem zu lösen, mussten wir zunächst eine Serveranwendung implementieren, die HTTP-Anfragen akzeptieren und diese einzeln an den PHP-Worker umleiten konnte, ohne ihn jedes Mal zu beenden.

Wir wussten, dass wir einen Webserver in reinem PHP (PHP-PM) oder mit einer C-Erweiterung (Swoole) schreiben konnten. Und obwohl jede Methode ihre eigenen Vorzüge hat, passten beide Optionen nicht zu uns – wir wollten etwas mehr. Wir brauchten mehr als nur einen Webserver – wir erwarteten eine Lösung, die uns die mit einem „harten Start“ in PHP verbundenen Probleme erspart und die gleichzeitig leicht für bestimmte Anwendungen angepasst und erweitert werden kann. Das heißt, wir brauchten einen Anwendungsserver.

Kann Go dabei helfen? Wir wussten, dass dies möglich ist, da die Sprache Anwendungen in einzelne Binärdateien kompiliert. es ist plattformübergreifend; verwendet ein eigenes, sehr elegantes Parallelverarbeitungsmodell (Parallelität) und eine Bibliothek für die Arbeit mit HTTP; Und schließlich stehen uns Tausende von Open-Source-Bibliotheken und -Integrationen zur Verfügung.

Die Schwierigkeiten bei der Kombination zweier Programmiersprachen

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

Zum Beispiel mit ausgezeichnete Bibliothek Alex Palaestras, es war möglich, Speicher zwischen PHP- und Go-Prozessen zu teilen (ähnlich wie mod_php in Apache). Aber diese Bibliothek verfügt über Funktionen, die ihre Verwendung zur Lösung unseres Problems einschränken.

Wir haben uns für einen anderen, allgemeineren Ansatz entschieden: die Interaktion zwischen Prozessen über Sockets/Pipelines aufzubauen. Dieser Ansatz hat sich in den letzten Jahrzehnten als zuverlässig erwiesen und wurde auf Betriebssystemebene gut optimiert.

Zunächst haben wir ein einfaches Binärprotokoll für den Datenaustausch zwischen Prozessen und den Umgang mit Übertragungsfehlern erstellt. In seiner einfachsten Form ähnelt dieser Protokolltyp Netzstring с Paket-Header mit fester Größe (in unserem Fall 17 Bytes), das Informationen über den Pakettyp, seine Größe und eine Binärmaske zur Überprüfung der Integrität der Daten enthält.

Auf der PHP-Seite haben wir verwendet Packfunktionund auf der Go-Seite die Bibliothek Kodierung/binär.

Es schien uns, dass ein Protokoll nicht ausreichte – und wir fügten die Möglichkeit zum Anrufen hinzu net/rpc go-Dienste direkt aus PHP. Dies hat uns später bei der Entwicklung sehr geholfen, da wir Go-Bibliotheken problemlos in PHP-Anwendungen integrieren konnten. Das Ergebnis dieser Arbeit ist beispielsweise in unserem anderen Open-Source-Produkt zu sehen Goridge.

Verteilen von Aufgaben auf mehrere PHP-Worker

Nach der Implementierung des Interaktionsmechanismus begannen wir darüber nachzudenken, wie wir Aufgaben am effizientesten an PHP-Prozesse übertragen können. Wenn eine Aufgabe eintrifft, muss der Anwendungsserver einen freien Worker für die Ausführung auswählen. Wenn ein Worker/Prozess mit einem Fehler beendet wird oder „stirbt“, entfernen wir ihn und erstellen einen neuen, um ihn zu ersetzen. Und wenn der Worker/Prozess erfolgreich abgeschlossen wurde, geben wir ihn an den Pool der Worker zurück, die für die Ausführung von Aufgaben zur Verfügung stehen.

RoadRunner: PHP ist nicht dafür gemacht, zu sterben, und Golang hilft nicht

Um den Pool aktiver Arbeiter zu speichern, haben wir verwendet gepufferter KanalUm unerwartet „tote“ Arbeiter aus dem Pool zu entfernen, haben wir einen Mechanismus zur Verfolgung von Fehlern und Zuständen von Arbeitern hinzugefügt.

Als Ergebnis erhielten wir einen funktionierenden PHP-Server, der alle in binärer Form dargestellten Anfragen verarbeiten kann.

Damit unsere Anwendung als Webserver funktionieren konnte, mussten wir einen zuverlässigen PHP-Standard wählen, der alle eingehenden HTTP-Anfragen darstellt. In unserem Fall haben wir einfach verwandeln net/http-Anfrage von Gehe zum Format PSR-7sodass es mit den meisten heute verfügbaren PHP-Frameworks kompatibel ist.

Da PSR-7 als unveränderlich gilt (manche würden sagen, dass dies technisch gesehen nicht der Fall ist), müssen Entwickler Anwendungen schreiben, die die Anfrage grundsätzlich nicht als globale Einheit behandeln. Dies passt gut zum Konzept langlebiger PHP-Prozesse. Unsere endgültige Implementierung, die noch benannt werden muss, sah folgendermaßen aus:

RoadRunner: PHP ist nicht dafür gemacht, zu sterben, und Golang hilft nicht

Wir stellen vor: RoadRunner – Hochleistungs-PHP-Anwendungsserver

Unsere erste Testaufgabe war ein API-Backend, das regelmäßig (viel häufiger als üblich) mit unvorhersehbaren Anfragen überhäuft. Obwohl Nginx in den meisten Fällen ausreichend war, kam es regelmäßig zu 502-Fehlern, da wir das System nicht schnell genug für den erwarteten Lastanstieg ausgleichen konnten.

Als Ersatz für diese Lösung haben wir Anfang 2018 unseren ersten PHP/Go-Anwendungsserver bereitgestellt. Und hatte sofort eine unglaubliche Wirkung! Wir haben nicht nur den 502-Fehler vollständig beseitigt, sondern konnten auch die Anzahl der Server um zwei Drittel reduzieren, was den Ingenieuren und Produktmanagern eine Menge Geld und Kopfschmerztabletten erspart hat.

Bis Mitte des Jahres hatten wir unsere Lösung verbessert, sie unter der MIT-Lizenz auf GitHub veröffentlicht und benannt RoadRunner, was seine unglaubliche Geschwindigkeit und Effizienz unterstreicht.

Wie RoadRunner Ihren Entwicklungs-Stack verbessern kann

Anwendung RoadRunner ermöglichte es uns, Middleware net/http auf der Go-Seite zu verwenden, um eine JWT-Überprüfung durchzuführen, bevor die Anfrage PHP erreicht, sowie WebSockets und den Aggregatzustand global in Prometheus zu verarbeiten.

Dank des integrierten RPC können Sie die API aller Go-Bibliotheken für PHP öffnen, ohne Erweiterungs-Wrapper schreiben zu müssen. Noch wichtiger ist, dass Sie mit RoadRunner neue Nicht-HTTP-Server bereitstellen können. Beispiele hierfür sind das Ausführen von Handlern in PHP AWS Lambda, Erstellen zuverlässiger Warteschlangenbrecher und sogar Hinzufügen gRPC zu unseren Anwendungen.

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

Abschluss

Manche Leute sind immer noch in der veralteten Vorstellung gefangen, PHP sei eine langsame, unhandliche Sprache, die sich nur zum Schreiben von Plugins für WordPress eignet. Diese Leute könnten sogar sagen, dass PHP eine solche Einschränkung hat: Wenn die Anwendung groß genug wird, muss man eine „ausgereiftere“ Sprache wählen und die über viele Jahre angesammelte Codebasis neu schreiben.

Auf all das möchte ich antworten: Denken Sie noch einmal darüber nach. Wir glauben, dass nur Sie Einschränkungen für PHP festlegen. Sie können Ihr ganzes Leben damit verbringen, von einer Sprache zur anderen zu wechseln und zu versuchen, die perfekte Lösung für Ihre Bedürfnisse zu finden, oder Sie beginnen, Sprachen als Werkzeuge zu betrachten. Die vermeintlichen Mängel einer Sprache wie PHP könnten tatsächlich der Grund für ihren Erfolg sein. Und wenn Sie es mit einer anderen Sprache wie Go kombinieren, werden Sie viel leistungsfähigere Produkte erstellen, als wenn Sie auf die Verwendung einer einzigen Sprache beschränkt wären.

Nachdem wir mit einer Menge Go und PHP gearbeitet haben, können wir sagen, dass wir sie lieben. Wir haben nicht vor, das eine für das andere zu opfern – im Gegenteil, wir werden nach Möglichkeiten suchen, noch mehr Nutzen aus diesem Dual-Stack zu ziehen.

UPD: Wir begrüßen den Schöpfer von RoadRunner und den Co-Autor des Originalartikels – Lachesis

Source: habr.com

Kommentar hinzufügen