PHPGangsta - Der praktische PHP Blog

PHP Blog von PHPGangsta


PHP und die lose Typisierung

with 6 comments

Heute mal eine sehr praktische Erfahrung:

Der Kollege nebenan fragte, ob ich mal kurz rübergucken kann, er verstehe die Ausgabe einer Funktion nicht. Vereinfacht sah der Code so aus (über Sinn und Unsinn soll hier nicht diskutiert werden, es ist nur vereinfacht dargestellt):

function show(array $headerArray) {
	foreach ($headerArray as $headerKey => $headerValue) {
		$outputString = $headerKey . ':';
		foreach ($headerValue as $valueKey => $value) {
			if ($valueKey != 'append') {
				$outputString .= $value;
			}
		}
		return $outputString;
	}
}

Die Funktion erwartet ein Array mit Header-Informationen. Diese sollen durchlaufen werden, und die Inhalte sollen als Wertepaare-String zurückgegeben werden, wenn der innere Key nicht „append“ ist.

Soweit ist der Code OK. Jetzt kommt der Aufruf:

$headerArray = array("From" => array(
						0 		=> 'john.doe@some.city',
						'append'=> true			));

echo show($headerArray);

Wie lautet die Ausgabe? Korrekt, nämlich so:

From:

Wenn ich den Aufruf leicht ändere:

$headerArray = array("From" => array(
						'0' 		=> 'john.doe@some.city',
						'append'=> true			));

echo show($headerArray);

Wir nutzen also als Key nun den String ‚0‘. Jetzt sollte das richtige rauskommen. Aber man liegt immernoch falsch:

From:

Da wurde ich stutzig. Man sollte denken, jetzt wird der String ‚0‘ mit dem String ‚append‘ verglichen. Da es ungleich ist, sollte die Email-Adresse ausgegeben werden. Falsch gedacht.

PHP scheint den Key eines Arrays als Integer zu casten wenn möglich. Egal ob wir 0 oder ‚0‘ als Key verwenden, beim Auslesen des Arrays erhalten wir immer einen Integerwert. Das sollte man sich auf der Zunge zergehen lassen.
Als Folge vergleicht er also immer den Integer 0 mit dem String ‚append‘. Damit ein Vergleich möglich ist, castet PHP den String korrekterweise als Integer, und das ergibt ja bekannterweise 0.

Das ursprüngliche Problem lag natürlich beim != . Man hätte das ganze vermeiden können, indem man !== verwendet, dann wäre das Problem nicht aufgetreten, und ich hätte einen Artikel weniger.

Written by Michael Kliewe

Oktober 12th, 2009 at 12:02 pm

Posted in PHP

Tagged with , ,

Client-IP Problem bei Reverse-Proxy-Betrieb

without comments

In einem meiner letzten Artikel schrieb ich ja bereits über Reverse-Proxies. Der Reverse-Proxy nimmt die Verbindung vom Client (Browser) entgegen, dann kann er entweder selbst den Request bedienen (statische Dateien von der lokalen Platte oder aus dem Cache), oder er verbindet sich zu einem der Backend-Webserver, ruft dort die geforderte Datei ab, und sendet sie dem Client zurück.

Ein Problem entsteht nun auf dem Backend-Webserver: Alle Requests kommen vom Reverse-Proxy. Wenn nun in den PHP-Scripten die Client-IP-Adresse verwendet wird, steht darin die IP des Reverse-Proxies.

Betroffen ist in diesem Fall die PHP-Variable  $_SERVER[‚REMOTE_ADDR‘]  als auch das Apache-Log, denn dort taucht auch immer nur die IP des Reverse-Proxy auf.

xforwardedfor3

127.0.0.1 - - [03/Oct/2009:10:45:24 +0200] "GET /phpinfo.php HTTP/1.0" 200 7800 "-" "Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729) FirePHP/0.3"

127.0.0.1 deshalb, da ich direkt auf der Linux-Maschine sowohl den nginx als auch den Apache laufen habe.

Da gibt es natürlich Lösungen. Zuerst einmal müssen wir die Client-IP irgendwie an den Backend-Webserver übergeben. Dafür gibt es den Header „X_FORWARDED_FOR“, da wird der nginx die Client-IP reinschreiben.

Im nginx muss dann folgendes gesetzt werden:

location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
........

Ein phpinfo() liefert dann die korrekte Client-IP in X_FORWARDED_FOR (34 ist der Server, 33 der Client):

xforwardedfor4

