Entwicklung hybrider PHP / Go-Anwendungen mit RoadRunner

Die klassische PHP-Anwendung ist Single-Threaded, starkes Laden (es sei denn, Sie schreiben natürlich auf Mikroframes) und der unvermeidliche Tod des Prozesses nach jeder Anfrage ... Eine solche Anwendung ist schwer und langsam, aber wir können ihr durch Hybridisierung ein zweites Leben geben. Zur Beschleunigung - wir dämonisieren und optimieren Speicherlecks, um eine bessere Leistung zu erzielen - werden wir unseren eigenen Golang RoadRunner PHP-Anwendungsserver einführen, um die Flexibilität zu erhöhen - den PHP-Code zu vereinfachen, den Stapel zu erweitern und die Verantwortung zwischen dem Server und der Anwendung zu teilen. Im Wesentlichen wird unsere Anwendung so funktionieren, als ob wir sie in Java oder einer anderen Sprache schreiben würden.

Dank der Hybridisierung litt eine zuvor langsame Anwendung nicht mehr unter 502 Fehlern unter Last, die durchschnittliche Antwortzeit auf Anforderungen verringerte sich, die Produktivität stieg und die Bereitstellung und Montage wurde einfacher, da die Anwendung vereinheitlicht und unnötige Bindungen in Form von nginx + php-fpm beseitigt wurden.


Anton Titov ( Lachezis ) ist CTO und Mitbegründer von SpiralScout LLC mit 12 Jahren aktiver Erfahrung in der kommerziellen Entwicklung von PHP. In den letzten Jahren hat er Golang aktiv auf dem Entwicklungsstapel des Unternehmens implementiert. Anton sprach auf der PHP Russia 2019 über ein Beispiel.

PHP-Anwendungslebenszyklus


Schematisch sieht ein abstraktes Anwendungsgerät mit einem bestimmten Framework so aus.



Wenn wir eine Anfrage an einen Prozess senden, geschieht Folgendes:

  • Projektinitialisierung;
  • Laden von gemeinsam genutzten Bibliotheken, Frameworks und ORMs;
  • Laden von Bibliotheken, die für ein bestimmtes Projekt erforderlich sind;
  • Routing;
  • Routing-Anfrage an einen bestimmten Controller;
  • Antwortgenerierung.

Dies ist das Funktionsprinzip einer klassischen Single-Threaded-Anwendung mit einem einzigen Einstiegspunkt, der nach jeder Ausführung vollständig zerstört wird oder seinen Status löscht. Der gesamte Code wird aus dem Speicher entladen, der Worker wird gelöscht oder setzt einfach seinen Status zurück.

Faul laden


Die standardmäßige und einfache Möglichkeit zur Beschleunigung ist die Implementierung des Lazy-Loading-Systems oder der On-Demand-Loading-Bibliotheken.



Beim Lazy-Loading fordern wir nur den notwendigen Code an.

Beim Zugriff auf einen bestimmten Controller werden nur die erforderlichen Bibliotheken in den Speicher geladen, verarbeitet und anschließend entladen. Auf diese Weise können Sie die durchschnittliche Antwortzeit des Projekts reduzieren und die Arbeit am Server erheblich vereinfachen. In allen Frameworks, die wir derzeit verwenden, ist das Prinzip des verzögerten Ladens implementiert.

Häufige Berechnungen zwischenspeichern


Die Methode ist komplizierter und wird beispielsweise im Symfony-Framework, in Template-Engines, in ORM-Schemata und im Routing aktiv verwendet. Dies ist kein Caching wie memcached oder Redis für Benutzerdaten. Dieses System erwärmt Teile des Codes im Voraus . Bei der ersten Anforderung generiert das System einen Code oder eine Cache-Datei, und bei nachfolgenden Anforderungen werden diese Berechnungen, die beispielsweise zum Kompilieren einer Vorlage erforderlich sind, nicht mehr ausgeführt.



Das Zwischenspeichern beschleunigt die Anwendung erheblich, erschwert sie jedoch gleichzeitig . Beispielsweise gibt es Probleme beim Ungültigmachen des Caches und beim Aktualisieren der Anwendung. Verwechseln Sie den Benutzercache nicht mit dem Anwendungscache. In einem Fall ändern sich die Daten im Laufe der Zeit, in dem anderen Fall nur, wenn der Code aktualisiert wird.

Anfrage bearbeiten


Wenn eine Anforderung von einem externen PHP-FPM-Server empfangen wird, stimmen der Anforderungseinstiegspunkt und die Initialisierung überein.

Es stellt sich heraus, dass die Anfrage des Kunden der Stand unseres Prozesses ist.

Die einzige Möglichkeit, diesen Status zu ändern, besteht darin, den Worker vollständig zu zerstören und mit einer neuen Anforderung von vorne zu beginnen.



Dies ist ein klassisches Modell mit einem Gewinde und seinen Vorteilen.

  • Alle Arbeiter am Ende der Anfrage sterben.
  • Speicherlecks, Race Condition und Deadlocks sind in PHP nicht enthalten. Sie können sich darüber keine Sorgen machen.
  • Der Code ist einfach: Wir schreiben, verarbeiten die Anfrage, sterben und gehen weiter.

