PHP und Apache – die verbreitetste Technologie, um Web-Anwendungen umzusetzen. Nicht selten stecken beide Tools viel Kritik bezüglich ihrer Performance ein. Doch auch bezüglich des Sprachendesigns muss vor allem PHP viel hinnehmen. In diesem Blog-Eintrag werden die Hauptkritikpunkte zusammengefasst. Im großen und ganzen kann man das Ökosystem um PHP als eine Sammlung von inkonsistenten 1:1-Wrappern zu C-Bibliotheken bezeichnen.
Dem entgegen stellt sich Node.js auf. Node.js ist ein asynchrones Kommunikations-Framework, das auf der JavaScript-Engine V8 von Google aufbaut, das oft für seine gute Performance und das gute Design gelobt wird.
Synchron vs Asynchron
Doch um den Unterschied zwischen den Kommunikationsmodellen von PHP und Node.js zu erläutern, muss man etwas tiefer einsteigen: was ist der Unterschied zwischen synchroner und asynchroner I/O?
Bei synchroner oder auch blockierender I/O bewirkt ein lesender Befehl, dass das Programm so lange einfriert, bis die zu lesenden Daten da sind. Die CPU verbringt also die meiste Zeit mit dem Warten auf Daten, aber nicht mit dem Rechnen, obwohl vielleicht einige Sachen noch gerechnet werden könnten. Damit ein Webserver trotz blockierender I/O auch noch andere Anfragen beantworten kann, startet der Server einfach für jede Anfrage einen eigenen Thread. Es wird viel Arbeitsspeicher mit auf Daten wartenden Programmen belegt.
Asynchrone Kommunikation dagegen verspricht eine bessere Nutzung der Ressourcen: Nach einem Lesebefehl sind die Daten noch nicht da, aber das Programm läuft weiter. Es können weitere Lesebefehle abgesetzt werden. Erst, wenn alles getan wurde, was getan werden muss, wird gewartet. Im günstigsten Fall sind zu diesem Augenblick sogar schon die ersten Daten da und es kann mit dem Verarbeiten angefangen werden. Des weiteren können mehrere gleichzeitig ankommende Lesebefehle vom Betriebssystem besser verarbeitet werden, da zum Beispiel Linux Zugriffe auf Festplatten umsortiert, um den Lesekopf der Festplatte so wenig wie möglich zu bewegen.
In Node.js sieht das ganze derart aus, dass man zu einem lesenden Befehl eine Callback-Funktion angibt, die bei Ankunft der Daten ausgelöst werden soll. Über dieses Kommunikationsparadigma lassen sich asynchrone Programme entwickeln, die so viel I/O wie möglich losfeuern, anstatt jeweils auf Daten zu warten.
Der Benchmark
Vom Prinzip her ist das asynchrone I/O-Konzept der synchronen Kommunikation überlegen. Auch der in Node.js verwendeten V8-Engine bescheinigt man die 50-fache Verarbeitungsgeschwindigkeit von JavaScript gegenüber PHP.
Doch abgesehen von diesen punktuellen Messungen und theoretischen Überlegungen – wieviel Vorteil bringt Node.js wirklich? Dazu folgender Versuchsaufbau:
- Eine Beispieldatenbank enthält 2 Tabellen mit jeweils 300 Einträgen, die abgefragt wird
- Ein Script soll zwei SQL-Abfragen losfeuern, alle Daten auswählen und anschließend in HTML-Tabellen rendern
- Im ersten Versuchslauf wird die reine Rechenzeit des Standalone-Scripts mit dem Unix-Befehl time gemessen. Um die Startupzeit des Programms vernachlässigbar klein zu halten, wird der Code 1000 mal in einem Programmaufruf ausgeführt
- Im zweiten Versuchsdurchlauf wird das Script in einem Webserver ausgeführt und dieser Server wird mit Anfragen bombardiert
Der Code
Ansehen für Node.js mit mysql-native (bessere Code-lesbarkeit)
Die dazugehörige Beispieldatenbank kann man sich ebenfalls herunterladen.
Die Ergebnisse
Im ersten Versuchsaufbau gibt es schon ein überraschendes Ergebnis:
PHP | Node.js mit mysql | Node.js mit mysql-native |
---|---|---|
real 0m7.410s user 0m3.653s sys 0m2.107s |
real 0m9.771s user 0m6.707s sys 0m1.399s |
real 0m7.533s user 0m5.249s sys 0m0.984s |
Wie man sieht, ist PHP 2 Sekunden eher fertig und hat auch nur halb so viel CPU-Zeit „verbraten“. Nach einem Wechsel des MySQL-Adapters kommt man auf etwa die gleiche Laufzeit wie PHP, allerdings immer noch mit signifikant mehr vergeudeter Rechenzeit.
Der zweite Versuchsaufbau gibt ein noch erschütternderes Bild ab. Mit dem Benchmarktool „ab“ von Apache wurde ein Anfragesturm auf die mit dem Versuchsprogramm eingerichteten Webserver losgelassen. Gearbeitet wurde einerseits mit den Parametern „ab -c 200 -n 20000“ und mit „ab -c 20 -n 2000“. Der Parameter -c steht für die Anzahl gleichzeitiger Anfragen und -n steht für die Anzahl der GET-Requests, die insgesamt gefeuert werden sollen.
Hier performt PHP+Apache 10 mal so schnell wie Node.js. Der Einsatz des mysql-native-Adapters in Node.js verschlechterte das Ergebnis hier sogar noch. Hier das ernüchternde Ergebnis:
Wert | PHP+Apache | Node.js | ||
---|---|---|---|---|
-c 20 | -c 200 | -c 20 | -c 200 | |
Anfragen/s | 1173 | 1146 | 119 | 114 |
Antwortzeit in ms | 17 | 174 | 167 | 1750 |
Durchsatz in MiB/s | 21 | 20 | 2 | 2 |
CPU-Auslastung | 100% auf allen 4 Kernen |
80% Node.js 16% MySQL 16% MySQL |
||
Antwortzeit -c 1 | 9.4 ms | 8.9 ms |
Wie man sieht, haben beide Server etwa gleichbleibenden Durchsatz, egal wieviel Anfragen gleichzeitig ankommen. Ein bisschen merkt man ihnen die Last allerdings an. Die Antwortzeit steigt ebenfalls bei beiden Servern proportional mit der Auslastung.
Da Apache für jede Anfrage einen neuen PHP-Prozess startet, können alle 4 Kerne ausgelastet werden. Node.js hingegen arbeitet den Code nur auf einem Kern ab und benutzt mehrere Threads nur, um blockierende IO-Operationen abzufangen. PHP kann also bis zu 4 mal mehr Ressourcen auslasten als Node.js. Hinzu kommt, dass PHP im ersten Benchmark nur etwa halb so viel CPU-Zeit verbraucht hat bei etwa gleicher Ausführungszeit. Alles in allem erreicht Node.js somit nur ein Zehntel des Durchsatzes von PHP+Apache.
Fazit
Der Faktor 10 ist kein endgültiges Kriterium. Würde man den Node.js-Server replizieren und die Lasten ausbalancieren, könnte man zumindest alle 4 Kerne des Servers auslasten und „nur noch“ 2.5 mal langsamer als PHP+Apache sein. Stecken die Node.js-Entwickler jetzt noch ein bisschen Arbeit in die Mikrooptimierung des Frameworks, lassen sich bestimmt noch bessere Ergebnisse erzielen. Denn ein großer Teil der CPU-Zeit dürfte Node.js für die Umrechnung von Strings in Buffer zu verwenden.
Für praktische Einsatzzwecke, wo es auf den Datendurchsatz ankommt, sollte man allerdings beim bereits bewährten und optimierten PHP bleiben. Vielleicht wendet sich das Blatt, aber bisdahin wird noch viel Wasser die Spree hinunterfließen.
Comments are closed