Nun installieren wir noch ein Apache-Modul. Dieses Modul sorgt dafür, dass in die Variable $_SERVER[‚REMOTE_ADDR‘]  der Wert aus X-FORWARDED-FOR geschreiben wird, damit wir keine PHP-Scripte anpassen müssen. Außerdem sorgt dieses Modul dafür, dass im Apache-Log dieser Wert auftaucht.

Das Module, das es für diese Aufgabe gibt, lautet „mod_rpaf“. Einfach danach googlen, downloaden und in der Apache-Konfiguration laden. Oder unter Linux:

sudo apt-get install libapache2-mod-rpaf

Noch kurz konfigurieren /etc/apache2/mods-available/rpaf.conf:

<IfModule mod_rpaf.c>
RPAFenable On
RPAFsethostname On
RPAFproxy_ips 127.0.0.1
</IfModule>

Das Ergebnis sieht dann so aus:

xforwardedfor2

192.168.1.33 - - [03/Oct/2009:10:47:23 +0200] "GET /phpinfo.php HTTP/1.0" 200 7808 "-" "Mozilla/5.0 (Windows; U; Windows NT 6.0; de; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3 (.NET CLR 3.5.30729) FirePHP/0.3"

Möchte man das Modul nicht installieren, muß man überall in seinen PHP-Scripten die Variable $_SERVER[‚X_FORWARDED_FOR‘] statt $_SERVER[‚REMOTE_ADDR‘] nutzen, und das Apache-Log anpassen:

LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined

Damit hätten wir das Problem auch gelöst, überall steht nun die Client-IP zur Verfügung, die Anwendungen und Logs laufen wieder korrekt.

Written by Michael Kliewe

Oktober 7th, 2009 at 8:35 am

Algorithmus-Wettbewerb: Shortest Path

with 36 comments

Da eine Diskussion in den Kommentaren aufkam, warum eine Berechnung einer „Browsergame-Weltkarte“ so lang dauert, möchte ich hier dazu aufrufen, euer geballtes Uni-Wissen anzubringen und der ganzen Welt zu zeigen, wie man das Problem effizient und schnell löst.

Ausgangslage: Wir haben eine quadratische Weltkarte, die aus 100*100 Feldern besteht. Da die Spieler von jedem Feld zu jedem Feld ziehen können sollen, müssen kürzeste Wege berechnet werden. Dabei ist zu beachten, dass es einige Felder gibt, die unpassierbar sind. Hier mal ein kleines Schema:

dijkstra1

Wir möchten nun also eine Datenbank erstellen mit allen Wegen (in diesem kleinen Fall wären das bereits 496 verschiedene Wege(31+30+29+….+2+1)). Die Distanz zu einem senkrech oder waagerecht anliegenden Feld beträgt 1. Man kann diagonal gehen, der Weg von 1 nach 8 ist also erlaubt, er beträgt Wurzel 2. In die Datenbank soll der komplette Pfad geschrieben werden, also nicht nur die Länge, sondern auch die Zwischenstationen. Es soll auch jeweils nur ein Weg reingeschrieben werden, der „Rückweg“ braucht nicht eingetragen zu werden. So sparen wir die Hälfte an Speicherplatz.

Die Ergebnis-Tabelle sieht dann so aus:

dijkstra2

Ich habe meinen Algorithmus nochmal rausgekramt, den ich 2006 erstellt habe, und gestern nochmal laufen lassen. Hier die Laufzeit-Ergebnisse von ein paar Durchläufen, damit wir etwas vergleichen können:

dijkstra3

Bei einer 80*80 Karte dauert es also knapp 4 Stunden. Als Info: Ich habe den Dijkstra-Algorithmus unter Verwendung eines Fibonacci-Heaps implementiert. Aufgrund der Laufzeiten dürfte mein Algorithmus irgendwas um O(n²) sein. Eigentlich müßte er bei O(n * log(n) + m) sein, aber naja… Die großen Datenmengen haben mich damals noch dazu gebracht, ein Script hinterher laufen lassen, denn wie man oben sieht sind viele Daten doppelt drin beim Weg, das kann man optimieren. Der Weg von 1 nach 6 besteht aus dem Weg von 1 nach 5 und dann noch den Schritt nach 6. Aber das soll hier nicht gemacht werden.

Hier noch die Ausgangsdaten, die ich genutzt habe bzw. die Details zu den gemessenen Karten:

maps.phps

Weitere Stichworte, die bei der Auswahl des idealen Algorithmus hilfreich sein könnten: Dijkstra, Floyd/Warshall, Fibonacci-Heap, Bellman-Ford, A*-Algorithmus

