Seite wählen

NETWAYS Blog

Profiling von PHP mit Xdebug

Oftmals fällt es gar nicht auf. Man entwickelt sein Programm und greift auf Funktionen zurück, die schon seit Dekaden bestens funktionieren. Allerdings stellt man irgendwann fest, daß alles immer langsamer wird. Auf den ersten Blick ist nicht ersichtlich warum es zu diesen Einbußen kommt. Der zweite Blick auf den Profiler zeigt das die vermeintlich robuste Funktion eigentlich langsam ist. Durch mehrmalige Verwendung zwingt es die ganze Applikation in die Knie.
Auch beim tunen hilft’s: PHP ist an vielen Stellen inkonsistent und reagiert entsprechend anders, wie schaut es mit der Geschwindigkeit von Iteratoren zu Schleifen aus – oder array_key_exists zu isset? Hier gibt der Profiler Aufschluss was man am besten vermeidet und was man lieber vermeidet.
Einen guten guten Profiler für PHP kommt von Xdebug (wahrscheinlich auch der einzige). Installiert kriegt man ihn am besten aus dem Distro Repository (z.B. von Ubuntu) oder einfach aus den Sourcen mit phpize && sh configure && make && make install. Nach dem er nicht dauerhaft benötigt kann man ihn mit einem Trigger über die URL aktivieren. Über die Kommandozeile setzt man einfach das entsprechende ini-flag und der Profiler schreibt Valgrind kompatible Ausgabe nach /tmp. Eine Standardkonfiguration für Xdebug schaut demnach folgendermaßen aus:

[xdebug]
xdebug.profiler_enable=0
xdebug.profiler_enable_trigger=1

Trigger Möglichkeiten:

wget http://localhost/myscript.php?XDEBUG_PROFILE=1
php -d xdebug.profiler_enable=On myscript.php

Die Datei (z.B. /tmp/cachegrind.out.24869) kann man mit WinCacheGrind oder KCachegrind einsehen und optimiert entweder anhand der Zeitabläufe oder anhand der Aufrufe. Je nach Menge spart das ordentlich Zeit.
Hier ein Beispiel anhand verschiedener Iterationsmöglichkeiten in PHP:


Der Test ist natürlich nicht objektiv, zeigt aber, daß die vermeintlich gleiche Aufgabe nicht gleich optimal abläuft. Häufig findet man mit dem Profiler dann Funktionen im Code welche gnadenlos vergewaltigt werden (oder für einen anderen Zweck entworfen wurden) – Und das verbrennt dann richtig Zeit!

Marius Hein
Marius Hein
Head of IT Service Management

Marius Hein ist schon seit 2003 bei NETWAYS. Er hat hier seine Ausbildung zum Fachinformatiker absolviert und viele Jahre in der Softwareentwicklung gearbeitet. Mittlerweile ist er Herr über die interne IT und als Leiter von ITSM zuständig für die technische Schnittmenge der Abteilungen der NETWAYS Gruppe. Wenn er nicht gerade IPv6 IPSec Tunnel bohrt, sitzt er daheim am Schlagzeug und treibt seine Nachbarn in den Wahnsinn.

FastCGI und Icinga-web

PHP skaliert in Standardinstallationen relativ schlecht. Grund dafür ist, daß der Webserver jedes mal eine neue Interpreterinstanz erstellen muss und danach wieder zerstört. Das kostet Zeit und eine Menge Performance / Nerven. Besser funktioniert es mit FastCGI. Hier werden die Interpreterinstanzen unabhängig vom Webserver gestartet und stehen fertig geladen bereit wenn der Request beim Webserver eintrifft. Leider ist die Konfiguration etwas schwieriger da es für FastCGI keine richtige Standardlösung gibt. Je nach Webserver werden eigene Wrapper benötigt die gestartet werden müssen. Weshalb ein solches Konstrukt oft auf wackeligen Beinen steht und anfällig für Bedienfehler ist.
Mit PHP 5.3 führt die Interpretersprache einen eigenen Manager für FastCGI Prozesse ein: FPM – FastCGI Process Manager. Mit ihm ist es auf einfache Art möglich Gruppen von Applikationen anzulegen, Skalierung einzustellen und dadurch den Überblick über kritische oder schwer gewichtige Webapplikationen zu behalten.
Ich habe mal versucht den FPM mit Apache2 (mpm-worker) unter Ubuntu am Beispiel von Icinga-web zum laufen zu bekommen. Ausgangssituation ist ein installierter Apache2 und eine Standard Icinga-web Installation aus den Tutorials oder den Quickstart Guides.
1) Installation der benötigten Pakete und aktivieren der benötigten Module:

