Durchleuchtet

PHP-Anwendungen mit DTrace analysieren
Kommentare

Analysewerkzeuge und Debugger wie strace, GDB und Xdebug werden von Entwicklern und Systemadministratoren gleichermaßen in ihrer täglichen Arbeit verwendet. Sie geben einen tiefen Einblick in einen laufenden Prozess, beschränken sich jedoch darauf, eine Anwendung immer auf dieselbe Weise zu analysieren und die Daten in einem vorgegebenen Format auszugeben. Auch sind sie für Produktivumgebungen ungeeignet. Das von Sun Microsystems entwickelte dynamische Tracing-Framework Dtrace verfolgt einen alternativen Ansatz und versucht diese Probleme zu vermeiden.

DTrace ist im Betriebssystemkern verankert und wird von dem Entwickler durch eine Skriptsprache angesprochen. Dadurch können selektiv Daten aus Programmen aggregiert und in einem beliebigen Format dargestellt werden. Darüber hinaus ist es möglich, Daten aus mehreren parallel laufenden Programmen zu aggregieren, was insbesondere bei der Entwicklung von LAMP-Anwendungen nützlich ist. Bei der Entwicklung von DTrace wurde darauf Wert gelegt, dass man Anwendungen ohne einen Neustart oder eines erneuten Kompilierens untersuchen kann. So lassen sich auch optimierte Binaries, wie sie auf Livesystemen eingesetzt werden, mit DTrace nutzen.

Probes

Analysiert man eine Anwendung mit Programmen wie strace oder truss, generiert man zunächst Profile, die man daraufhin auf Probleme durchsucht. Im Gegensatz dazu stellt DTrace dem Entwickler alle analysierbaren Punkte innerhalb des laufenden Systems zur Verfügung, aus denen man als Entwickler die relevanten Punkte heraussucht und DTrace damit beauftragt, Daten über die ausgewählten Punkte zu sammeln. Diese analysierbaren Punkte werden als „Probes“ bezeichnet und können sowohl dynamisch vom Kernel generiert oder statisch in ein Programm einprogrammiert werden. Ob eine Probe dynamisch oder statisch generiert wird, hängt von der Wahl des Probe Provider ab. Wir werden den statischen Provider PHP verwenden. Damit können wir PHP ohne Kenntnisse der Internals analysieren. Es existieren eine Reihe weiterer nützlicher Probe Provider. Hier werden wir, neben dem statischen Provider für PHP, auch auf den PID Provider eingehen, der für jeden Aufruf einer C-Funktion Probes generiert. Eine Probe wird durch eine Probe Description eindeutig bestimmt und besteht aus einem 4-Tupel der Form Providername:Modulname:Funktionsname:Probenamen. Der Modulname ist nur bei Probes des Betriebsystemkernels gesetzt, während der Funktionsname den Namen der C-Funktion, in der die Probe aufgerufen wurde, beinhaltet. DTrace aktiviert ausschließlich die verwendeten Probes. Solange DTrace nicht verwendet wird, wirkt es sich nicht – im Gegensatz zu Debugging Binaries – negativ auf die Performance aus. Da DTrace ein Programm mithilfe eines Skripts analysiert, wird das Programm nicht, wie bei einem Debugger, zeitweise angehalten.

Verfügbarkeit

DTrace wurde ursprünglich für Sun Solaris 10 entwickelt, mittlerweile aber auch auf OpenSolaris, MacOS 10.5 und FreeBSD 7.1 portiert. Die Implementierungen sind teilweise plattformspezifisch und die Listen der verfügbaren Probes können sind unterscheiden. DTrace ist unter Suns CDDL lizenziert, sodass eine Portierung auf Linux aufgrund der Inkompatibilität zur GNU General Public License nicht existiert.

Statische Probes, die auf die Eigenheiten der einzelnen Anwendungen zugeschnitten sind, existieren für eine Vielzahl von Anwendungen, darunter auch Ruby und Python. Sie sind entweder einkompiliert oder durch eine Erweiterung ladbar. PHP kann man mithilfe der DTrace Extension aus dem PECL Repository um statische Probes erweitern. Mit dem 2009.06-Release von OpenSolaris und dem Sun Webstack 1.5 wird PHP um neue Probes erweitert. Diese direkt in die Sprache eingebetteten Probes erlauben unter anderem das Abfangen von Exceptions und Errors.

