Tuning mod_perl | ||
|
Tunen im mod_perl-UmfeldDas ProblemDie Aufgabe: Das Webfrontend von OpenSMART ist zu langsam. Erste und häufigste Antwort: Nimm "mod_perl". Gesagt, getan, die Anpassungen im Script sind überschaubar, die Konfiguration von Apache auch. Etwas schneller kommt mir das ja schon vor, aber ist es das auch wirklich ? Und: Läuft es wirklich unter mod_perl ? Regel 1: Umgebung für einfache, reproduzierbare Tests schaffenDa kommen wir ja gleich zu einer der großen Regeln beim Tunen (von egal was): Schaff dir erstmal eine Umgebung, in der du aussagekräftige Werte möglichst einfach und möglichst reproduzierbar bekommst. Wie schafft man das in einer Web-Umgebung ? SiegeDie Antwort heisst siege. Siege ist ein Stress-Tester / Antwortzeitmessungstool für Webserver und Webapplikationen. Damit kann ich also einfach mehrere Requests gleichzeitig und insgesamt viele hintereinander auf meine Applikation loslassen und bekomme hinterher vernünftige Laufzeitergebnisse. Also lasse ich siege -c 5 -r 10 http://localhost/cgi-bin/monitorgui.cgi5 gleichzeitige User mit 10 Wiederholungen auf meine Applikation los. Das Ergebnis: Transactions: 50 hits Availability: 100.00 % Elapsed time: 67.44 secs Data transferred: 656400 bytes Response time: 5.85 secs Transaction rate: 0.74 trans/sec Throughput: 9733.10 bytes/sec Concurrency: 4.34 Successful transactions: 50 Failed transactions: 0 Longest transaction: 8.13 Shortest transaction: 2.06(die absoluten Zahlen sind eigenlich nebensächlich, wir wollen einfach später noch damit vergleichen). Läuft die Applikation wirklich unter mod_perl ?Nur: Läuft das jetzt unter mod_perl oder noch als normales CGI-Script ? Ein "top" bringt es an den Tag: Während siege läuft, sind viele "monitorgui.cgi"-Prozesse zu finden. Schade, mod_perl läuft noch nicht. OK, Konfig geprüft, Fehler gefunden (hier: Es war ein Tippfehler in der Konfiguration), apache neu gestartet, siege nochmal angestartet. Das Ergebnis: Transactions: 50 hits Availability: 100.00 % Elapsed time: 34.98 secs Data transferred: 652650 bytes Response time: 2.12 secs Transaction rate: 1.43 trans/sec Throughput: 18657.80 bytes/sec Concurrency: 3.04 Successful transactions: 50 Failed transactions: 0 Longest transaction: 12.79 Shortest transaction: 0.28Die durchschnittliche Antwortzeit hat sich halbiert, Allerdings gibt es immer noch vereinzelt sehr lange Transaktionen. Und: top zeigt, dass "httpd"-Prozesse laufen, nicht mehr monitorgui.cgi. Wir sehen: Aha, mod_perl hat unsere Antwortzeiten halbiert. Regel 2: so viel Profilen wie möglichApache::DProfAber ist das alles, geht da nicht noch mehr ? Und wo werden die Antwortzeiten verbraten ? Die Antworten darauf liefert "Apache::DProf", ein Perl-Modul (zu finden auf CPAN) zum Profilen von mod_perl. Apache::DProf installieren, konfigurieren, Apache neu starten, siege laufen lassen (und spätestens hier erkennen wir, dass es sinnvoll ist, automatische Lasttests zu bauen), und Apache::DProf-Ausgaben mit "dprofpp" auswerten. Offensichtlich ist db_getStatus_Complete ein deutlicher Zeitfresser. Benchmark, Devel::DProfMal sehen, wie können wir das auseinandernehmen ? Das können wir ja auch ohne mod_perl/apache einfach so auf der Kommandozeile profilen: use lib "/home/osmart/lib"; use Benchmark qw( :all ); use OpenSMART::dbaccess; timethis(30,"db_getStatus_Complete");Das liefert einem jetzt die Laufzeit für 30 Aufrufe von "db_getStatus_Complete" (da ist es wieder: Automatische Tests mit Zeitaussagen sind toll.) Das jetzt noch gebündelt mit Profiling: perl -d:DProf ./benchmark dprofpp -r tmon.outDas erste Kommando läßt den Profiler mitlaufen (sehr ähnlich wie weiter oben Apache::DProf) und erzeugt eine Datei "tmon.out", die von "dprofpp" analysiert und in einem vernünftigem Format ausgegeben wird. "-r" gibt "echte Zeiten", nicht nur verbrauchte CPU-Minuten an. Das halte ich bei Datenbank-Abfragen für den besseren Weg, schließlich kostet IO keine CPU, aber trotzdem Zeit. Ergebnis hier:
%Time ExclSec CumulS #Calls sec/call Csec/c Name
32.6 2.380 2.380 30 0.0793 0.0793 DBI::st::execute
29.1 2.124 7.586 30 0.0708 0.2529 OpenSMART::dbaccess::db_getStatus_
Complete
13.9 1.018 1.514 37050 0.0000 0.0000 OpenSMART::debugmod::logit
6.81 0.496 0.496 74100 0.0000 0.0000 Log::Log4perl::Logger::__ANON__
5.35 0.390 0.390 30 0.0130 0.0130 DBD::Pg::db::_login
4.93 0.359 1.039 12330 0.0000 0.0001 OpenSMART::debugmod::logit_dumper
3.71 0.270 0.270 30 0.0090 0.0090 DBI::st::fetchall_arrayref
Wir sehen, dass wir ein Drittel der Zeit in der Datenbank und ein
weiteres knappes Drittel in der Logik von db_getStatus_Complete
verbringen. Ausserdem ist offensichtlich logit (das ist ein
Wrapper um Log4perl) nicht wirklich performant).
Regel 3: Die größten Verbraucher zuerst tunenWir haben jetzt 3 Möglichkeiten, weiter vorzugehen:
Tun wir mal so für diese Beschreibung, als ob das SQL-Statement und die Datenbank-Einstellungen getuned wären. Bleibt noch die Logik von db_getStatus_Complete und logit. Weil's einfacher und schneller geht, ziehen wir das Tunen von logit vor, auch wenn das im Gegensatz zu Regel 3 steht. :-) Tunen von logit:logit ist ein OpenSMART interner Wrapper um Log4perl. Eigentlich sollen die Aufrufe nur im Debug-Fall dazu dienen, sinnvolle Log-Ausgaben zu bekommen. Der fragliche Code innerhalb von db_getStatus_Complete sieht so aus:
my $rows = $sth->fetchall_arrayref();
while ( my $row = ( shift(@$rows)) ) {
logit_dumper($logger,'debug',
"Host/check/cp: in maintenance ?",$row);
... # Irgendwelche Logik
logit($logger,'debug',"In Maintenance: $maintenance_found");
... # Noch mehr Logik
}
}
Wir sehen, innerhalb einer Schleife, die über die gesamten Datenbank-Ergebnisse geht, wird einmal logit und einmal logit_dumper (das wiederum hauptsächlich logit aufruft) aufgerufen. Regel 4: In große Schleifen nur das hineinschreiben, was wirklich hinein muß!Nun, wir haben etwa 1300-1500 Ergebnis-Zeilen zu bearbeiten, also 2600-3000 logit-Aufrufe. Aber eigentlich kann die Log-Ausgabe auch vor der Schleife erfolgen. Gesagt, getan, 13% Laufzeit gespart. Tunen der Logik innerhalb von db_getStatus_CompleteDer Code sieht inzwischen so aus:
my $rows = $sth->fetchall_arrayref();
logit_dumper($logger,'debug',
"All host/check/cp from database",$rows);
while ( my $row = ( shift(@$rows)) ) {
my $maintenance_found=0;
for my $x (0 .. $#{$maint}) {
if ($row->[0] =~ m/$maint->[$x]->[0]/
and
$row->[1] =~ m/$maint->[$x]->[1]/
and
$row->[2] =~ m/$maint->[$x]->[2]/
) {
.... # Mach irgendwas damit.
last;
}
}
... # Noch mehr Logik
}
Innerhalb der großen (1300-1500-fach)-while-Schleife gibt es eine weitere for-Schleife mit ca. 5-20 Durchläufen, in denen einige reguläre Ausdrücke überprüft werden. Regel 5: Statische Reguläre Ausdrücke sind oftmals schneller als dynamische.Ich weiss über die Applikation, dass in den 1300-1500 Durchläufen der äußeren Schleife nur in sehr wenige (5-20) Durchläufen die if-Bedingung erfüllt wird. Also liegt es nahe, die einzelnen $maint->[$x]->]0]Ausdrücke von Hand in einen großen, statischen regulären Ausdruck zusammenzubauen:
my $host_regex;
my $x;
for $x (0 .. $#{$maint}) {
$host_regex .= "$maint->[$x]->[0]|";
}
$host_regex =~ s/\|$//;
In der Schleife wird dann zuerst auf diesen großen regulären Ausdruck geprüft und dann kann ja meistens schon nach der IF-Bedingung weitergemacht werden:
while ( my $row = ( shift(@$rows)) ) {
my $maintenance_found=0;
if ($row->[0] =~ m/$host_regex/o) {
for my $x (0 .. $#{$maint}) {
if ($row->[0] =~ m/$maint->[$x]->[0]/
and
$row->[1] =~ m/$maint->[$x]->[1]/
and
$row->[2] =~ m/$maint->[$x]->[2]/
) {
... # mach irgendwas
last;
}
}
}
... # Noch mehr Logik
}
Voila, 5-10% schneller. ZusammenfassungNatürlich tuned sich jede Applikation anders, aber die Hilfsmittel Apache::DProf, Devel::DProf und Benchmark können Nadelöhre doch zielgerichtet auffinden. |