# aptitude install libapache2-mod-fastcgi php5-fpm
# a2enmod fastcgi actions rewrite
Enabling module fastcgi.
Enabling module actions.
Enabling module rewrite.

2) Vorbereiten der Konfiguration für FPM

# pwd
/etc/php5/fpm
# cat pool.d/icinga-web.conf
[icinga-web]
listen = 127.0.0.1:9000
user = www-data
group = www-data
pm = dynamic
pm.max_children = 100
pm.start_servers = 30
pm.min_spare_servers = 30
pm.max_spare_servers = 50
;pm.max_requests = 500
chroot =
chdir = /usr/local/icinga-web/pub

Ich habe die Standardkonfiguration für „www“ vorher deaktiviert. Ansonsten würden sich die Sockets auf Port 9000 einen Strich durch die Rechnung machen.
3) Fehlt noch eine Apache Konfiguration (Icinga-web Standard mit FastCGI Erweiterung):

# pwd
/etc/apache2/sites-available
# cat fcgi-test
	FastCgiExternalServer /var/www/php5.external -host 127.0.0.1:9000
	DocumentRoot /var/www
	AddHandler php5-fcgi .php
	Action php5-fcgi /usr/lib/cgi-bin/php5.external
	Alias /usr/lib/cgi-bin/ /var/www/
	AliasMatch /icinga-web/modules/([A-Za-z0-9]*)/resources/styles/([A-Za-z0-9]*\.css)$ /usr/local/icinga-web/app/modules/$1/pub/styles/$2
	AliasMatch /icinga-web/modules/([A-Za-z0-9]*)/resources/images/([A-Za-z_\-0-9]*\.(png|gif|jpg))$ /usr/local/icinga-web/app/modules/$1/pub/images/$2
	Alias /icinga-web/js/ext3 /usr/local/icinga-web/lib/ext3
	Alias /icinga-web /usr/local/icinga-web/pub
	        Order allow,deny
	        Allow from all
	        Order allow,deny
	        Allow from all
	        Order allow,deny
	        Allow from all
	        DirectoryIndex index.php
	        Options FollowSymLinks
	        AllowOverride all
	        Order allow,deny
	        Allow from all
# a2ensite  fcgi-test
Enabling site fcgi-test.

4) Apachen neu laden und den FPM Pool starten:

# ps aux | grep fpm | head -3
root      5213  0.0  0.0 145792  4872 ?        Ss   13:18   0:00 php-fpm: master process (/etc/php5/fpm/php-fpm.conf)
www-data  5214  0.1  0.4 173028 34312 ?        S    13:18   0:00 php-fpm: pool icinga-web
www-data  5215  0.3  0.4 175140 36824 ?        S    13:18   0:00 php-fpm: pool icinga-web

Das ganze ist zwar nur rudimentär getestet und icinga-web nicht optimiert für FastCGI, bietet aber Potential für mehr: Persistenter Agavi Cache, Connection Pooling für die Datenbank, usw.

Marius Hein
Marius Hein
Head of IT Service Management

Marius Hein ist schon seit 2003 bei NETWAYS. Er hat hier seine Ausbildung zum Fachinformatiker absolviert und viele Jahre in der Softwareentwicklung gearbeitet. Mittlerweile ist er Herr über die interne IT und als Leiter von ITSM zuständig für die technische Schnittmenge der Abteilungen der NETWAYS Gruppe. Wenn er nicht gerade IPv6 IPSec Tunnel bohrt, sitzt er daheim am Schlagzeug und treibt seine Nachbarn in den Wahnsinn.

Speicherwarnung für PHP