In unserem Fall wollen wir die Probes aus der PECL Extension von Wez Furlong benutzen. Eine Liste aller verfügbaren Probes auf einem System kann man dem Befehl dtrace -l ausgeben. Die Liste kann dabei durch eine Probe Description eingeschränkt werden. Wir wollen die Liste durch den Identifier php::: auf alle PHP-relevanten Probes beschränken. Die DTrace Extension für PHP stellt die Probes function-entry und function-return zu Verfügung. Die Probe function-entry tritt zu Beginn eines PHP-Funktionsaufrufs auf, die Probe function-return nach dessen Abarbeitung. In unserem Beispiel wollen wir DTrace zunächst verwenden, um die Ausführungszeit einiger für uns interessanter Funktionen einer Beispielanwendung zu messen. Hierfür schreiben wir ein Skript, wo DTrace vorgibt, welche Probes wir benutzen wollen und wie wir unsere Daten aggregieren wollen. Die hierbei verwendete Skriptsprache ist syntaktisch an C angelehnt und besteht aus Probe Descriptions und dazugehörigen Programmblöcke der Form:

probe description
/ predicate /
{
  action statements
}

Eine Probe kann bis zu fünf Argumente an den Programmblock übergeben, die in den Variablen arg0 bis arg4 gespeichert werden. An function-entry und function-return wird als erstes Argument der Name der Funktion übergeben. Das zweite und dritte Argument enthält den Pfad des Skripts und die dazugehörige Zeilennummer. Wenn eine Probe auftritt, führt DTrace die von uns definierten Aktionen aus. Dabei kann es sich beispielsweise um eine Ausgabe oder die Definition einer Variablen handeln. Wir können eigene Variablen und Arrays definieren und auf eine Reihe vordefinierter Variablen und Aktionen zurückgreifen, die uns DTrace zur Verfügung stellt. Listing 1 zeigt ein einfaches Beispiel. Die Variable timestamp wird von DTrace bereitgestellt und jede Nanosekunde inkrementiert. Wir verwenden diese Variable, um die Zeit zu berechnen, die vom Anfang der Funktion (function-entry) und bis zu deren Ende vergangen ist. Variablen haben unterschiedliche Geltungsbereiche. Das Präfix self-> wird benutzt, um eine Variable zu allozieren, die nur für den aktuellen Thread gilt. Dies verhindert in Multithread Environments wie Apache das gegenseitige Überschreiben der Variable durch parallele Threads. Neben Thread-lokalen Variablen stellt DTrace auch Clause-lokale Variablen zur Verfügung. Diese haben das Präfix this-> und sind nur innerhalb eines Codeblocks gültig. Sie werden benutzt, um Namenskonflikte mit globalen Variablen zu verhindern.

Listing 1
#!/usr/sbin/dtrace -s 
php$target:::function-entry 
{ 
  self->calltime = timestamp; 
} 

php$target:::function-return 
{ 
  printf("Funktion %s Zeit %dn", copyinstr(arg0), timestamp - self->calltime); 
}
Actions

Die Ausgabe auf die Konsole erfolgt über die aus PHP bekannte Prozedur printf(). Die Aktion copyinstr() wird verwendet, um eine String-Variable aus dem Adressraum der Applikation in den Speicher von DTrace zu kopieren. Diese Vorgehensweise ist nötig, um den String ausgeben zu können, und ist durch die Implementierung von DTrace bedingt. copyinstr() muss immer auf Strings angewendet werden, die aus der Anwendung an das Skript übergeben werden. Dies gilt nicht für Strings, die innerhalb eines Skripts gesetzt worden sind. Falls es sich um Probes wie die Syscall Probe handelt, die innerhalb des Kernels auftreten, müssen die Argumente nicht kopiert werden. Den Backtrace erhalten wir über die Aktion ustack(). DTrace gibt bei der Benutzung von ustack() die Namen der aufgerufenen C-Funktionen aus. Für einige Sprachen, z. B. Java, existieren für den Anwender transparente Implementierungen von ustack(), die zusätzlich programmiersprachenspezifische Informationen und Funktionsnamen ausgeben. Für PHP existiert eine solche Implementierung allerdings nicht. Die Namen der aufgerufenen internen PHP-Funktionen lassen sich jedoch durch ein einfaches Namensschema ableiten. Im Regelfall gilt, dass interne PHP-Funktionen mit dem Präfix zif_ gekennzeichnet sind, interne Methodennamen mit dem Präfix zim_. Weitere verwendbare Aktionen sind im DTrace-Wiki aufgelistet.

