Mailinglisten-Archive |
Hi René,
Am Sonntag, 29. Oktober 2006 03:15 schrieb René Thiel:
> > Wird jede Zeile als einzelnes Insert an MySQL geschickt?
> > Wenn ja, kann man da sicher optimieren, entweder prepared Statement
> > benutzen oder mehrere Datensätze gleichzeitig eintragen.
> > (Beispiel: INSERT INTO table (id, name) VALUES (1, 'wert1'), (2,
> > 'wert'), ...)
>
> Diese Idee hatte ich auch schon, bringt bei 41.000 Datensätzen einen
> Geschwindigkeitsvorteil von ca. 30%.
30% sind schon mal eine erhebliche Verbesserung. :-)
> Das Problem sind aber die Log-Dateien mit fast 1.000.000 Zeilen
> (wie schon geschrieben dauert es schon 336 sec, um nur die einzelnen
> Zeilen unbearbeitet in die DB zu schreiben.)
>
> > Was Optimierung angeht kann ich ohne deinen Code zu kennen nur
> > raten. Was ist den das eigentliche Problem mit der Geschwindigkeit,
> > woher kommen diese 600 Sekunden?
>
> <?php
> .....
> $sql = "INSERT INTO `".$Jahr."_".$Standort."` ( ";
> $sql .= " `DateTime` ";
> $sql .= ", `IP` ";
> $sql .= ", `Call` ";
> $sql .= ", `Typ` ";
> $sql .= ", `Verbindungen` ";
> $sql .= ", `Kommentar` ";
> $sql .= ") VALUES ";
>
> foreach ($teile as $buffer)
> {
> if (preg_match("'( CALL )'",$buffer))
> {
> $teiler_1 = split(" ",$buffer);
> $DateTime = $Jahr."-".$month[$teiler_1[0]]."-".$teiler_1[1]."
> ".$teiler_1[2]; $IP = $teiler_1[3];
> $Call = $teiler_1[4]." ".$teiler_1[5];
> $Typ = $teiler_1[6];
> $entferner = "/".$teiler_1[0]." ".$teiler_1[1]."
> ".$teiler_1[2]." ".$teiler_1[3]." ".$teiler_1[4]." ".$teiler_1[5]."
> ".$teiler_1[6]."/";
> $teiler_3 = ltrim(preg_replace($entferner,"",$buffer));
> $teiler_4 = split(" ",$teiler_3);
> $Verbindungen = $teiler_4[0];
> $Verbindungen_x = "'".quotemeta($Verbindungen)."'";
> $Verbindungen = mysql_real_escape_string($Verbindungen);
> $Kommentar =
> ltrim(@preg_replace($Verbindungen_x,"",$teiler_3)); $Kommentar =
> mysql_real_escape_string($Kommentar);
>
> if ($Zaehler > 0) {$sql .= ", ";}
> $Zaehler++;
> $sql .= "( ";
> $sql .= " '".$DateTime."' ";
> $sql .= ", '".$IP."' ";
> $sql .= ", '".$Call."' ";
> $sql .= ", '".$Typ."' ";
> $sql .= ", '".$Verbindungen."' ";
> $sql .= ", '".$Kommentar."' ";
> $sql .= ") ";
> }
> }
> $ergebnis = @mysql_query($sql, $verbindung);
> .....
> ?>
>
> Aus:
> Jul 31 12:26:31 172.16.199.1 CALL 3 A:Disc 523->808704
> G729AB,60(56,3)/G729AB,0(0,0) TEL2:523:->GW1:808704: Cause: Normal
> call clearing
> mache ich damit:
> INSERT INTO `2006_816` (`DateTime`, `IP`, `Call`, `Typ`,
> `Verbindungen`, `Kommentar`) VALUES ('2006-07-31 12:26:31',
> '172.16.199.1', 'CALL 3', 'A:Disc', '523->808704',
> 'G729AB,60(56,3)/G729AB,0(0,0) TEL2:523:->GW1:808704: Cause: Normal
> call clearing'), ...
Es wäre sehr wichtig, Dein komplettes Skript zu kennen und genau zu
messen, an welchen Stellen wieviel Zeit gebraucht wird. Nur so wirst Du
die eigentlichen Zeitfresser entdecken.
Es fängt mit solchen Kleinigkeiten wie
preg_match("'( CALL )'",$buffer)
an, wo Du halt anstatt eines regulären Ausdruckes in diesem Fall viel
lieber strpos() oder strstr() verwenden solltest.
> > Wenn die Auswertung jahresweise geschieht, wird das Script ja nicht
> > sehr häufig aufgerufen
>
> Richtig, es geht hier nur um das Einlesen der Log-Files und deren
> Auswertung, die Anzeige der ausgewerteten Daten (inkl. Tortengrafik)
> ist unproblematisch.
>
> Allerdings sprechen wir über 19 Standorte und (bisher) 26 Monate -
> das dauert schon ein paar Tage.
>
> > wenn der Kunde im nächsten Jahr doppelt soviele Logfiles hat, muss
> > das auch irgendwie gehen.
>
> Nicht doppelt soviele Logfiles, aber wahrscheinlich noch größere -
> und genau deshalb meine Frage: Habe ich denen zu viel versprochen?
Also es gibt ganz grundsätzliche Lösungsansätze. Da Du keine Details
genannt hast, führe ich sie einfach mal auf. Vielleicht hast Du sie ja
schon umgesetzt, aber für alle Fälle... :-)
1. Logrotate verwenden und damit die Logs häufig rotieren, z.B. einmal
die Woche. Dann hast Du pro Woche eine Log-Datei. Damit werden die
einzelnen Dateien handhabbarer.
2. Anstatt die Log-Dateien einmal im Jahr in die Datenbank einzulesen,
die Dateien sofort nach der Rotation einlesen.
3. Nur Log-Dateien einlesen, die noch nicht eingelesen wurden.
D.h. der erste logische Ansatzpunkt ist, die zu verarbeitende Datenmenge
so zu verringern, daß sie handhabbarer wird.
Die nächsten Ansatzpunkte wäre dann schon im Skript. Exakte
Zeitmessungen, wo die Zeit liegen bleibt, wären daher sehr wichtig. Aus
eigener Erfahrung kann ich Dir sagen, daß man in einem Skript
tatsächlich oft exakt einzelne Funktionen als Zeitfresser
identifizieren kann. An diesen Stellen kannst Du dann ansetzen und nach
alternativen Programmierlösungen suchen, die performanter sind. Bei
großen Datenmengen wirkt sich das dann merklich aus. Grundsätzliche
Empfehlungen wären hier, was Du ja sicher ohnehin schon machst:
Lies die Log-Dateien immer nur Zeilenweise ein und behalte nur soviel
Daten im Speicher, wie Du gerade aktuell verarbeiten mußt. Also ein
Minimum an Resourcen einsetzen.
Ein Flaschenhals Deines Ansatzes scheint die Datenbankverbindung zu sein
(TCP/IP-Verbindungen auf einen anderen Rechner sind hier übrigens
langsamer, als Localhost-Socket-Verbindungen). Daher habe ich einen Tip
für Dich. Ich habe letztes Jahr einmal vor einem ähnlichen Problem
gestanden, obwohl es da noch um weit geringere Datenmenge ging. Damals
habe ich eine Lösung gefunden, um Daten in die Datenbank zu bekommen,
die gegenüber INSERTs wirklich extrem performant ist. Diese Lösung
sollte Dein Skript deutlich beschleunigen und geht so:
Anstatt die Daten mit INSERTs in die Datenbank zu schreiben, liest Du
die Daten zeilenweise aus der Log-Datei aus und schreibst sie im
gleichen Augenblick auseinandergenommen zeilenweise in eine andere
csv-ähnliche Textdatei weg, wobei Du eine bestimmte Syntax einhältst.
So hast Du nebenbei immer nur genau eine Zeile im Speicher. Wenn Du die
Textdatei fertig geschrieben hast, liest Du sie mit einem einzigen
SQL-Befehl, LOAD DATA INFILE, in die Datenbank ein:
http://dev.mysql.com/doc/refman/4.1/en/load-data.html
Ich kann Dir versprechen, daß die Daten mit einer Dir bisher
unbekannten, unglaublichen Geschwindigkeit eingelesen werden... ;-)
Beachte allerdings, daß hier spezielle Rechte benötigt werden:
"For security reasons, when reading text files located on the server,
the files must either reside in the database directory or be readable
by all. Also, to use LOAD DATA INFILE on server files, you must have
the FILE privilege. See Section 5.7.3, 'Privileges Provided by MySQL'"
Wenn Du alle diese Tricks ausgeschöpft hast, kannst Du dann noch ein
Bißchen am Skriptparameter max_execution_time schrauben, z.B. auch mit
set_time_limit().
Zusätzlich könntest Du mal gucken, ob das PHP-Skript schneller läuft,
wenn Du es nicht über den Browser, sondern über die Kommandozeile
ausführst. Kommandozeile ist übrigens ein gutes Stichwort:
Wenn Du die von mir vorgeschlagenen Lösungen implementierst, dann
brauchst Du übrigens kein PHP-Skript. Ich habe das damals mit einem
Shellskript gelöst. Das Shellskript kann den LOAD DATA INFILE-Befehl
nämlich über den mysql-Befehl absetzen und dabei die Benutzerkennung
mitgeben. :-) In dem Zusammenhang kannst Du auch über Cronjobs
nachdenken, die nach dem von mir erwähnten Logrotate dann automatisch
die Daten in die Datenbank schreiben. (PHP über CLI hat übrigens
standardmäßig eine unbegrenzte max_execution_time.)
Wie Du siehst, gibt es also glücklicherweise eine Menge Ansätze, der
Datenmenge Herr zu werden. Ich denke also nicht, daß Du Deinem Kunden
zuviel versprochen hast. :-)
Viele Grüße
Lutz
php::bar PHP Wiki - Listenarchive