Andererseits laden wir für jede Anforderung das Framework und alle Bibliotheken vollständig, führen einige Berechnungen durch und kompilieren die Vorlagen neu. Mit jeder Anfrage in einem Kreis produzieren wir viele Manipulationen und unnötige Arbeit.

Wie es auf dem Server funktioniert


Höchstwahrscheinlich wird eine Reihe von Nginx und PHP funktionieren. Nginx funktioniert als Reverse-Proxy: Geben Sie den Benutzern einen Teil der Statik und delegieren Sie einen Teil der Anforderungen an den PHP-Prozessmanager PHP-FPM unten. Der Manager stellt bereits einen separaten Mitarbeiter für die Anforderung bereit und verarbeitet sie. Danach wird der Arbeiter zerstört oder geräumt. Als Nächstes wird ein neuer Worker für die nächste Anforderung erstellt.



Ein solches Modell funktioniert stabil - die Anwendung ist fast unmöglich zu töten. Unter hohen Belastungen wirkt sich der Arbeitsaufwand für das Initialisieren und Zerstören von Workern jedoch auf die Systemleistung aus, da selbst bei einer einfachen GET-Anforderung häufig eine Reihe von Abhängigkeiten gezogen und die Datenbankverbindung erneut hergestellt werden muss.

Beschleunigung der Anwendung


Wie kann die klassische Anwendung nach Einführung von Cache und Lazy-Loading beschleunigt werden? Welche anderen Möglichkeiten gibt es?

Wenden Sie sich der Sprache selbst zu .

  • Verwenden Sie OPCache. Ich denke, niemand läuft PHP in der Produktion ohne OPCache aktiviert?
  • Warten Sie auf RFC: Preloading . Sie können eine Reihe von Dateien in eine virtuelle Maschine vorladen.
  • JIT - beschleunigt die Anwendung bei CPU-gebundenen Aufgaben erheblich. Leider hilft es bei Aufgaben im Zusammenhang mit Datenbanken nicht viel.

Verwenden Sie Alternativen . Zum Beispiel die virtuelle HHVM-Maschine von Facebook. Es führt Code in einer optimierten Umgebung aus. Leider ist HHVM nicht vollständig mit der PHP-Syntax kompatibel. Alternativ sind kPHP-Compiler von VK oder PeachPie, die Code vollständig in .NET C # konvertieren, eine Alternative.

Vollständig in eine andere Sprache umschreiben. Dies ist eine radikale Option - das vollständige Laden von Code zwischen Anforderungen wird vollständig vermieden.

Sie können den Status der Anwendung vollständig im Speicher speichern , diesen Speicher aktiv für die Arbeit verwenden, das Konzept eines sterbenden Arbeiters vergessen und die Anwendung zwischen Anforderungen vollständig löschen.

Um dies zu erreichen, verschieben wir den Einstiegspunkt, der früher zusammen mit dem Initialisierungspunkt lag, tief in die Anwendung.

Einstiegspunkt übertragen - Dämonisierung


Dadurch wird eine Endlosschleife in der Anwendung erstellt: Eingehende Anforderung - Führen Sie sie durch das Framework aus - generieren Sie eine Antwort an den Benutzer. Dies ist eine ernsthafte Einsparung - das gesamte Bootstrapping, die gesamte Initialisierung des Frameworks wird nur einmal ausgeführt, und dann werden mehrere Anforderungen von der Anwendung verarbeitet.



Wir passen die Anwendung an


Interessanterweise können wir uns darauf konzentrieren, nur den Teil der Anwendung zu optimieren, der zur Laufzeit ausgeführt wird : Controller, Geschäftslogik. In diesem Fall können Sie das Lazy-Loading-Modell aufgeben. Wir werden am Anfang des Bootstrapping-Projekts teilnehmen - zum Zeitpunkt der Initialisierung. Vorläufige Berechnungen: Routing, Vorlagen, Einstellungen und ORM-Schemata erhöhen die Initialisierung, sparen jedoch in Zukunft Verarbeitungszeit für eine bestimmte Anforderung.



Ich empfehle nicht, beim Herunterladen eines Workers Vorlagen zu kompilieren, aber das Herunterladen aller Konfigurationen ist beispielsweise hilfreich.

Modelle vergleichen


Vergleichen Sie die dämonisierten (links) und klassischen Modelle.



Das dämonisierte Modell benötigt vom Zeitpunkt der Erstellung des Prozesses bis zur Rückgabe der Antwort an den Benutzer mehr Zeit. Die klassische Anwendung ist für die schnelle Erstellung, Verarbeitung und Zerstörung optimiert.

Alle nachfolgenden Anforderungen nach dem Aufwärmen des Codes sind jedoch viel schneller. Das Framework, die Anwendung und der Container befinden sich bereits im Speicher und sind bereit, Anforderungen anzunehmen und schnell zu antworten.

Probleme des langlebigen Modells


Trotz der Vorteile weist das Modell eine Reihe von Einschränkungen auf.

Speicherlecks. Die Anwendung liegt sehr lange im Speicher, und wenn Sie die "Kurven" der Bibliothek, die falschen Abhängigkeiten oder globalen Zustände verwenden, beginnt der Speicher zu lecken. Irgendwann wird ein schwerwiegender Fehler angezeigt, der die Anforderung des Benutzers unterbricht.