Predicates

Anstatt alle PHP-Funktionen zu messen, wollen wir nur noch eine einzige PHP-Funktion betrachten. Die Filterung nach weiteren Kriterien erfolgt über Predicates, die nach der Probe Description definiert werden. Die D-Sprache unterstützt keine if-Konstrukte, weshalb Predicates die einzige Möglichkeit sind, Programmblöcke durch Konditionen einzuschränken. Innerhalb eines Programmblocks kann keine weitere Filterung mehr vorgenommen werden. Predicates können beliebige Boolean-Ausdrücke beinhalten. Die Syntax ist ähnlich zu PHP. Ist der Wert eines Ausdrucks Null, wird er als false evaluiert, andernfalls als true. Listing 2 zeigt, wie man eine Probe Description auf die Funktion page_get_cache einschränkt. Bedingt durch die Implementierung der PHP DTrace Probes, sollten wir zudem immer prüfen, ob das erste Argument gesetzt ist. In dem Fall, dass es nicht gesetzt ist, handelt es ich um den Beginn einer neuen PHP-Datei und um keinen Funktionsaufruf.

Listing 2
#!/usr/sbin/dtrace -s 
php$target:::function-entry
/ arg0 && copyinstr(arg0) == "page_get_cache" /
{ 
  self->calltime = timestamp; 
} 

php$target:::function-return 
/ arg0 && copyinstr(arg0) == "page_get_cache" /
{ 
  printf("Funktion %s Zeit %dn", copyinstr(arg0), timestamp - self->calltime); 
}
Aggregation

Mit einem eigenen Sprachobjekt und dazugehörigen Funktionen stellt DTrace eine leistungsstarke Methodik zur Aggregation von Daten bereit. Die „Aggregations“ genannten Konstrukte sind die am häufigsten verwendete Art, statistische Daten zu generieren. Sie werden mit einem @ und einem optionalen Namen ausgewiesen und mithilfe eines Indizierungsschlüssels gruppiert, wobei das Sprachkonstrukt assozativen Arrays aus PHP ähnelt. Aggregations können zusammen mit Verteilungsfunktionen wie count(), quantize() und max() verwendet werden. Somit haben wir ein leistungsstarkes Werkzeug, um Standardabweichungen, Durchschnitt und Verteilung von Laufzeiten zu messen. In Listing 3 erweitern wir unser Beispiel und zählen die Anzahl der Funktionsaufrufe. Dabei wird die Aufrufstatistik nach Funktionsnamen gruppiert. Komplexere Aggregationen lassen sich mit der Funktion quantize() umsetzen, die eine Tabelle der aggregierten Daten ihrer Verteilung entsprechend darstellt. Aggregations werden beim Beenden eines DTrace-Skripts automatisch ausgegeben. Die Aktion printa() erlaubt es zudem, ein eigenes Ausgabeformat zu definieren. Listing 4 veranschaulicht diese Funktionalität anhand einer Query-Methode eines Datenbankabstraktions-Layers.

Listing 3
#!/usr/sbin/dtrace -s
php$target:::function-return 
/arg0/
{ 
    @count[copyinstr(arg0)] = count(); 
} 

END 
{
    printa("Es wurden %@8u Funktionen aufgerufenn", @count);
}
Listing 4
#!/usr/sbin/dtrace -s 
php$target:::function-entry 
/ arg0 && copyinstr(arg0) == "make_query" / 
{ 
    self->calltime = timestamp;
}

php$target:::function-return
/ arg0 && copyinstr(arg0) == "make_query" / 
{
        this->exectime = timestamp - self->calltime;
        @quant["Quantize", copyinstr(arg0)] = quantize(this->exectime);
        @avgr["Average", copyinstr(arg0)] = avg(this->exectime);
        @sum["Sum", copyinstr(arg0)] = sum(this->exectime);
}
Den Überblick bewahren

Da DTrace im Kernel läuft, überblickt es alle laufenden Anwendungen und ist nicht, wie ein herkömmlicher Debugger, an die Grenzen des Adressraums der Applikation gebunden. Deshalb können wir DTrace-Skripte schreiben, die nicht nur einen PHP-Prozess analysieren, sondern die Daten aller PHP-Prozesse sammeln. Betrachten wir die Liste der aktivierten PHP Probes, sehen wir, dass DTrace die Prozess-ID (PID) an den Providernamen der Anwendung anhängt. Der Providername eines PHP-Skripts lautet also beispielsweise php1234:::. Wollen wir die Probes aller PHP-Prozesse aktivieren, bietet sich die Wildcard * an. Unsere Probe Description lautet damit php*:::. Die Wildcard lässt sich genauso auf alle anderen Teile der Probe Description anwenden. Ebenso können wir eine vorgegebene PID aktivieren, indem wir den Platzhalter