Ich bin SEHR gespannt was ihr dabei erreicht! Diskussion in den Kommentaren sehr erwünscht! Auch gern mit Fragen, Zwischenständen etc.

Written by Michael Kliewe

Oktober 4th, 2009 at 4:40 pm

Posted in PHP

Bessere Performance mit einem Reverse Proxy

with 20 comments

Reverse_Proxy_setupIn diesem Artikel geht es nicht um PHP, sondern darum, wie man ein PHP-Applikation via Webserver einer großen Besuchermenge zugänglich macht. Der Standard ist aktuell ein Apache (1.3 oder 2.x), der PHP beherrscht (via mod_php oder FastCGI). Darüber kann man dann wunderbar die Webseiten „serven“.

Doch was tut man, wenn durch eine große Besucheranzahl der Webserver droht, in die Knie zu gehen? Der Apache ist sehr vielseitig, reich an Features, stabil und eigentlich nicht wegzudenken. Doch genau diese Vielseitigkeit und der Feature-Reichtum sind ein Nachteil. Er verbraucht außerdem sehr viel Speicher bei hoher Belastung, sprich vielen tausend Verbindungen.
Deshalb stellt man einen Reverse-Proxy vor den Apache-Webserver. Häufig können diese Proxys auch gleich noch die SSL-Verbindung terminieren, Cachen, und mehr oder minder umfangreiches Loadbalancing.

Einige Bereiche einer Webseite benötigen keinen voll ausgestatteten Apache-Boliden, es reicht ein einfacher, kleinerer Webserver. Dazu gehören alle statischen Inhalte, wie Bilder, Javascripte, CSS und statisches HTML.

Außerdem bereiten langsame Clients Probleme bei großen Webseiten. Wenn viele langsame Clients die Apache-Prozesse am Leben halten, weil die Bytes nur langsam durch das Netz tröpfeln, können andere Requests nicht bedient werden. Ein leichtgewichtiger Reverse-Proxy löst dieses Problem.

Sicher ist euch Squid ein Begriff. Squid kann sowohl als Proxy und auch als Reverse Proxy genutzt werden. Squid ist aber so umfangreich und schwer zu konfigurieren, dass er kaum zum Einsatz kommt als Reverse Proxy.

lighty (lighttpd) dürfte auch vielen bekannt vorkommen. Er hat sehr wenig Ressourcenanforderungen an CPU und Speicher, läuft auf High-Traffic-Seiten wie Youtube, Wikipedia, Pirate Bay, Imageshack und weiteren.

Nginx-battleship-alt.svgEin weiterer interessanter leichtgewichtiger Webserver ist nginx (gesprochen: engine-x). nginx ist neuer, bietet auch eine großartige Leistung wie Lighty (je nach Einsatzgebiet unterschiedlich, aber auf gleichem Niveau; auf jeden Fall Klassen besser als Apache). Seiten wie WordPress.com, Github, SourceForge und vielen weiteren werden von nginx bereitgestellt. Mehr als 5% aller Webseiten laufen mittlerweile durch einen nginx!
Hier scheiden sich die Geister, welcher von beiden nun besser ist. Wenn man sich mal 1-2 Stunden im Internet umschaut, sind 60% für nginx und 40% schwören auf lighttpd. Das größte Problem des Lighty ist wohl sein Memory-Leak-Problem, jedenfalls liest man das recht häufig von Umsteigern. Deshalb verlassen viele nun das Lager in Richtung nginx.

Im Anhang habe ich auch noch einige Seiten aufgelistet, die sich mit Vergleichen (Features, Performance) beschäftigt haben.

Die größten Vorteile: sehr geringer Speicherverbrauch, der auch nicht wächst bei sehr vielen Verbindungen, da es sich um eine „event-driven architecture“ handelt, anders als der Prozess/Thread-getriebene Apache. Ideal für kleine statische Dateien und als Reverse Proxy.

Mit nginx hat man auch einen Loadbalancer, der RoundRobin, weighted RoundRobin, Heartbeat-Funktionalität(er merkt, wenn ein Backend-Server tot ist) uvm bietet. Er kann selbst Dateien zur Verfügung stellen oder aber an einen/mehrere Apache weitergeben. Man kann ihn auch zum Cachen benutzen, er unterstützt nativ den Memcached.

Hier gibt es auch einen schönen Bericht darüber, wie nginx als IMAP/POP/WEB/SMTP Proxy betrieben werden kann. 10.000 IMAP-Verbindungen, einige davon SSL, und dann nur 10% CPU Last finde ich beeindruckend.