Das man bei PHP eigentlich nicht auf den Speicher achten muss ist entweder Freud oder Leid. Die Freude ist begrenzt denn ab einer gewissen Größe ist der Speicherverbrauch nicht mehr so einfach „wegzuimplementieren“.
Abhilfe schafft hier eine PECL Erweiterung namens „memtrack“ die PHP Warnung anhand von Schwellwerten in die Fehlerbehandlung von PHP einkippt. Man wird also während der Entwicklung bereits gewarnt oder überprüft regelmäßig seine Logs nach ausreißern.
Die Installation ist denkbar einfach mit PECL oder per Hand:

# wget "http://pecl.php.net/get/memtrack-0.2.1.tgz"
# tar -xvzf memtrack-0.2.1.tgz
# cd memtrack-0.2.1
# phpize
# sh configure
# make install

Danach erstellt man sich einen php.ini Eintrag um das Modul erstmal zu laden:

# cat memtrack.ini
[memtrack]
extension=memtrack.so
memtrack.enabled = 1
memtrack.soft_limit=1M
memtrack.vm_limit=3M

Wir stellen eine Prozessschwelle (vm_limit) von 3MB und eine Ausführungsschwelle von 1MB. Außerdem aktivieren wir das Module global für alle laufenden PHP Interpreter Instanzen.
Mit Hilfe eines kleinen Scripts den Speicherverbrauch aufdrehen und sich über die Warnungen freuen (oder weinen):

# cat test.php

# php test.php
PHP Warning:  [memtrack] [pid 5704] user function buildArray() executed in /tmp/test.php on line 13 allocated 27787264 bytes in Unknown on line 0
PHP Stack trace:
PHP   1. {main}() /tmp/test.php:0
PHP Warning:  [memtrack] [pid 5704] virtual memory usage on shutdown: 31571968 bytes in Unknown on line 0
Marius Hein
Marius Hein
Head of IT Service Management

Marius Hein ist schon seit 2003 bei NETWAYS. Er hat hier seine Ausbildung zum Fachinformatiker absolviert und viele Jahre in der Softwareentwicklung gearbeitet. Mittlerweile ist er Herr über die interne IT und als Leiter von ITSM zuständig für die technische Schnittmenge der Abteilungen der NETWAYS Gruppe. Wenn er nicht gerade IPv6 IPSec Tunnel bohrt, sitzt er daheim am Schlagzeug und treibt seine Nachbarn in den Wahnsinn.

JasperServer und SOAP

Sich eigene Reports oder sogar erweiterbare Reportframeworks zu schreiben ist mühsam und zeitaufwendig. Vor allem wenn es bereits gute Lösungen in dem Bereich gibt wie z.B. JasperServer. Neben einem eigenem Interface zur Verwaltung von Berichten und deren Ressourcen exportiert der Server gleichzeitig seine ganze Funktionalität durch SOAP nach außen. Die Installation geht mit wenigen Zeilen von statten. Entweder kann man sich ein Komplettpaket ziehen oder nur die Applikation welche auf einem Tomcat Server deployed werden muss.
Jasper Reports können mit einem Reportdesigner (iReport) erstellt werden und auf den Server übertragen werden. Danach können sie entweder direkt abgerufen werden oder zeitgesteuert per Mails verschickt werden.
Mit PHP und etwas gemogel kommt man relativ einfach zu seinem Report im gewünschten Format:

<?php
$jasper_wsdl = 'http://10.121.0.95:8080/jasperserver/services/repository?wsdl';
$client = new SOAPClient($jasper_wsdl, array (
        'login' => 'jasperadmin',
        'password' => 'jasperadmin',
        'trace' => true
));
$request = '<?xml version="1.0" encoding="UTF-8"?>
<request operationName="runReport" locale="en">
        <argument name="RUN_OUTPUT_FORMAT"><![CDATA[PDF]]></argument>
        <resourceDescriptor name="" wsType="" uriString="/Icinga/reports/host.overview" isNew="false">
                <label>null</label>
                <parameter name="p_host_object_id"><![CDATA[1]]></parameter>
        </resourceDescriptor>
</request>';
// Rückgabe ist nicht XML sondern multipart
try {
 $re = $client->runReport($request);
} catch (SOAPFault $e) {
        if ($e->getMessage() !== 'looks like we got no XML document') {
                throw $e;
        }
}
$headers = $client->__getLastResponseHeaders();
$m = array();
// Splitten der Ausgabe und finden des reports
if (preg_match('/boundary="([^"]+)"/', $headers, $m)) {
        $content = explode('--'. $m[1], $client->__getLastResponse());
        foreach ($content as $part) {
                if (strpos($part, 'Content-Id: <report>')!==false) {
                        list($header, $content) = explode("\r\n\r\n", $part);
                        file_put_contents('report.pdf', $content);
                        break;
                }
        }
}

