Der Aufstieg von Hybrid-PHP: die Verbindung von PHP mit Go und Rust
(yekdeveloper.com)- In letzter Zeit rückt ein hybrider Ansatz in den Fokus, bei dem Go und Rust innerhalb einer PHP-Monolith-Struktur als Erweiterungssprachen integriert werden
- Früher wurde mit der Kombination aus Go-Microservices und einem PHP-8.3-Monolithen ein ausgewogenes Verhältnis zwischen Produktivität und hoher Performance erreicht
- Gemäß dem Pareto-Prinzip (80 % des Traffics konzentrieren sich auf 20 % der APIs) war die Optimierung von Hotspot-Endpunkten unverzichtbar; früher wurde darauf mit Caching und der Auslagerung in Go-Services reagiert, was jedoch die Komplexität erhöhte
- Durch die jüngsten Fortschritte im PHP-Ökosystem sind Techniken wie FFI, Rust-Erweiterungen und Go-Erweiterungen (FrankenPHP) entstanden, mit denen sich die Performance innerhalb des Monolithen deutlich steigern lässt
- Rust-Erweiterungen bieten gleichzeitig Speichersicherheit und Geschwindigkeit, und FrankenPHP zeigt im Worker-Mode dank Go-basierter Erweiterungen eine mehr als vierfache Performance-Steigerung
- Ohne die Kosten und Risiken einer Neuschreibung in Go oder Rust in Kauf zu nehmen, lässt sich mit dem Ansatz Hybrid-PHP sowohl Produktivität als auch Geschwindigkeit sichern
Hintergrund und bisherige Architektur
- Bisher wurde rund um eine bestehende DDD-Monolith-Anwendung (mother) für die Optimierung bestimmter Funktionen separat Go-basierte Microservices (children) entwickelt
- Die Go-Microservices übernahmen die Verarbeitung von High-Performance-Traffic, während der PHP-8.3-Monolith in einem kleinen Backend-Team schnelle Feature-Entwicklung und zuverlässige Deployments ermöglichte
- Diese Struktur bot einen ausgewogenen Punkt, an dem sich Geschwindigkeit, Stabilität und Produktivität zugleich erreichen ließen
Performance-Engpässe und bisherige Gegenmaßnahmen
- Häufig wurde das Pareto-Prinzip beobachtet, nach dem sich 80 % des Traffics auf 20 % der API-Endpunkte konzentrieren
- Für diese performancekritischen 20 % wurden verschiedene Maßnahmen eingeführt, darunter optimierter Code, zusätzliche Caching-Schichten und die Auslagerung in Go-Microservices
- Allerdings stieß dieser Ansatz bei Komplexität und Betriebsaufwand an Grenzen
Hybride Optionen im modernen PHP-Ökosystem
- Inzwischen gibt es mehr Technologien, mit denen sich die Performance direkt innerhalb eines PHP-Monolithen verbessern lässt
-
1. FFI (Foreign Function Interface)
- Mit der FFI-Funktion von PHP kann C-Code direkt aus PHP heraus aufgerufen werden
- Auch systemnahe oder performancekritische Logik lässt sich so innerhalb eines PHP-Projekts umsetzen
- Wegen der Kosten für Context Switching wird der Einsatz jedoch nur in passenden Situationen empfohlen
-
2. Rust-basierte Erweiterungen
- PHP-Erweiterungen können in Rust (oder Zig) entwickelt werden
- Stark belastete Bereiche lassen sich an Rust-Erweiterungen mit Speichersicherheit und Compiler-Performance auslagern, wodurch sich Zuverlässigkeit und hohe Geschwindigkeit zugleich erreichen lassen
-
3. Go-basierte Erweiterungen: FrankenPHP
- Nach dem jüngsten Umstieg auf FrankenPHP zeigte sich im worker mode eine mehr als viermal höhere Performance als zuvor
- Mit einem aktuellen Release ist es nun auch möglich, PHP-Erweiterungen in Go zu schreiben
- Dadurch lässt sich die API-Performance von Go direkt innerhalb eines PHP-Monolithen nutzen, sodass sich Produktivität und Geschwindigkeit ohne sprachliche Aufteilung verbinden lassen
Warum keine vollständige Migration nach Go oder Rust?
- Kosten und Risiko einer vollständigen Neuschreibung sind hoch
- Eine bereits große und stabile Anwendung komplett auf Go oder Rust umzustellen, bedeutet erhebliches Risiko und hohen Ressourceneinsatz
- PHP hat weiterhin eigene Stärken
- Für die meisten Anforderungen bleiben die schnelle Entwicklung mit PHP, das zugängliche Ökosystem und die ausreichend hohe Geschwindigkeit wettbewerbsfähig
- Wenn nur die Bereiche mit echten Performance-Grenzen hybrid mit Go oder Rust aufgebaut werden, entfällt die Notwendigkeit einer vollständigen Migration
Fazit: der Wert von Hybrid-PHP
- Das moderne PHP-Ökosystem bietet neben schneller Entwicklungsproduktivität auch Anbindungsoptionen für High-Performance-Erweiterungen in C, Rust und Go
- Mit einer solchen hybriden Struktur lassen sich Geschwindigkeit und Produktivität zugleich sichern
- Damit entsteht ein neues Architekturparadigma, das eine PHP-zentrierte Entwicklung beibehält und bei Bedarf eine selektive sprachbasierte Erweiterung ermöglicht
5 Kommentare
Irgendwie wirkt es so, als würde sich das entwickeln wie bei JavaScript, das sich ebenfalls verändert.
Bei Node.js mit Rust vielleicht noch, aber bei PHP wirkt es auf mich unpraktisch, ständig Dinge wie
$tippen zu müssen. Empfinden Leute, die PHP gut beherrschen, das normalerweise nicht als besonders störend?Fühlt es sich nicht schnell vertraut an, selbst wenn es zunächst unbequem ist, sobald man es eine Weile benutzt?
Der Mensch ist ein anpassungsfähiges Wesen.
Ich habe mich, wenn überhaupt, am Konzept von Variablen/Funktionen in PHP selbst gestört, aber ich habe nie das
$-Schreibschema als störend empfunden.Zu sagen, man könne es wegen des Dollarzeichens nicht benutzen, ~~die Leute mit Dollarzeichen verdienen viel Geld, und weil es nicht der US-Dollar, sondern der Simbabwe-Dollar ist, verdienen sie nicht besonders viel~~ – das waren doch eher scherzhafte Bemerkungen, oder ...
Hacker-News-Kommentar
Ich entwickle zunehmend eine Abneigung gegen Allzweck-Frameworks (Spring, Laravel, Phoenix usw.). Am Anfang sind sie wirklich produktiv, aber in Legacy-Projekten wiederholen sich immer dieselben Probleme. Da sich Infrastrukturumgebung und Business-Anforderungen von Projekt zu Projekt unterscheiden, kann man nie nur der empfohlenen Arbeitsweise des Frameworks folgen, und überall häufen sich zusätzliche Patches oder Abhängigkeits-Tricks. Wenn man dann auf eine neue Sprache oder Version upgraden will, brechen all diese angepassten Teile auseinander. Am Ende aktualisiert niemand mehr etwas, bis es auf der Infrastruktur gar nicht mehr läuft und dann die tränenreiche Großmigration kommt. Mehrere Bibliotheken zu kombinieren und die eigenen Abstraktionen selbst zu bauen, kann zwar mehr Zeit kosten, aber man kann flexibel schrittweise upgraden und schnell die Richtung ändern. Das Go-Ökosystem wirkt in dieser Hinsicht ideal. Anfangs war es ungewohnt, aber inzwischen mag ich diesen Ansatz sogar lieber.
Bei Web-Frameworks ist das Gefühl stark: „Am Anfang großartig, irgendwann nur noch hinderlich.“ Wenn man eine einfache App baut, fühlt sich so etwas wie „Blog in 15 Minuten“ bei Rails fast wie Magie an, aber sobald es komplex wird, wird das Framework eher zum Hindernis. Persönlich finde ich ein „mittleres Level“ an HTTP-Setup wie Express + Node.js oder Vert.x + Java angenehmer.
In Python kann man zwischen Microframeworks (Flask-artig) und Macroframeworks (Django) unterscheiden. Ich wähle immer Django. Flask schlägt fast nichts vor, daher ist jedes Projekt jedes Mal wieder eine neu dekorierte Schneeflocke. Bei Authentifizierung, Templates, Cookies, E-Mail usw. entsteht Entscheidungserschöpfung, weil man aus N Optionen wählen muss. Vor allem werden solche Bibliotheken oft von Einzelpersonen entwickelt, sodass Wartungs- und Sicherheitsqualität stark schwanken. Django dagegen sorgt dafür, dass die meisten Projekte ähnlich aussehen, und liefert fast alle Grundfunktionen sofort mit. Ich habe nur dann einen Grund, Erweiterungsbibliotheken zu nutzen, wenn es spezielle Anforderungen gibt. Weil so viel direkt gepflegt und geprüft ist, halte ich Code-Verlässlichkeit und Sicherheit für höher.
Dass es in Go kein riesiges Framework gibt, liegt daran, dass das Typsystem der Sprache noch ziemlich unvollständig ist. Es ist schwer, komplexe Bibliotheken zu bauen, die gut zusammenpassen. Ich habe ganze 9 Jahre auf Generics gewartet, bevor ich mein erstes Datenbank-Toolkit für Go gebaut habe. Es war erfolgreich, aber ich hatte trotzdem das Gefühl, dass das früher in Java angenehmer war. Wenn man Result-Typen auf andere generische Typen mit map/filter/reduce anwenden könnte, wäre das eine ganz andere Welt. Schon allein Union-Typen würden dafür sorgen, dass man
anynicht mehr verwenden müsste. Selbst Overloading würde den Code deutlich sauberer machen. Das Go-Typsystem muss sich noch weiterentwickeln.In meinem Bereich sind nur Go und Rust nützlich. Eine Kultur rund um meinungsstarke Frameworks passt nicht gut. Rails, Laravel und Django sind meiner Meinung nach hervorragend, wenn relationale DB-CRUD-Arbeit klar im Vordergrund steht.
Ich habe in den letzten 5 Jahren nur PSR-(Php Standards Recommendation)-kompatible Komponenten verwendet und überhaupt keine Frameworks. Der Grund ist, dass große Frameworks auf lange Sicht am Ende doch nicht mehr passen. Es gibt zu viele Einschränkungen, und Pflege sowie Updates werden zu schwer. Ob großes Firmenprojekt oder persönlicher Service: Eine Architektur rund um PSR-Komponenten fühlt sich besser an.
Ich verstehe, dass ein hybrider Ansatz (C, Rust, Go usw. parallel zu PHP) sinnvoll sein kann, wenn die Codebasis so groß ist, dass ein kompletter Rewrite unmöglich ist. Aber wenn es nicht unbedingt nötig ist, ist meine Erfahrung, dass man mit einer C#-API sowohl Entwicklungsgeschwindigkeit als auch Laufzeitleistung bekommt. Bis C++ oder Rust musste ich fast nie gehen. PHP ist auch gut, aber Dinge wie typisierte Arrays gibt es dort noch nicht. Zum Beispiel ist es eine Sprache, die nicht ablehnt, wenn in einem Datumsarray ein String landet.
Ich habe lange mit C# gearbeitet und Erfahrung mit verschiedenen Web-/API-Frameworks. Wenn man sich etwas tiefer mit PHP beschäftigt, merkt man, wie viele eingebaute Grundfunktionen für Webentwicklung es gibt. Es hat Nachteile, aber wenn man schnell etwas bauen muss, ist PHP meiner Meinung nach der Gewinner.
Es stimmt, dass PHP falsche Typen in Arrays zulässt (z. B. einen String in einem Datumsarray). Manchmal tauchen dadurch seltsames Verhalten oder Bugs auf. Ich bin kürzlich auf einen Bug gestoßen, bei dem beim Decodieren mit
json_decode()numerische Schlüssel alsintund andere alsstringhereinkamen, sodass Schlüsseltypen gemischt wurden. Solche Details können ziemlich bizarr sein, aber trotzdem ist PHP selbst eine wirklich attraktive Frankenstein-Sprache.In der Praxis verhindert ein statischer Analysator solche Typfehler normalerweise. Außerdem ist es sehr wahrscheinlich, dass PHP bald Generics-Unterstützung bekommt. Mehr dazu gibt es im Blog von thephp.foundation.
Ich bin der Autor des Artikels, danke fürs Lesen. Tatsächlich muss man nicht alles neu schreiben. Wenn man PHP mit worker-basierten Laufzeiten wie swoole oder frankenphp betreibt, kann man Performance auf Node-Niveau erreichen. Probleme mit typisierten Arrays oder Generics werden von statischen Analysatoren wie phpstan unterstützt, und mit Type-Anmerkungen kann man die Typsicherheit deutlich erhöhen.
Seit dem Support-Ende von VB6 habe ich beschlossen, überhaupt keine Microsoft-Sprachen mehr zu verwenden. Nur Open-Source-Sprachen sind gut für die seelische Gesundheit.
Als ich bei {{company}} angefangen habe, lief unternehmensweit noch PHP 5.4, und damals war die Abneigung gegen PHP enorm. Aber nachdem ich modernes PHP erlebt habe, fühlt es sich in genau dem Moment, in dem wir gerade am Ende unseres Weges weg von PHP angekommen sind, fast so an, als sei diese Migration heute eher ein Rückschritt. Alle beurteilen PHP immer noch nach der 5.x-Ära, dabei ist es heute völlig anders.
Ich sehe das etwas anders. Wenn man auf den Arbeitsmarkt schaut, gibt es immer noch ein bestimmtes Bild von PHP (ob gut oder schlecht), das die Einstellung wirklich guter Entwickler einschränkt. Selbst wenn PHP technisch nicht mehr so schlecht ist wie früher, hat es aus Sicht des Unternehmensbrandings immer noch Sinn, von PHP wegzugehen.
Der Aussage „PHP ist jetzt awesome“ stimme ich nicht zu. Es ist ohne Zweifel besser geworden, aber „awesome“ ist etwas übertrieben.
PHP wird immer besser. Ich denke, ich sollte es mir bald einmal genauer ansehen.
Wir hatten vor 4 Jahren genau dieselbe Diskussion und haben uns am Ende entschieden, auf PHP 8 zu upgraden und dabei zu bleiben. Für unser Team hat diese Entscheidung in den letzten Jahren gut funktioniert.
Pasir ist frankenphp ähnlich, aber auf Rust-Basis, sehr vielversprechend, aber noch in einer frühen Entwicklungsphase.
Pasir github Pasir verwendet auf Rust portierte Zend-API-Bindings.
Zend-API-Rust-Bindings Es gibt auch interessante Experimente wie ngx-php, bei dem PHP über die Zend API direkt in das nginx-Binary eingebettet wird.
ngx-php github workerman setzt auf ein hybrides asio-Backend und erreicht damit eine sehr schnelle Laufzeit.
workerman github
Ich frage mich, ob Pasir, frankenphp usw. auch bestehende PHP-Module unterstützen.
Danke für die Empfehlung, sieht wirklich großartig aus. Aber ich stimme zu, dass es, wie du sagst, noch weit von Production-Reife entfernt ist. Wir haben uns am Ende für frankenphp entschieden, das von der php foundation unterstützt wird.
Die Komplexität von Debugging und Wartung wird oft unterschätzt. Wenn man die Wahl hat, sollte man solche Kombinationen meiner Meinung nach lieber vermeiden.
Ich habe meine App ebenfalls von PHP auf Go umgeschrieben, und für die Firma war das eine erfolgreiche Investition. Ich konnte 20.000 Zeilen PHP auf 4.000 Zeilen Go reduzieren und die Effizienz stark steigern. Wenn man ein PHP-Unternehmen ist, würde ich sogar sehr empfehlen, einen umfassenden Rewrite einzuplanen und dabei auch Tests hinzuzufügen (was in Go einfacher ist). Ich halte das für besser, als sich mit den Wartungsschmerzen eines gemischten Stacks wie Rust/PHP oder Go/PHP herumzuplagen.
Ich frage mich, wie die Zeilenzahl beim Umstieg von PHP auf Go so stark sinken konnte. Ich halte Go für eine sehr wortreiche Sprache. Nach meiner Erfahrung liegt PHP eher im Mittelfeld, Haskell ist am kompaktesten, und Java/Go werden wegen Fehlerbehandlung usw. eher länger.
Dass die Zeilenzahl beim Wechsel von PHP auf Go auf ein Fünftel gesunken sein soll, leuchtet mir nicht ein. PHP hat viele Kurzschreibweisen, Go eher nicht.
Bei Rewrites ist für mich immer wichtig, ob die bessere Performance und Effizienz „an der Sprache lagen oder an Architekturverbesserungen, die bei der Neufassung eingeflossen sind“.
Ich hätte erwartet, dass der Code bei einem Rewrite nach Go wegen des
if err != nil-Musters eher um das Zehnfache wächst. Ich habe einmal einen Rewrite nach Python erlebt, und dort wurde der Code ebenfalls sehr wortreich, außerdem machten Patterns wie Dependency Injection das Testen umständlich.Ich empfehle grundsätzlich keinen Rewrite (obwohl ich selbst zwei erfolgreich abgeschlossen habe, würde ich mich wohl trotzdem nicht bewusst dafür entscheiden). Moderne PHP-Laufzeiten sind inzwischen wirklich schnell und lohnen sich auszuprobieren. Besonders wenn man Task-Caching wie bei swoole gut nutzt, ist es in manchen Fällen genauso schnell wie Go (Benchmarks dazu sollte man sich ansehen).
Manchmal denke ich, wir müssen uns wirklich wieder auf die Grundlagen konzentrieren: Pixel, Daten, Latenz/Bandbreite. Auch das Web ist letztlich ein Optimierungsproblem: „die richtigen Pixel innerhalb der verfügbaren Netzwerkressourcen so schnell sichtbar wie möglich für menschliche Augen zu zeichnen“. Ich finde, man sollte so denken: „Welche Pixel wird der Benutzer gleich sehen?“, „Welche Daten braucht man, um sie zu zeichnen?“, „Welche künftig nützlichen Daten sollte man vorab prefetchen?“
Ich baue alumina-ui mit egui für WASM, und statt komplexes Wissen über HTML, JavaScript, CSS usw. zu brauchen, bekommt man einfach ein Canvas in Browsergröße und rendert direkt per WebGL. Es ist extrem praktisch, in einer Sprache, die ich mag, schnelle, GL-beschleunigte Grafik auszugeben. WASM/WebGL gefällt mir wegen dieser Abstraktion sehr.
Sich nur auf die Pixel zu konzentrieren, die der Nutzer sieht, ist zu kurz gedacht. In Softwareprojekten muss man nicht nur die unmittelbare UX optimieren, sondern auch die Entwicklungszeit. Die Verzögerung bis zum ersten sichtbaren Screen und die tatsächliche Entwicklungszeit stehen keineswegs proportional zueinander.
FrankenPHP wirkt sehr interessant, ist in der Praxis aber etwas eigenartig. Niemand nutzt PHP ohne Module, aber welche PHP-Module FrankenPHP unterstützt, ist nicht klar, und es ist auch unklar, ob man eigene gewünschte Zusatzmodule bauen und hinzufügen kann. Es ist eng mit Caddy verknüpft, und ich kenne diesen Webserver nicht gut und bevorzuge nginx. Es gibt keine Anleitung, daher weiß ich nicht, ob man es unter nginx als Ersatz für php-fpm verwenden kann. Außerdem scheinen die Docker-Images von Caddy oder FrankenPHP nur an Let's-Encrypt-Zertifikate zu denken, sodass es bei eigener SSL-Konfiguration oder reinem HTTP-Betrieb wirklich unintuitiv wird.
Das Modulproblem löst man in der Regel, indem man direkt im Dockerfile selbst baut. Wenn man zum Beispiel das
pgsql-Modul hinzufügen will, installiert man per apt die Abhängigkeiten, installiert das Modul mitdocker-php-ext-installund entfernt bzw. bereinigt danach die Abhängigkeiten wieder. HTTP kann man ebenfalls einfach im Caddyfile konfigurieren, indem man direkt Port 80 öffnet.Beim statischen Build sind standardmäßig Dutzende wichtiger PHP-Module enthalten. Die genaue Modulliste und die Build-Skripte kann man in frankenphp build-static.sh sehen.
Ich frage mich, bei welcher Art von Workload Erweiterungen in Sprachen wie C/Rust/Go wirklich zwingend nötig sind. Dass es solche Fälle gibt, verstehe ich, aber ich würde gern besser verstehen, warum man diese Komplexität dem Stack hinzufügen muss und ob sich das nicht auch anders lösen ließe.
Was ich an PHP am meisten hasse, ist, dass bei jeder HTTP-Anfrage die gesamte Anwendung neu gebootstrapt, Autoloading ausgeführt und Konfigurationen erneut ausgewertet werden. Natürlich gibt es Caches usw., aber verglichen mit einem Modell wie bei Go, bei dem die Engine immer läuft, ergibt das für mich immer noch keinen Sinn.
reactphp.org
php.net ev module
pecl-event
workerman.net
frankenphp worker Dokumentation
Für mich ist genau das eher der größte Vorteil von PHP. Scale-out wird dadurch extrem einfach.
Ich mag diese Struktur eher. Der Zustand bleibt inhärent minimal (bis zu einem gewissen Grad).
Du hast recht, dieser Ansatz ist wirklich miserabel. Besonders problematisch ist es, wenn PHP als eigene Templatesprache verwendet wird. Eigene Templating-Engines oder persistent laufende Laufzeiten, die das beheben sollen, sind am Ende auch nur „einem Schwein Lippenstift auftragen“. Man hätte von Anfang an lieber eine Sprache wählen sollen, deren Name nicht Personal HomePage war.