psenv::pushli(); eval($_oclass[„target“]); psenv::popli(); ?>

anhängen und eine PID oder einen Programmaufruf auf der Kommandozeile an DTrace übergeben. Der Befehl dtrace -n ‚php

psenv::pushli(); eval($_oclass[„target“]); psenv::popli(); ?>

:::‘ -c ‚php example.php‘ zeigt alle aufgerufenen PHP Probes des angegebenen PHP-Aufrufs.

Haben wir die Einstiegshürde von DTrace überwunden, können wir unser Wissen nutzen, um Daten aus PHP und MySQL gleichzeitig zu aggregieren. Hierfür müssen beide Applikationen auf demselben Rechner laufen. Entwickler können dies benutzen, um sich eine Liste der ausgeführten Queries auf dem Rechner auszugeben. Eine Unterstützung von statischen Probes wird es in MySQL erst ab Version 6.0 geben. Solange können Sie entweder auf die vorgezogene Integration von statischen Probes für MySQL 5.1 in OpenSolaris 2009.06 oder mit etwas Wissen der MySQL-Internals auf den PID Provider zurückgreifen. Der PID Provider generiert für jeden registrierten Funktionsaufruf eine entry- und eine return-Probe. MySQL parsed den Query in der Funktion start_new_query. Wir können die Probe Description pid*::*start_new_query*:entry verwenden, um den Query abzufangen. Die Wildcards sind nötig, da es sich um kompilierte C++-Funktionen handelt, deren Namen sich durch Name-Mangeling des Compilers verändert haben. Die an die Probe übergebenen Argumente entsprechen den Parametern der Funktion. Das Skalar arg2 enthält den Query String, Listing 5 zeigt die Ausgabe. Eine Zuordnung zwischen PHP-Prozess und Query in MySQL ist derzeit jedoch noch nicht möglich.

Eine weitere Anwendung für den PID Provider ist das aufzeigen von Fehlern. PHP Errors werden durch den @-Operator unterdrückt. Dennoch wird die PHP-interne Funktion zend_error aufgerufen. Wir können durch das Analysieren dieser Funktion nachvollziehen, in welcher Datei und Zeile der Fehler aufgetreten ist, unabhängig von der Verwendung des @-Operators oder der Einstellung des Error-Reporting-Levels.

Listing 5
#!/usr/sbin/dtrace -s 
pid$target::*start_new_query*:entry 
{ 
  trace(copyinstr(arg0)); 
}
fstat

Wir wollen uns im Folgenden ein etwas komplexeres Beispiel ansehen. Es wird uns helfen, die Anzahl der fstat-Systemaufrufe zu zählen. Mit diesen Aufrufen testet PHP, ob eine Datei existiert, wie groß sie ist und wann sie das letzte Mal modifiziert wurde. Dieser Systemaufruf wird von PHP sehr häufig verwendet und ist mit einem Context Switch in den Adressraum des Kernel verbunden. Aus diesem Grund kann er zu einem Flaschenhals in High-Performance-Anwendungen werden. Mit dem in Listing 6 gezeigten Skript können wir die Aufrufe von fstat zählen und die dafür verwendete Zeit berechnen. Die verwendete Syscall Probe wird für jedes Programm, das fstat aufruft, aktiviert. Da wir nur die von PHP ausgeführten Systemaufrufe zählen wollen, werden wir die Variable isInsidePhp verwenden, um zu prüfen, ob wir uns innerhalb einer PHP-Datei befinden. Wir inkrementieren die Variable bei jeder function-entry-Probe und dekrementieren sie, sobald der Funktionsaufruf beendet worden ist. Daraus ergibt sich dann die Tiefe des PHP Callstacks.

Listing 6
#!/usr/sbin/dtrace -s 
BEGIN 
{ 
  self->isInsidePhp = 0; 
} 

php*:::function-entry 
{ 
  self->isInsidePhp++; 
  self->filename = copyinstr(arg1); 
} 

php*:::function-return 
{ 
  self->isInsidePhp--; 
} 

syscall::fstat*:entry 
/ self->isInsidePhp > 0 / 
{ 
  self->timestamp = timestamp; 
} 