Man kann auch den kompletten Apache abschaffen und PHP unter nginx betreiben, hier eine kleine Anleitung für Debian. Hier ist noch eine sehr schöne Anleitung, die alle Funktionen beleuchtet incl. Konfigurationszeilen. Es gibt viele dutzend Module, mit denen man nginx erweitern kann, dazu einfach das englische nginx-Wiki durchstöbern.

Ich habe selbst noch keinen nginx laufen, da ich keinerlei Webseite im Internet betreibe, wo sich der „Aufwand“ eines Reverse Proxy lohnt. Aber in naher Zukunft werde ich wahrscheinlich mit dem Thema konfrontiert, eine High-Traffic-Seite mit aufbauen zu können. Und da werden wir sicher einen Reverse-Proxy einsetzen.

Falls jemand Erfahrungen im High-Traffic-Bereich hat, möge er gern seine Meinung dazu kundtun, ich würd mich freuen!

—————————————
Es gibt viele Vergleiche zwischen Apache, Lighty und nginx. Hier einige Quellen (googlen geht natürlich auch):
http://www.wikivs.com/wiki/Lighttpd_vs_nginx
http://hostingfu.com/article/nginx-vs-lighttpd-for-a-small-vps
http://royal.pingdom.com/2008/04/17/alternative-web-servers-compared-lighttpd-nginx-litespeed-and-zeus/
http://barry.wordpress.com/2008/04/28/load-balancer-update/ incl. Kommentare
http://www.joeandmotorboat.com/2008/02/28/apache-vs-nginx-web-server-performance-deathmatch/

Written by Michael Kliewe

Oktober 2nd, 2009 at 12:40 am

Posted in Allgemein,PHP

Tagged with ,

Verteiltes Rechnen mit Javascript und Google Gears

with 20 comments

timegate_scrWie ich ja bereits des öfteren anmerkte, habe ich früher ein Browsergame programmiert. Begonnen habe ich 2003, als es erst recht wenige Browsergames gab, und diese auch alle von Privatleuten als Hobby betrieben wurden. Da man natürlich Alleinstellungsmerkmale braucht gegen „die Konkurrenz“, baut man viele Ideen ein, die andere nicht haben. Unter anderem war in meinem Browsergame (es handelte sich um ein Aufbauspiel a la OGame, Stoneage etc.) das erste Mal auch ein Turniermodus (1on1) möglich, so dass man abseits der lang laufenden Welten auch in einem KO-Modus gegen andere antreten konnte. Dazu wurden Speed-Server für die ausgelosten Paare gestartet, wo dann 2 Spieler gegeneinander antreten mussten und gewisse Siegbedingungen erreichen mussten. ICQ-Benachrichtigungen bei bestimmten Ereignissen hatte ich auch recht früh eingebaut.

In der folgende Version (die leider nie das Licht der Welt erblickt hat) kamen noch viele weitere interessante Features hinzu. Der Quellcode wurde komplett neu geschrieben in PHP5, war nun voll objektorientiert (in PHP4 war das ja nur sehr beschränkt möglich), und auch neue „Web2.0“ Funktionalitäten waren mit drin.

So gab es zum Beispiel eine Möglichkeit, dass Spieler sich selbst Weltkarten zusammenbauen konnten mit einem einfachen Editor. Der Spieler hat dazu mehrere Feldtypen (Gras, Fels, Wasser, Berg, Moor) und kann daraus eine Karte basteln, auf der er dann gegen andere antreten kann (und wenn sie gut bewertet wird, kann es auch eine Karte für einen „großen“ Server werden, wo tausende Spieler drauf spielen). Eines der Probleme, die es dabei gab, war die Berechnung der Wege. Man muss, um seine Armee zu bewegen, nur das Zieldorf anwählen und kann dort angreifen. Der Dijkstra-Algorithmus bestimmt dann die Zeit, die die Armee unterwegs ist. Dazu muss man den genauen Weg kennen, denn es gibt auch unpassierbares Gelände (in diesem Fall Berge) oder Flächen, auf denen die Armee langsamer ist (Moor). Da die Karten mitunter sehr groß sind (zB 1000*1000), dauert diese Berechnung recht lang. Diese Berechnung bei jeder Armeebewegung zu machen macht die Webseite ziemlich lahm. Also habe ich für die „Standardkarten“ diese Berechnungen bereits erledigt und die 500.000 Ergebnisse (von jedem Feld zu jedem anderen) in einer Datenbank gespeichert.

dijkstraWenn nun aber viele Spieler neue Karten basteln, schafft das der Server nicht mehr, er muss ja auch nebenbei noch für die eigentlichen Webseiten und Datenbanken herhalten. Eine Berechnung für eine Karte dauert mitunter 20 Stunden (Dijkstra Algorithmus in PHP).