Das Problem wird auf zwei Arten gelöst.

  • Schreiben Sie genauen Code und verwenden Sie bewährte Bibliotheken.
  • Arbeiter aktiv überwachen. Wenn Sie den Verdacht haben, dass im Prozess Speicher verloren geht, ändern Sie ihn proaktiv in ein Analogon mit einer Untergrenze, dh einfach in eine neue Kopie, bei der es noch nicht gelungen ist, ungereinigten Speicher anzusammeln.

Datenlecks . Wenn wir beispielsweise während einer eingehenden Anforderung den aktuellen Benutzer des Systems in einer globalen Variablen speichern und vergessen, diese Variable nach der Anforderung zurückzusetzen, besteht die Möglichkeit, dass der nächste Benutzer des Systems versehentlich Zugriff auf Daten erhält, die er nicht sehen sollte.

Das Problem wird auf der Ebene der Anwendungsarchitektur gelöst.

  • Speichern Sie einen aktiven Benutzer nicht in einem globalen Kontext. Alle für den Anforderungskontext spezifischen Daten werden vor der nächsten Anforderung verworfen und gelöscht.
  • Behandeln Sie Sitzungsdaten sorgfältig. Sitzungen in PHP - mit dem klassischen Ansatz ist dies ein globales Objekt. Wickeln Sie es richtig ein, damit es bei nachfolgender Anforderung zurückgesetzt wird.

Ressourcenmanagement .

  • Überwachen Sie die Verbindungen zur Datenbank. Wenn die Anwendung ein oder zwei Monate im Speicher bleibt, wird die offene Verbindung höchstwahrscheinlich innerhalb dieser Zeit geschlossen: Die Datenbank wird neu installiert, neu gestartet oder die Firewall setzt die Verbindung zurück. Ziehen Sie auf Codeebene in Betracht, die Verbindung erneut herzustellen, oder setzen Sie die Verbindung nach jeder Anforderung zurück und lösen Sie sie bei der nächsten Anforderung erneut.
  • Vermeiden Sie langlebige Dateisperren. Wenn Ihr Mitarbeiter einige Informationen in eine Datei schreibt, gibt es kein Problem. Wenn diese Datei jedoch geöffnet ist und gesperrt ist, hat kein anderer Prozess in Ihrem System Zugriff darauf, bis die Sperre aufgehoben wird.


Entdecken Sie das langlebige Modell


Stellen Sie sich ein langlebiges Arbeitsmodell vor, das eine Anwendung dämonisiert, und suchen Sie nach Möglichkeiten, sie zu implementieren.

Nicht blockierender Ansatz


Wir verwenden asynchrones PHP - wir laden die Anwendung einmal in den Speicher und verarbeiten eingehende HTTP-Anforderungen innerhalb der Anwendung. Jetzt sind die Anwendung und der Server ein Prozess . Wenn die Anfrage eintrifft, erstellen wir eine separate Coroutine oder geben in der Ereignisschleife ein Versprechen, verarbeiten es und geben es dem Benutzer.



Der unbestreitbare Vorteil des Ansatzes ist die maximale Leistung. Es ist auch möglich, interessante Tools zu verwenden, z. B. WebSocket direkt in Ihrer Anwendung zu konfigurieren .

Der Ansatz erhöht jedoch die Komplexität der Entwicklung erheblich. Es ist erforderlich, ELDO zu installieren. Beachten Sie, dass nicht alle Datenbanktreiber unterstützt werden und die PDO-Bibliothek ausgeschlossen ist.

Um Probleme im Falle einer Dämonisierung mit einem nicht blockierenden Ansatz zu lösen, können Sie bekannte Tools verwenden: ReactPHP , Amphp und Swoole - eine interessante Entwicklung in Form einer C-Erweiterung. Diese Tools arbeiten schnell, haben eine gute Community und eine gute Dokumentation.

Blockierungsansatz


Wir erhöhen keine Coroutinen innerhalb der Anwendung, sondern von außen.



Wir greifen nur einige Anwendungsprozesse auf , wie es PHP-FPM tun würde. Anstatt diese Anforderungen in Form eines Prozessstatus zu übertragen, liefern wir sie von außen in Form eines Protokolls oder einer Nachricht.

Wir schreiben denselben Single-Threaded-Code , den wir kennen, wir verwenden dieselben Bibliotheken und dasselbe PDO. Die ganze harte Arbeit mit Sockets, HTTP und anderen Tools wird außerhalb der PHP-Anwendung erledigt.

Von den Minuspunkten: Wir müssen den Speicher überwachen und uns daran erinnern, dass die Kommunikation zwischen zwei verschiedenen Prozessen nicht kostenlos ist , sondern dass wir Daten übertragen müssen. Dies verursacht einen leichten Overhead.

Um das Problem zu lösen, gibt es bereits ein PHP-RM-Tool , das in PHP geschrieben ist. In der ReactPHP-Bibliothek ist eine Integration mit mehreren Frameworks möglich . PHP-PM ist jedoch sehr langsam, es verliert Speicher auf Serverebene und unter Last zeigt es nicht so viel Wachstum wie PHP-FRM.

Wir schreiben unseren Anwendungsserver


Wir haben unseren Anwendungsserver geschrieben , der PHP-RM ähnelt, aber es gibt mehr Funktionen. Was wollten wir vom Server?