syscall::fstat*:return 
/ self->isInsidePhp > 0/ 
{ 
  @quantize[self->filename] = quantize(timestamp - self->timestamp); 
  @count[self->filename] = count(); 
} 

END 
{ 
  trace("Auswertung"); 
}

Wie bereits besprochen, führt die DTrace Extension die Probe function-entry auch dann aus, wenn wir eine neue PHP-Datei ausführen. Der Name der aktuellen PHP-Datei steht uns innerhalb der Syscall Probe nicht zur Verfügung. Wir müssen ihn deshalb in einer Thread-lokalen Variablen zwischenspeichern. Sie wird in der function-entry-Probe gesetzt, da wir zu diesem Zeitpunkt Zugriff auf den Dateipfad des ausgeführten Skripts haben. Abschließend berechnen wir die Zeit, die der Systemaufruf benötigt hat. Das von uns verwendete OpenSolaris stellt hierzu die zwei Varianten fstat und fstat64 bereit. Mithilfe des Wildcard können wir beide Funktionen durch dieselbe Probe Description abfangen. Ferner existieren die gesonderten Probes BEGIN und END, die zu Beginn bzw. am Ende eines D-Skripts auftreten. Sie erlauben uns, Variablen und Aggregations zu initialisieren und in einem eigenen Format auszugeben.

Schöner Artikel?

Hat Ihnen der Artikel gefallen? Dies und mehr ist alles Teil der Ausgabe 4.09 unseres PHP Magazins. Alle zwei Monate frisch am Kiosk! Zur jeweils aktuellen Ausgabe geht es hier.

Es ist zu beachten, dass wir in unserem Beispiel Thread-lokale Variablen verwenden. Die Syscall Probe wird für jeden fstat-Aufruf aktiviert, also auch für alle anderen laufenden Programme auf dem System. Eine Überprüfung, ob wir uns innerhalb des PHP-Skripts befinden, resultiert mit globalen Variablen daher in einer Race Condition zwischen parallel laufenden PHP-Prozessen. Erst die Verwendung der Thread-lokalen Variablen schränkt die Syscall Probes so ein, dass wir eine richtige Auswertung für die PHP-Prozesse bekommen. Im Gegensatz zu Probes aus Applikationen, die im Userspace laufen, können Kernel-Probes, wie die Syscall Probes, nicht an eine PID gebunden sein.

Fazit

Mit DTrace will Sun Microsystems dem Entwickler kein neues Analysetool an die Hand geben, sondern ein ganzes Tracing-Framework bereitstellen. Damit ist zwar eine höhere Einstiegshürde verbunden, die ein hohes Maß an Verständnis für das System erfordert – ist sie jedoch erstmal überwunden, hat der Entwickler mit DTrace ein mächtiges Werkzeug zur Hand, dass weitaus mehr Möglichkeiten bietet als hier gezeigt werden konnte. Neben der wachsenden Unterstützung für statische Probes in Programmiersprachen, darunter Python, Java und der JavaScript Engine von Mozilla Firefox, existieren darüber hinaus eine Vielzahl vorgefertigter DTrace-Skripte. Eine große Sammlung nützlicher Skripte für den täglichen Einsatz wurde im Rahmen des DTraceToolkit-Projekts zusammengestellt. Die Sammlung macht es auch für Neulinge einfach, DTrace zu verwenden und kann erfahrenen Entwicklern als Hilfe für die Entwicklung eigener Skripte dienen. Führend bei der Integration von neuen Probes ist OpenSolaris. MacOS-X-10.5-Anwender müssen die Unterstützung für statische Probes oftmals nachinstallieren oder die Anwendung neu kompilieren.

DTrace lohnt sich für Entwickler, die einen tiefen Einblick in die Abläufe einer komplexen Anwendung oder eine effiziente Möglichkeit benötigen, Fehler auf Produktionssystemen zu analysieren. Hierbei muss der Entwickler jedoch vorsichtig sein, da er zwar durch die DTrace-eigene Skriptsprache weitaus mehr Freiheiten als mit anderen Tools hat, sich aber nicht sicher sein kann, ob sein Skript auch immer fehlerfrei ist. So ist klar, dass DTrace keinen herkömmlichen Debugger oder Analysewerkzeuge ersetzen kann.

David Soria Parra ist Software Engineer bei Sun Microsystems und arbeitet an der Unterstützung von DTrace in PHP.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -