RoadRunner: PHP nicht zum Sterben gebracht oder Golang zur Rettung



Hallo Habr! Wir bei Badoo arbeiten aktiv an der PHP-Leistung , da wir ein ziemlich großes System in dieser Sprache haben und das Problem der Leistung eine Frage des Geldsparens ist. Vor mehr als zehn Jahren haben wir für dieses PHP-FPM eine Reihe von Patches für PHP erstellt, die später offiziell ausgeliefert wurden.

In den letzten Jahren hat PHP große Fortschritte gemacht: Der Garbage Collector hat sich verbessert, das Stabilitätsniveau hat sich verbessert - heute können Sie in PHP Dämonen und langlebige Skripte ohne besondere Probleme schreiben. Dadurch konnte Spiral Scout noch weiter gehen: RoadRunner löscht im Gegensatz zu PHP-FPM nicht den Speicher zwischen Anforderungen, was zu einem zusätzlichen Leistungsgewinn führt (obwohl dieser Ansatz den Entwicklungsprozess verkompliziert). Wir experimentieren jetzt mit diesem Tool, haben jedoch noch keine Ergebnisse, die geteilt werden könnten. Das Warten auf sie hat mehr Spaß gemacht, wir veröffentlichen die Übersetzung der Ankündigung des RoadRunner von Spiral Scout.

Der Ansatz des Artikels liegt uns sehr nahe: 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 eine zugunsten der anderen aufzugeben.

Viel Spaß!



In den letzten zehn Jahren haben wir Anwendungen sowohl für Fortune 500-Unternehmen als auch für Unternehmen mit einer Zielgruppe von nicht mehr als 500 Benutzern erstellt. Während dieser ganzen Zeit haben unsere Ingenieure das Backend hauptsächlich in PHP entwickelt. Vor zwei Jahren hat jedoch nicht nur die Leistung unserer Produkte, sondern auch deren Skalierbarkeit stark beeinflusst. Wir haben Golang (Go) in unseren Technologie-Stack aufgenommen.

Fast sofort stellten wir fest, dass Go es uns ermöglicht, größere Anwendungen mit bis zu 40-mal höherer Leistung zu erstellen. Damit konnten wir bestehende in PHP geschriebene Produkte erweitern und durch eine Kombination der Vorteile beider Sprachen verbessern.

Wir werden erzählen, wie die Kombination von Go und PHP hilft, echte Entwicklungsprobleme zu lösen, und wie es für uns zu einem Werkzeug geworden ist, das einen Teil der Probleme lösen kann, die mit dem "sterbenden" PHP-Modell verbunden sind .

Ihre alltägliche PHP-Entwicklungsumgebung


Bevor wir darüber sprechen, wie Go das "sterbende" PHP-Modell beleben kann, schauen wir uns Ihre Standard-PHP-Entwicklungsumgebung an.

In den meisten Fällen starten Sie die Anwendung mit einer Kombination aus dem Nginx-Webserver und dem PHP-FPM-Server. Der erste liefert statische Dateien und leitet bestimmte Anforderungen an PHP-FPM um, und PHP-FPM selbst führt den PHP-Code aus. Vielleicht verwenden Sie ein weniger beliebtes Bundle von Apache und mod_php. Obwohl es etwas anders funktioniert, sind die Prinzipien dieselben.

Überlegen Sie, wie PHP-FPM Anwendungscode ausführt. Wenn eine Anforderung eintrifft, initialisiert PHP-FPM einen untergeordneten PHP-Prozess und übergibt die Anforderungsdetails als Teil seines Status (_GET, _POST, _SERVER usw.).

Der Status kann sich während der Ausführung des PHP-Skripts nicht ändern, sodass Sie einen neuen Satz von Eingabedaten nur auf eine Weise abrufen können: indem Sie den Prozessspeicher löschen und erneut initialisieren.

Dieses Ausführungsmodell hat viele Vorteile. Sie müssen sich keine großen Sorgen um den Speicherverbrauch machen, alle Prozesse sind vollständig isoliert. Wenn einer von ihnen stirbt, wird er automatisch neu erstellt und hat keine Auswirkungen auf die anderen Prozesse. Dieser Ansatz weist jedoch auch Nachteile auf, die beim Versuch auftreten, die Anwendung zu skalieren.

Nachteile und Ineffizienzen einer regulären PHP-Umgebung


Wenn Sie sich in PHP beruflich weiterentwickeln, wissen Sie, wo Sie mit der Auswahl eines Frameworks ein neues Projekt starten können. Es ist eine Bibliothek für Abhängigkeitsinjektion, ORMs, Übersetzungen und Vorlagen. Natürlich können alle Benutzereingaben bequem in einem Objekt platziert werden (Symfony / HttpFoundation oder PSR-7). Frameworks sind cool!