Mit vorhandenen Frameworks kombinieren. Wir wünschen uns eine flexible Integration mit fast allen Frameworks auf dem Markt. Ich habe keine Lust, ein Tool zu schreiben, das nur in einem bestimmten Fall funktioniert.

Unterschiedliche Prozesse für Server und Anwendung . Möglichkeit eines Hot-Neustarts, sodass Sie bei der lokalen Entwicklung F5 drücken und den neuen aktualisierten Code anzeigen sowie einzeln erweitern können.

Hohe Geschwindigkeit und Stabilität . Wir schreiben noch einen HTTP-Server.

Einfache Erweiterbarkeit . Wir wollen den Server nicht nur als HTTP-Server verwenden, sondern auch für einzelne Szenarien wie einen Warteschlangenserver oder einen gRPC-Server.

Arbeiten Sie nach Möglichkeit sofort: Windows, Linux, ARM-CPU.

Möglichkeit zum Schreiben sehr schneller Multithread-Erweiterungen, die für unsere Anwendung spezifisch sind.

Wie Sie bereits verstanden haben, werden wir in Golang schreiben.

RoadRunner Server


Um einen PHP-Server zu erstellen, müssen Sie 4 Hauptprobleme lösen:

  • Stellen Sie die Kommunikation zwischen Golang- und PHP-Prozessen her.
  • Prozessmanagement: Schaffung, Zerstörung und Überwachung von Arbeitnehmern.
  • Aufgaben ausgleichen - effiziente Verteilung von Aufgaben an die Arbeitnehmer. Da wir ein System implementieren, das einen einzelnen Mitarbeiter für eine bestimmte eingehende Aufgabe blockiert, ist es wichtig, ein System zu erstellen, das schnell anzeigt, dass der Prozess die Arbeit beendet hat und bereit ist, die nächste Aufgabe anzunehmen.
  • HTTP-Stack - Senden von HTTP-Anforderungsdaten an den Worker. Es ist eine einfache Aufgabe, einen eingehenden Punkt zu schreiben, an den der Benutzer eine Anfrage sendet, die an PHP übergeben und zurückgegeben wird.

Varianten der Interaktion zwischen Prozessen


Lösen wir zunächst das Kommunikationsproblem zwischen Golang- und PHP-Prozessen. Wir haben mehrere Möglichkeiten.

Einbetten: Einbetten eines PHP-Interpreters direkt in Golang. Dies ist möglich, erfordert jedoch eine benutzerdefinierte PHP-Assembly, eine komplexe Konfiguration und einen gemeinsamen Prozess für den Server und PHP. Wie in go-php zum Beispiel, wo der PHP-Interpreter in Golang integriert ist.

Shared Memory - Die Verwendung von Shared Memory Space, bei dem Prozesse diesen Speicher gemeinsam nutzen . Es erfordert mühsame Arbeit. Beim Datenaustausch müssen Sie den Status manuell synchronisieren, und die Anzahl der möglicherweise auftretenden Fehler ist recht groß. Shared Memory hängt auch vom Betriebssystem ab.

Schreiben Sie Ihr Transportprotokoll - Goridge


Wir sind einen einfachen Weg gegangen, der in fast allen Lösungen auf Linux-Systemen verwendet wird - wir haben das Transportprotokoll verwendet. Es wird über die Standard-PIPES- und UNIX / TCP-SOCKETS geschrieben .

Es hat die Fähigkeit, Daten in beide Richtungen zu übertragen, Fehler zu erkennen, Anforderungen zu kennzeichnen und Header darauf zu setzen. Eine wichtige Nuance für uns ist die Fähigkeit, das Protokoll ohne Abhängigkeiten sowohl von PHP als auch von Golang zu implementieren - ohne C-Erweiterungen in einer reinen Sprache.

Wie bei jedem Protokoll ist die Grundlage ein Datenpaket. In unserem Fall hat das Paket einen festen Header von 17 Bytes.



Das erste Byte wird zugewiesen, um den Pakettyp zu bestimmen. Dies kann ein Stream oder ein Flag sein, das die Art der Datenserialisierung angibt. Dann packen wir die Datengröße zweimal in Little Endian und Big Endian. Wir verwenden dieses Erbe, um Übertragungsfehler zu erkennen. Wenn wir feststellen, dass die Größe der gepackten Daten in zwei verschiedenen Reihenfolgen nicht übereinstimmt, ist höchstwahrscheinlich ein Datenübertragungsfehler aufgetreten. Dann werden die Daten übertragen.



In der dritten Version des Pakets werden wir ein solches Erbe beseitigen, einen klassischeren Ansatz mit einer Prüfsumme einführen und die Möglichkeit hinzufügen, dieses Protokoll mit asynchronen PHP-Prozessen zu verwenden.

Um das Protokoll in Golang und PHP zu implementieren, verwendeten wir Standardtools.

Auf Golang: Codierungs- / Binärbibliotheken sowie Io- und Netzbibliotheken für die Arbeit mit Standardpipes und UNIX / TCP-Sockets.

In PHP: die bekannte Funktion zum Arbeiten mit binären Daten packen / entpacken und die Erweiterungsströme und Sockets für Pipes und Sockets.

Bei der Implementierung trat ein interessanter Nebeneffekt auf. Wir haben es in die Standard-Golang-Netz- / RPC-Bibliothek integriert, mit der wir den Servicecode von Golang direkt in der Anwendung aufrufen können.

Wir schreiben einen Service:

//  sample type  struct{} // Hi returns greeting message. func (a *App) Hi(name string, r *string) error { *r = fmt.Sprintf("ll, %s!", name) return nil } 

Mit einer kleinen Menge Code rufen wir es aus der Anwendung auf:

 <?php use Spiral\Goridge; require "vendor/autoload.php"; $rpc = new Goridge\RPC( new Goridge\SocketRelay("127.0.0.1", 6001) ); echo $rpc->call("App.Hi", "Antony"); 

PHP Process Manager


Der nächste Teil des Servers ist die Verwaltung der PHP-Mitarbeiter.


Worker ist ein PHP-Prozess, den wir ständig von Golang aus beobachten. Wir erfassen das Fehlerprotokoll in der STDERR-Datei, kommunizieren mit dem Mitarbeiter über das Goridge-Transportprotokoll und erfassen Statistiken zum Speicherverbrauch, zur Ausführung von Aufgaben und zum Blockieren.

Die Implementierung ist einfach - dies ist die Standardfunktionalität von os / exec, runtime, sync, atomic. Um Arbeiter zu schaffen, verwenden wir Worker Factory .


Warum Arbeiterfabrik? Weil wir sowohl über Standardrohre als auch über Muffen kommunizieren möchten. In diesem Fall unterscheidet sich der Initialisierungsprozess geringfügig. Wenn Sie einen Worker erstellen, der per Pipe kommuniziert, können Sie ihn sofort erstellen und Daten direkt senden. Bei Sockets müssen Sie einen Worker erstellen, warten, bis er das System erreicht, einen PID-Handshake durchführen und erst dann weiterarbeiten.

Task Balancer


Der dritte Teil des Servers ist für die Leistung am wichtigsten.

Für die Implementierung verwenden wir die Standard-Golang-Funktionalität - einen gepufferten Kanal . Insbesondere erstellen wir mehrere Worker und fügen sie als LIFO-Stack in diesen Kanal ein.

Nach dem Empfang von Aufgaben vom Benutzer senden wir eine Anfrage an den LIFO-Stapel und fordern die Ausgabe des ersten freien Arbeiters an. Wenn der Worker für eine bestimmte Zeit nicht zugeordnet werden kann, erhält der Benutzer einen Fehler vom Typ "Timeout-Fehler". Wenn der Worker zugewiesen ist - er wird vom Stapel abgerufen, blockiert, danach erhält er die Aufgabe vom Benutzer.

Nachdem die Aufgabe verarbeitet wurde, wird die Antwort an den Benutzer zurückgegeben, und der Worker steht am Ende des Stapels. Er ist bereit, die nächste Aufgabe erneut auszuführen.

Wenn ein Fehler auftritt, erhält der Benutzer einen Fehler, da der Worker zerstört wird. Wir bitten Worker Pool und Worker Factory, einen identischen Prozess zu erstellen und ihn auf dem Stapel zu ersetzen. Auf diese Weise kann das System auch bei schwerwiegenden Fehlern arbeiten, indem einfach Analogien zu PHP-FPM neu erstellt werden.


Infolgedessen stellte sich heraus, dass ein kleines System implementiert wurde, das sehr schnell funktioniert - 200 ns für die Arbeitszuweisung . Es kann auch bei schwerwiegenden Fehlern funktionieren. Jeder Mitarbeiter verarbeitet zu einem bestimmten Zeitpunkt nur eine Aufgabe, sodass wir den klassischen Blockierungsansatz verwenden können .

Proaktive Überwachung


Ein separater Teil sowohl des Prozessmanagers als auch des Task Balancers ist das proaktive Überwachungssystem.


Dies ist ein System, das einmal pro Sekunde Mitarbeiter abfragt und Indikatoren überwacht: Es untersucht, wie viel Speicher sie verbrauchen, wie viel sie sich befinden, ob sie inaktiv sind. Zusätzlich zur Nachverfolgung überwacht das System Speicherlecks. Wenn der Arbeiter einen bestimmten Grenzwert überschreitet, werden wir ihn sehen und vorsichtig aus dem System entfernen, bevor ein schwerwiegendes Leck auftritt.

HTTP-Stack


Der letzte und einfache Teil.

Wie wird es umgesetzt:

  • erhöht einen HTTP-Punkt auf der Golang-Seite;
  • wir erhalten eine Anfrage;
  • in das PSR-7-Format konvertieren;
  • Senden Sie die Anfrage an den ersten freien Mitarbeiter.
  • Entpacken Sie die Anforderung in ein PSR-7-Objekt.
  • wir verarbeiten;
  • Wir generieren die Antwort.

Für die Implementierung haben wir die Standard- Golang NET / HTTP-Bibliothek verwendet . Dies ist eine berühmte Bibliothek mit vielen Erweiterungen. Kann sowohl über HTTPS als auch über das HTTP / 2-Protokoll arbeiten.

Auf der PHP-Seite haben wir den PSR-7-Standard verwendet . Es ist ein unabhängiges Framework mit vielen Erweiterungen und Middlewares. Der PSR-7 ist unveränderlich im Design , was gut zum Konzept langlebiger Anwendungen passt und globale Abfragefehler vermeidet.

Beide Strukturen in Golang und PSR-7 sind ähnlich, was erheblich Zeit für die Zuordnung einer Anforderung von einer Sprache zu einer anderen spart.

Zum Starten des Servers ist eine Mindestbindung erforderlich:

 http: address: 0.0.0.0:8080 workers: command: "php psr-worker.php" pool: numWorkers: 4 

Darüber hinaus kann ab Version 1.3.0 der letzte Teil der Konfiguration weggelassen werden.

Laden Sie die Server-Binärdatei herunter, legen Sie sie im Docker-Container oder im Projektordner ab. Alternativ schreiben wir global eine kleine Konfigurationsdatei, die beschreibt, welchen Pod wir hören werden, welcher Worker der Einstiegspunkt ist und wie viele benötigt werden.

Auf der PHP-Seite schreiben wir eine primäre Schleife, die eine PSR-7-Anforderung empfängt, verarbeitet und eine Antwort oder einen Fehler an den Server zurückgibt.

 while ($req = $psr7->acceptRequest()) { try { $resp = new \Zend\Diactoros\Response(); $resp->getBody()->write("hello world"); $psr7->respond($resp); } catch (\Throwable $e) { $psr7->getWorker()->error((string)$e); } } 

Montage Um den Server zu implementieren, haben wir eine Architektur mit einem Komponentenansatz gewählt. Dies ermöglicht es, den Server für die Anforderungen des Projekts zusammenzustellen und einzelne Teile je nach den Anforderungen der Anwendung hinzuzufügen oder zu entfernen.

 func main() { rr.Container.Register(env.ID, &env.Service{}) rr.Container.Register(rpc.ID, &rpc.Service{}) rr.Container.Register(http.ID, &http.Service{}) rr.Container.Register(static.ID, &static.Service{}) rr.Container.Register(limit.ID, &limit.Service{} // you can register additional commands using cmd.CLI rr.Execute() } 

Anwendungsfälle


Berücksichtigen Sie die Optionen für die Verwendung des Servers und die Änderung der Struktur. Betrachten Sie zunächst die klassische Pipeline - die Arbeit des Servers mit Anforderungen.

Modularität


Der Server empfängt die Anforderung an einen HTTP-Punkt und leitet sie über eine Reihe von Middleware weiter, die in Golang geschrieben sind. Eine eingehende Anforderung wird in eine Aufgabe konvertiert, die der Mitarbeiter versteht. Der Server gibt die Aufgabe an den Worker weiter und gibt sie zurück.



Gleichzeitig kommuniziert der Mitarbeiter mithilfe des Goridge-Protokolls mit dem Server, überwacht dessen Status und überträgt Daten an ihn.

Middleware auf Golang: Autorisierung


Dies ist das erste, was zu tun ist. In unserer Anwendung haben wir Middleware geschrieben, um den Benutzer per JWT-Token zu autorisieren . Middleware wird für jede andere Art von Autorisierung auf die gleiche Weise geschrieben. Eine sehr banale und einfache Implementierung ist das Schreiben eines Ratenbegrenzers oder Leistungsschalters.



Die Autorisierung ist schnell . Wenn die Anforderung nicht gültig ist, senden Sie sie einfach nicht an die PHP-Anwendung und verschwenden Sie keine Ressourcen für die Verarbeitung nutzloser Aufgaben.

Überwachung


Der zweite Anwendungsfall. Wir können das Überwachungssystem direkt in Golang Middleware integrieren. Zum Beispiel Prometheus, um Statistiken über die Geschwindigkeit der Antwortpunkte und die Anzahl der Fehler zu sammeln.



Sie können die Überwachung auch mit anwendungsspezifischen Metriken kombinieren (standardmäßig mit 1.4.5 verfügbar). Zum Beispiel können wir die Anzahl der Anforderungen an die Datenbank oder die Anzahl der verarbeiteten spezifischen Anforderungen an den Golang-Server und dann an Prometheus senden.

Verteilte Verfolgung und Protokollierung


Wir schreiben Middleware mit einem Prozessmanager. Insbesondere können wir eine Verbindung zum Echtzeitsystem herstellen, um Protokolle zu überwachen und alle Protokolle in einer zentralen Datenbank zu sammeln , was beim Schreiben verteilter Anwendungen hilfreich ist.



Wir können Anfragen auch markieren , ihnen eine bestimmte ID geben und diese ID an alle nachgeschalteten Dienste oder Kommunikationssysteme zwischen ihnen weitergeben. Infolgedessen können wir eine verteilte Ablaufverfolgung erstellen und sehen, wie die Anwendungsprotokolle ablaufen.

Notieren Sie Ihren Abfrageverlauf


Dies ist ein kleines Modul, das alle eingehenden Anforderungen aufzeichnet und in einer externen Datenbank speichert. Mit dem Modul können Sie Anforderungen im Projekt wiederholen und ein automatisches Testsystem, ein Lasttestsystem oder einfach nur die API überprüfen.



Wie haben wir das Modul implementiert?

Wir bearbeiten einen Teil der Anfragen für Golang . Wir schreiben Middleware in Golang und können einen Teil der Anfragen an Handler senden, der ebenfalls in Golang geschrieben ist. Wenn ein Punkt in der Anwendung die Leistung beeinträchtigt, schreiben wir ihn in Golang um und ziehen den Stapel von einer Sprache in eine andere.



Wir schreiben einen WebSocket-Server . Die Implementierung eines WebSocket-Servers oder eines Push-Benachrichtigungsservers wird zu einer trivialen Aufgabe.

  • Golang-Service auf Serverebene.
  • Für die Kommunikation verwenden wir Goridge.
  • Dünne Serviceschicht in PHP.
  • Wir implementieren den Benachrichtigungsserver.

Wir erhalten eine Anfrage und stellen eine WebSocket-Verbindung her. Wenn die Anwendung eine Art Benachrichtigung an den Benutzer senden muss, startet sie diese Nachricht über das RPC-Protokoll an den WebSocket-Server.



Verwalten Sie Ihre PHP-Umgebung. Beim Erstellen eines Worker Pools hat RoadRunner die volle Kontrolle über den Status von Umgebungsvariablen und ermöglicht es Ihnen, diese nach Belieben zu ändern. Wenn wir eine große verteilte Anwendung schreiben, können wir eine einzige Quelle für Konfigurationsdaten verwenden und diese als System zur Konfiguration der Umgebung verbinden. Wenn wir eine Reihe von Diensten auslösen, klopfen alle diese Dienste auf ein einziges System, konfigurieren und funktionieren dann. Dies kann die Bereitstellung erheblich vereinfachen und ENV-Dateien entfernen.



Interessanterweise sind die im Worker verfügbaren env-Variablen im System nicht global. Dies verbessert die Containersicherheit geringfügig.

Integration der Golang-Bibliothek in PHP


Wir haben diese Option auf der offiziellen Website von RoadRunner verwendet . Dies ist eine Integration einer fast vollwertigen Datenbank mit der Volltextsuche BleveSearch innerhalb des Servers.



Wir haben die Dokumentationsseiten indiziert: Wir haben sie in Bolt DB platziert und anschließend eine Volltextsuche ohne eine echte Datenbank wie MySQL und ohne einen Suchcluster wie Elasticsearch durchgeführt. Das Ergebnis war ein kleines Projekt, bei dem einige Funktionen in PHP ausgeführt werden, die Suche jedoch in Golang.

Lambda-Funktionen implementieren


Sie können weiter gehen und die HTTP-Schicht vollständig entfernen. In diesem Fall ist die Implementierung von beispielsweise Lambda-Funktionen eine einfache Aufgabe.



Für die Implementierung verwenden wir die AWS- Standardlaufzeit für die Lambda-Funktion. Wir schreiben eine kleine Bindung, schneiden die HTTP-Server vollständig aus und senden die Daten im Binärformat an die Arbeiter. Wir haben auch Zugriff auf die Umgebungseinstellungen, mit denen wir Funktionen schreiben können, die direkt über das Amazon-Administrationsfenster konfiguriert werden.

Die Mitarbeiter sind während der gesamten Lebensdauer des Prozesses im Speicher, und die Lambda-Funktion bleibt nach der ersten Anforderung 15 Minuten lang im Speicher. Zu diesem Zeitpunkt wird der Code nicht geladen und reagiert schnell. In synthetischen Tests haben wir bis zu 0,5 ms pro eingehender Anfrage erhalten .

gRPC für PHP


Die schwierigere Option besteht darin, die HTTP-Schicht durch die gRPC-Schicht zu ersetzen. Dieses Paket ist auf GitHub verfügbar .


Wir können alle eingehenden Protobuf-Anfragen vollständig an eine untergeordnete PHP-Anwendung weiterleiten, dort können sie entpackt, verarbeitet und zurück beantwortet werden. Wir können Code sowohl in PHP als auch in Golang schreiben und Funktionen kombinieren und von einem Stapel auf einen anderen übertragen. Der Dienst unterstützt Middleware. Sowohl Standalone-Anwendung als auch in Verbindung mit HTTP können funktionieren.

Warteschlangenserver


Die letzte und interessanteste Option ist die Implementierung des Warteschlangenservers .


Auf der PHP-Seite erhalten wir lediglich eine binäre Nutzlast, entpacken sie, erledigen die Arbeit und teilen dem Server den Erfolg mit. Auf der Golang-Seite sind wir voll und ganz mit der Verwaltung der Verbindungen zu Maklern beschäftigt. Es kann RabbitMQ, Amazon SQS oder Beanstalk sein.

Auf der Golang-Seite implementieren wir die „ würdevolle Abschaltung“ der Arbeiter. Wir können wunderbar auf die Implementierung der „dauerhaften Verbindung“ warten. Wenn die Verbindung zum Broker unterbrochen wird, wartet der Server eine Weile mit der „Back-Off-Strategie“, hebt die Verbindung auf und die Anwendung bemerkt sie nicht einmal.

Wir können diese Anfragen sowohl in PHP als auch in Golang verarbeiten und auf beiden Seiten in die Warteschlange stellen:

  • von PHP über das Goridge-Protokoll Goridge RPC;
  • aus Golang - Kommunikation mit der SDK-Bibliothek.

Wenn die Nutzlast sinkt, fällt nicht der gesamte Verbraucher, sondern nur ein separater Prozess. Das System löst es sofort aus, die Aufgabe wird an den nächsten Mitarbeiter gesendet. Auf diese Weise können Sie Aufgaben ohne Unterbrechung ausführen.

Wir haben einen der Broker direkt im Serverspeicher implementiert und die Golang-Funktionalität verwendet. Auf diese Weise können wir eine Anwendung mithilfe von Warteschlangen schreiben, bevor wir den endgültigen Stapel auswählen. Wir heben die Anwendung lokal auf, starten sie und haben Warteschlangen, die im Speicher arbeiten und sich genauso verhalten wie RabbitMQ, Amazon SQS oder Beanstalk.

Wenn Sie zwei Sprachen in einem solchen Hybridpaket verwenden, sollten Sie sich daran erinnern, wie Sie diese trennen.

Separate Domain-Domains


Golang ist eine Multithread- und schnelle Sprache, die zum Schreiben von Infrastrukturlogik sowie Benutzerüberwachungs- und Autorisierungslogik geeignet ist.

Es ist auch nützlich, um benutzerdefinierte Treiber für den Zugriff auf Datenquellen zu implementieren. Dies sind Warteschlangen, z. B. Kafka, Cassandra.

PHP ist eine großartige Sprache zum Schreiben von Geschäftslogik.

Dies ist ein gutes System für HTML-Rendering, ORM und die Arbeit mit der Datenbank.

Werkzeugvergleich


Vor einigen Monaten verglich Habré PHP-FPM, PHP-PM, React-PHP, Roadrunner und andere Tools. Der Benchmark wurde für ein Projekt mit echtem Symfony 4 durchgeführt.

RoadRunner unter Last zeigt gute Ergebnisse und liegt vor allen Servern. Im Vergleich zu PHP-FPM ist die Leistung 6-8 mal höher.


Im gleichen Benchmark hat RoadRunner keine einzige Anfrage verloren, alles wurde zu 100% ausgearbeitet. Leider hat React-PHP unter Last 8 bis 9 Anfragen verloren - dies ist nicht akzeptabel. Wir möchten, dass der Server nicht abstürzt und stabil funktioniert.


Seit der Veröffentlichung von RoadRunner im öffentlichen Zugriff auf GitHub haben wir mehr als 30.000 Installationen erhalten. Die Community hat uns dabei geholfen, eine Reihe spezifischer Erweiterungen und Verbesserungen zu schreiben und zu glauben, dass die Lösung das Recht auf Leben hat.

RoadRunner ist gut, wenn Sie die Anwendung erheblich beschleunigen möchten , aber noch nicht bereit sind, in asynchrones PHP einzusteigen . Dies ist ein Kompromiss, der einen gewissen Aufwand erfordert, jedoch nicht so wichtig ist wie ein vollständiges Umschreiben der Codebasis.

Nehmen Sie RoadRunner, wenn Sie mehr Kontrolle über den PHP-Lebenszyklus haben möchten, wenn nicht genügend PHP-Funktionen vorhanden sind, z. B. für das Warteschlangensystem oder Kafka, und wenn Ihre beliebte Golang-Bibliothek Ihr Problem löst, das nicht in PHP enthalten ist, und das Schreiben Zeit braucht, die Sie auch nicht haben.

Zusammenfassung


Was wir bekommen haben, indem wir diesen Server geschrieben und in unserer Produktionsinfrastruktur verwendet haben.

  • Sie erhöhten die Reaktionsgeschwindigkeit der Anwendungspunkte im Vergleich zu PHP-FPM um das Vierfache .
  • 502 Fehler unter Last vollständig beseitigt . Bei Spitzenlasten wartet der Server nur etwas länger und reagiert, als ob keine Lasten vorhanden wären.
  • Nach der Optimierung von Speicherlecks bleiben Mitarbeiter bis zu 2 Monate im Speicher . Dies ist beim Schreiben verteilter Anwendungen hilfreich, da alle Anforderungen zwischen Diensten bereits auf Socket-Ebene zwischengespeichert sind.
  • Wir verwenden Keep-Alive. Dies beschleunigt die Kommunikation zwischen einem verteilten System erheblich.
  • Innerhalb der realen Infrastruktur haben wir alles in den Alpine Docker in Kubernetes gestellt . Das Bereitstellungs- und Erstellungssystem des Projekts ist jetzt einfacher. Sie müssen lediglich einen benutzerdefinierten RoadRunner-Build für das Projekt erstellen, ihn in das Docker-Projekt einfügen, das Docker-Image ausfüllen und dann unseren Pod ruhig auf Kubernetes hochladen.
  • Entsprechend dem tatsächlichen Zeitpunkt eines der Projekte für einzelne Punkte, die keinen Zugriff auf die Datenbank haben, beträgt die durchschnittliche Antwortzeit 0,33 ms .

Die nächste Fachkonferenz für PHP-Entwickler PHP Russia erst nächstes Jahr. Im Moment bieten wir Folgendes an:

  • Achten Sie auf GolangConf, wenn Sie sich für den Go-Teil interessieren und weitere Details erfahren oder Argumente für den Wechsel zu dieser Sprache hören möchten. Wenn Sie bereit sind, Ihre Erfahrungen zu teilen, senden Sie bitte Abstracts .
  • Nehmen Sie an HighLoad ++ in Moskau teil. Wenn für Sie alles wichtig ist, was mit hoher Leistung zu tun hat , reichen Sie vor dem 7. September einen Bericht ein oder buchen Sie ein Ticket.
  • Abonnieren Sie den Newsletter und den Telegrammkanal, um früher als andere eine Einladung zu PHP Russia 2020 zu erhalten.

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


All Articles