Was also tun? Richtig, wir verteilen die Arbeit auf die Client-Rechner. Die 500 Spieler, die auf der Webseite rumsurfen und das Spiel spielen, können nebenbei auch bei der Berechnung helfen. Doch wie macht man das? Aufwendige Dinge wie ein eigenes Client-Programm schreiben, oder Plugins für BOINC etc. sind zu aufwändig und übertrieben. Wie so häufig gibt es eine einfache Lösung: Gears von Google.

Mit Gears kann man asynchron Javascripte ausführen, die die Webseite nicht beeinträchtigen, indem man sogenannte WorkerPools mit Arbeit versorgt.

Hier habe ich mal ein einfaches Beispiel:

main.html

<script type="text/javascript" src="gears_init.js"></script>
<script type="text/javascript">
var workerPool = google.gears.factory.create('beta.workerpool');

workerPool.onmessage = function(a, b, message) {
	alert('result from worker ' + message.sender + ': \n' + message.body);
};

var childWorkerId = workerPool.createWorkerFromUrl('worker2.js');

workerPool.sendMessage([5, 1, 8], childWorkerId);
</script>

worker2.js:

var wp = google.gears.workerPool;
wp.onmessage = function(a, b, message) {
	// do calculation here
	var reply = message.body[0] + message.body[1] + message.body[2];
	wp.sendMessage(reply, message.sender);
}

Wenn man die entsprechende Webseite aufruft, wird man gefragt, ob der Zugriff auf Gears erlaubt werden soll. Wenn man Gears installiert hat, kann man also noch von Fall zu Fall unterscheiden, ob man der Webseite seine Resourcen zur Verfügung stellen möchte oder nicht.

gears_question

Was passiert nun? Die Webseite startet einen WorkerPool, in diesem Workerpool wird ein Worker gestartet, der das Script worker2.js ausführen soll. Wir senden an den darin befindlichen Worker ein Array mit 3 Zahlen.

Der Worker errechnet dann die Summe dieser Zahlen, und sendet das Ergebnis zurück an den WorkerPool. Diese Nachricht fangen wir mit dem „onMessage“ Eventhandler ab und zeigen das Ergebnis mittels alert() an.

Wie würde nun die Kartenberechnung funktionieren? Wir übergeben statt dem einfachen Array ein komplexes großes Array mit dem Graphen, den Kantengewichten usw.  Im Worker findet dann die eigentliche Dijkstra-Berechnung statt. Dort wäre es sinnvoll, nicht die ganze Karte zu berechnen (Dauer 20 Stunden), sondern nur kleine Häppchen, die in wenigen Sekunden bearbeitet sind. Statt das Ergebnis dann via alert() auszugeben, schicken wir es via AJAX zurück an den Webserver, der das Ergebnis in die Datenbank einträgt.

Leider ist es bei diesen ersten Tests geblieben, ich habe die eigentlichen umfangreichen Dijkstra-Berechnungen nie in Javascript mit Gears umgesetzt. Aber möglich ist es.

Eine einzige Hürde gibt es: Auf dem Clientrechner muss Gears installiert sein. Mit einer guten Community und Nutzern, die einem vertrauen, ist aber auch das machbar.

Alternativ kann man natürlich auch ohne Gears die Berechnungen in Javascript durchführen lassen. Dabei muss man jedoch beachten, dass der Browser dadurch nicht unbenutzbar wird. Man muss also die Berechnung künstlich bremsen, außerdem muss man eine Lösung dafür finden, dass der Browser lang laufende Javascripte gern auch mal abbrechen möchte.

longrunning

Mit Gears passiert sowas nicht.

Anwendungsfälle des Dijkstra-Algorithmus sind beispielsweise das bekannte „Friend of a Friend“ Problem (soziale Netzwerke zeigen an, über wieviele Kontakte man eine andere Person kennt) oder alle anderen Arten, wo man in einem Graphen den kürzesten Pfad ermitteln möchte. Man kann so sicherlich auch noch andere, komplexe Berechnungen auf die Clients auslagern.

Gears kann man natürlich auch noch für viele weitere Dinge nutzen, wie schöne Benachrichtigungen, als lokalen Speicher für Dateien oder Daten (Datenbank), Geolocation (was ja mittlerweile die Browser schon nativ beherrschen) usw.

Habt ihr Erfahrungen mit Gears, oder Ideen welche revolutionären Dinge man damit umsetzen könnte?

Written by Michael Kliewe

September 29th, 2009 at 10:35 pm