Aber alles hat einen Preis. In jedem Framework auf Unternehmensebene müssen Sie zum Verarbeiten einer einfachen Benutzeranforderung oder zum Zugreifen auf die Datenbank mindestens Dutzende von Dateien herunterladen, zahlreiche Klassen erstellen und verschiedene Konfigurationen analysieren. Das Schlimmste ist jedoch, dass Sie nach Abschluss jeder Aufgabe alles zurücksetzen und neu starten müssen: Der gesamte Code, den Sie gerade initiiert haben, wird unbrauchbar, sodass Sie keine weitere Anforderung mehr verarbeiten. Wenn Sie einem Programmierer, der in einer anderen Sprache schreibt, davon erzählen, werden Sie Verwirrung in seinem Gesicht sehen.

Seit Jahren suchen PHP-Ingenieure nach Möglichkeiten, um dieses Problem zu lösen, indem sie durchdachte Methoden wie Lazy Loading, Mikroframes, optimierte Bibliotheken, Cache usw. verwenden. Am Ende müssen Sie jedoch die gesamte Anwendung zurücksetzen und immer wieder von vorne beginnen. (Anmerkung des Übersetzers: Dieses Problem wird mit dem Aufkommen von Preload in PHP 7.4 teilweise gelöst.)

Kann PHP mit Go mehr als eine Anfrage überleben?


Sie können PHP-Skripte schreiben, die länger als ein paar Minuten (bis zu Stunden oder Tagen) dauern: zum Beispiel Cron-Tasks, CSV-Parser, Warteschlangenunterbrecher. Sie arbeiten alle nach einem Szenario: Sie extrahieren die Aufgabe, erledigen sie und warten auf das nächste. Der Code befindet sich ständig im Speicher und spart wertvolle Millisekunden, da zum Herunterladen des Frameworks und der Anwendung viele zusätzliche Schritte erforderlich sind.

Die Entwicklung langlebiger Skripte ist jedoch nicht so einfach. Jeder Fehler beendet den Prozess vollständig, die Diagnose von Speicherlecks ist ärgerlich und das Debuggen mit F5 ist nicht mehr möglich.

Mit der Veröffentlichung von PHP 7 verbesserte sich die Situation: Es erschien ein zuverlässiger Garbage Collector, es wurde einfacher, Fehler zu behandeln, und Kernel-Erweiterungen sind jetzt vor Lecks geschützt. Zwar müssen Ingenieure den Speicher noch sorgfältig handhaben und sich an Statusprobleme im Code erinnern (gibt es eine Sprache, in der Sie diese Dinge ignorieren können?). Und doch gibt es in PHP 7 weniger Überraschungen.

Ist es möglich, ein Modell für die Arbeit mit langlebigen PHP-Skripten zu verwenden, es für einfachere Aufgaben wie die Verarbeitung von HTTP-Anforderungen anzupassen und dadurch die Notwendigkeit zu beseitigen, bei jeder Anforderung alles von Grund auf neu herunterzuladen?

Um dieses Problem zu lösen, musste zunächst eine Serveranwendung implementiert werden, die HTTP-Anforderungen akzeptieren und einzeln an den PHP-Worker umleiten kann, ohne ihn jedes Mal zu töten.

Wir wussten, dass wir einen Webserver in reinem PHP (PHP-PM) oder mit der C-Erweiterung (Swoole) schreiben können. Und obwohl jede Methode ihre eigenen Vorteile hat, passten beide Optionen nicht zu uns - ich wollte etwas mehr. Es wurde nicht nur ein Webserver benötigt - wir erwarteten eine Lösung, die uns vor den Problemen bewahrt, die mit einem „harten Start“ in PHP verbunden sind und die gleichzeitig leicht an bestimmte Anwendungen angepasst und erweitert werden können. Das heißt, wir brauchten einen Anwendungsserver.

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

Schwierigkeiten beim Kombinieren von zwei Programmiersprachen


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

Mithilfe der hervorragenden Bibliothek von Alex Palaestras konnte beispielsweise die gemeinsame Nutzung von Speicher durch die PHP- und Go-Prozesse implementiert werden (ähnlich wie bei mod_php in Apache). Diese Bibliothek verfügt jedoch ü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 die Behandlung von Übertragungsfehlern erstellt. In seiner einfachsten Form ähnelt ein Protokoll dieses Typs einem Netzstring mit einem Paketheader fester Größe (in unserem Fall 17 Byte), der Informationen über den Pakettyp, seine Größe und eine Binärmaske zur Überprüfung der Datenintegrität enthält.

Auf der PHP-Seite haben wir die Pack-Funktion und auf der Go-Seite die Codierungs- / Binärbibliothek verwendet.

Ein Protokoll hat uns nicht gereicht - und wir haben die Möglichkeit hinzugefügt, net / rpc Go-Dienste direkt von PHP aus aufzurufen . Später hat es uns 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 Goridge zu sehen .

Verteilung der Aufgaben auf mehrere PHP-Mitarbeiter