Die Antworten vom Jasper sind MIME multipart/related weshalb man den nativen PHP Soap Client etwas austricken und die die Inhalte zusammensuchen muss.
Der Webservice ist übrigens hervorragend dokumentiert.

Marius Hein
Marius Hein
Head of IT Service Management

Marius Hein ist schon seit 2003 bei NETWAYS. Er hat hier seine Ausbildung zum Fachinformatiker absolviert und viele Jahre in der Softwareentwicklung gearbeitet. Mittlerweile ist er Herr über die interne IT und als Leiter von ITSM zuständig für die technische Schnittmenge der Abteilungen der NETWAYS Gruppe. Wenn er nicht gerade IPv6 IPSec Tunnel bohrt, sitzt er daheim am Schlagzeug und treibt seine Nachbarn in den Wahnsinn.

PHP – beschleunigt!

Wer kennt sie nicht – langsame PHP Seiten. Obwohl mittlerweile Speicher und Rechenleistung keine Rolle mehr spielen kommen manche PHP Applikationen nicht richtig in die Gänge.
Hier könnte der Zend Community Edition Server abhilfe schaffen. Ein kompilierter Apache mit einem Satz ausgewählter Module und ein aktuelles PHP5 bieten eine robuste Basis für die Auslieferung. Der Zend CE Server ist kostenfrei und kann auf der Herstellerseite von Zent heruntergeladen werden.
Im Hintergrund werkeln ein Apache2 für PHP und ein lighttpd für ein Admininterface welches komfortable Einstellungen in PHP erlaubt. Weiterhin ist der Zend Optimizer+ integriert, ein PHP Opcode- und Pagecacher welcher die Seite ziemlich fix generiert.
Auf Distributionen, welche keine aktuelle PHP Version bereitstellen, kann das Packet eine eigenständige Laufzeitumgebung für PHP Anwendungen bieten ohne sich zu tief ins System zu verwurzeln.
Die Installation geht schnell von statten. Man brauch nur den Installer Zend und schon gehts los:

root@rt:/usr/local/src# ls
ZendServer-5.0.3_Tarball_B3-php5.3.3-linux-glibc23-x86_64
ZendServer-CE-php-5.3.3-5.0.3-linux-glibc23-x86_64.tar.gz
root@rt:/usr/local/src# cd \
ZendServer-5.0.3_Tarball_B3-php5.3.3-linux-glibc23-x86_64/
root@rt:/usr/local/src/ZendServer-5.0.3_[...]# bash install.sh
Welcome to Zend Server installation script!
Please specify an installation path [/usr/local]:
[...]

Leider verursachen die Installierten neuen Bibliotheken Probleme, der Ursprungszustand ist schnell wiederhergestellt:

root@rt:~# rm -f /etc/ld.so.conf.d/zend_server.conf
root@rt:~# rm -f /usr/local/zend/lib/libcrypto.so.0.9.8
root@rt:~# /etc/init.d/ssh restart

Gesteuert werden die Dienste mit einem eigenen Script:

root@rt:~# /usr/local/zend/bin/zendctl.sh -h

Danach ist der Server einsatzbereit. Konfiguration und Wartung gestalten sich Apache üblich. Alle wichtigen Konfigurationen befinden sich unter /usr/local/zend (Standardinstallation).



Marius Hein
Marius Hein
Head of IT Service Management

Marius Hein ist schon seit 2003 bei NETWAYS. Er hat hier seine Ausbildung zum Fachinformatiker absolviert und viele Jahre in der Softwareentwicklung gearbeitet. Mittlerweile ist er Herr über die interne IT und als Leiter von ITSM zuständig für die technische Schnittmenge der Abteilungen der NETWAYS Gruppe. Wenn er nicht gerade IPv6 IPSec Tunnel bohrt, sitzt er daheim am Schlagzeug und treibt seine Nachbarn in den Wahnsinn.