Nach der Implementierung des Interaktionsmechanismus haben wir uns Gedanken darüber gemacht, wie Aufgaben am besten auf PHP-Prozesse übertragen werden können. Wenn eine Aufgabe eintrifft, muss der Anwendungsserver einen freien Mitarbeiter auswählen, um sie auszuführen. Wenn der Worker / Prozess mit einem Fehler beendet wurde oder "gestorben" ist, werden wir ihn entfernen und im Gegenzug einen neuen erstellen. Und wenn der Worker / Prozess erfolgreich funktioniert hat, geben wir ihn an den Pool der Worker zurück, die zur Erledigung der Aufgaben verfügbar sind.



Wir haben einen gepufferten Kanal verwendet , um den Pool aktiver Mitarbeiter zu speichern. Um unerwartet „tote“ Mitarbeiter aus dem Pool zu entfernen, haben wir einen Mechanismus zum Verfolgen von Fehlern und des Status von Arbeitnehmern hinzugefügt.

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

Damit unsere Anwendung als Webserver arbeiten konnte, musste ich einen zuverlässigen PHP-Standard für die Darstellung eingehender HTTP-Anforderungen auswählen. In unserem Fall konvertieren wir einfach die net / http-Anfrage von Go in das PSR-7- Format, sodass sie mit den meisten heute verfügbaren PHP-Frameworks kompatibel ist.

Da PSR-7 als unveränderlich angesehen wird (jemand wird sagen, dass dies technisch nicht der Fall ist), müssen Entwickler Anwendungen schreiben, die die Anforderung im Prinzip nicht als globale Einheit behandeln. Dies passt gut zum Konzept langlebiger PHP-Prozesse. Unsere endgültige Implementierung, die noch keinen Namen erhalten hat, sah folgendermaßen aus:



Einführung in RoadRunner - einen leistungsstarken PHP-Anwendungsserver


Unsere erste Testaufgabe war ein API-Backend, das regelmäßig unvorhersehbare Anforderungsschübe verursachte (viel häufiger als gewöhnlich). Obwohl in den meisten Fällen genügend Nginx-Funktionen vorhanden waren, trat regelmäßig ein 502-Fehler auf, da wir das System nicht schnell genug für den erwarteten Anstieg der Last ausgleichen konnten.

Um diese Lösung zu ersetzen, haben wir Anfang 2018 unseren ersten PHP / Go-Anwendungsserver bereitgestellt. Und sofort einen unglaublichen Effekt bekommen! Wir haben nicht nur den Fehler 502 vollständig beseitigt, sondern konnten auch die Anzahl der Server um zwei Drittel reduzieren und so eine Menge Geld und Pillen gegen Kopfschmerzen für Ingenieure und Produktmanager sparen.

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

Wie RoadRunner Ihren Entwicklungsstapel verbessern kann


Durch die Verwendung von RoadRunner konnten wir Middleware net / http auf der Go-Seite verwenden, um die JWT-Überprüfung durchzuführen, bevor die Anforderung in PHP eingeht, sowie um WebSockets und global aggregierte Status 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 RoadRunner andere Server als HTTP bereitstellen kann. Beispiele hierfür sind das Ausführen von AWS Lambda- Handlern in PHP, das Erstellen robuster Warteschlangenauflöser und das Hinzufügen von gRPC zu unseren Anwendungen.

Mithilfe der PHP- und Go-Communitys haben wir die Stabilität der Lösung erhöht, in einigen Tests die Anwendungsleistung um das 40-fache gesteigert, die Debugging-Tools verbessert, die Integration in das Symfony-Framework implementiert und die Unterstützung für HTTPS, HTTP / 2, Plug-Ins und PSR-17 hinzugefügt.

Fazit


Einige sind immer noch fasziniert von der veralteten Vorstellung von PHP als einer langsamen, umständlichen Sprache, die nur zum Schreiben von Plugins für WordPress geeignet ist. Diese Leute können sogar sagen, dass PHP eine solche Einschränkung hat: Wenn die Anwendung groß genug wird, müssen Sie eine „ausgereiftere“ Sprache wählen und die über viele Jahre angesammelte Codebasis neu schreiben.

Ich möchte das alles beantworten: Denken Sie noch einmal nach. Wir glauben, dass nur Sie selbst Einschränkungen für PHP festgelegt haben. Sie können Ihr ganzes Leben damit verbringen, von einer Sprache in eine andere zu wechseln und zu versuchen, die perfekte Kombination mit Ihren Bedürfnissen zu finden, oder Sie können beginnen, Sprachen als Werkzeuge wahrzunehmen. Die offensichtlichen Mängel einer Sprache wie PHP können tatsächlich die Gründe 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 nur eine einzige Sprache verwenden würden.

Nachdem wir mit einer Reihe von Go und PHP gearbeitet haben, können wir sagen, dass wir sie lieben. Wir planen nicht, eines zugunsten des anderen zu opfern - im Gegenteil, wir werden nach Wegen suchen, um noch mehr Nutzen aus diesem Doppelstapel zu ziehen.

UPD: Willkommen beim RoadRunner-Ersteller und Co-Autor des Originalartikels - Lachezis

Source: https://habr.com/ru/post/de434272/


All Articles