Andreas Korthaus: /PERL Performance-Frage - Parsen

Hallo!

Ich habe kürzlich eine ausführliche Diskussion von wegen Synchronisation und Datenübertragung mit Klaus Mock geführt(</archiv/2002/9/24654>). Dabei ging es um das folgende Datenformat, welches per HTTP von einem zum anderen Server geschickt wird:

[TABELLENNAME]
ID:4711
HERKUNFT:123
NAME:ABCDEF
WERT:1234
MEMO:xyzdsdskdkjdsk
MODFLAG:1
[EOR]
ID:4712
HERKUNFT:123
NAME:ABCDEF
WERT:1234
MEMO:xyzdsdskdkjdsk
MODFLAG:1
[EOR]
...
[ANDERE_TABELLE]
ID:815
HERKUNFT:123
...

Die Frage ist jetzt, wie man diese Daten am performantesten mit PHP parsen kann, da es sich durchaus um einige 1000 Datensätze handeln kann.
Klaus hatte für PERL folgendes vorgeschlagen:

my($tabelle,%daten);
foreach(@inputlines) {
 chomp;
 if(/^[EOR]$/) {
    run_import_record($tabelle,%daten) if $tabelle;
  %daten = ();
  }
 elsif(/^[(.+)]$/) {
  $tabelle = $1;
  }
 else {
  my($name,$wert) = split('\s*:\s*',$_,2);
  $daten{$name}=$wert;
  }
 }

In PHP könnte das dann so aussehen:

foreach($inputlines as $line) {
    if (preg_match("/^[EOR]$/", $line)) {
        run_import_record($tabelle,$daten);
        unset($daten);
    }
    else if(preg_match("/^[(.+)]$/", $line, $matches)) {
        $tabelle = $matches[1];
    }
    else {
        list($name, $wert) = preg_split("/:/", $line, 2);
        $daten[$name]=$wert;
    }
}

Das ganze soll in PHP laufen(oder würde PERL einen erheblichen Performance-Vorteil bedeuten?).

Es gibt 2 mögliche Varianten:
1. Übertragung direkt als String in einer POST-Variable
2. Übertragung als multipart/form-data, also als "Datei-Upload".

Beim 2. Ansatz könnte ich in PHP die temporäre Datei direkt mit file() öffnen und hätte einen Array mit Zeilen, den ich wie oben abarbeiten könnte.

Beim ersten Ansatz müßte ich erst bei \n splitten, was nicht unbedingt optimal sein sollte, aber ich weiß keine besser Möglichkeit diesen String zu parsen. Hat vielleicht jemand ne Idee wie man die Performance des Parsens noch verbessern könnte?

Viele Grüße
Andreas

  1. Hi,

    Die Frage ist jetzt, wie man diese Daten am performantesten mit PHP parsen kann, da es sich durchaus um einige 1000 Datensätze handeln kann.

    wie regelmäßig findet die Synchronisation statt? Wenn es sich um große Datenmassen handelt macht das nur Sinn, wenn es sich z.B. um einen täglichen oder manuellen Abgleich handelt. Dabei ist die Performance unerheblich - es kommt nicht auf Zeit an.

    Klaus hatte für PERL folgendes vorgeschlagen:

    Auf CPAN (http://www.cpan.org/) findest Du das Modul Config::IniFiles, welches AFAIK exakt leisten kann, was Du brauchst.

    1. Übertragung direkt als String in einer POST-Variable
    2. Übertragung als multipart/form-data, also als "Datei-Upload".

    1. ist zu bevorzugen, weil bei 2. ein erheblicher semantischer und datenbehafteter Overhead stattfindet. Es ist also "mehr und komplizierter", ohne Dir einen direkten Vorteil zu bieten.

    Beim ersten Ansatz müßte ich erst bei \n splitten, was nicht unbedingt optimal sein sollte,

    Ist der Arbeitsspeicher sooo knapp, dass ein paar Kilobyte Daten nicht auch zwei mal drin stehen können? :-)

    Hat vielleicht jemand ne Idee wie man die Performance des Parsens noch verbessern könnte?

    Wie gesagt, ich halte das nicht für relevant. Natürlich darf man nicht sinnfrei Ressourcen verschwenden; aber einen erheblichen Aufwand zu treiben, um ein paar hundertstel Sekunden zu sparen, obwohl das ganze in Gottfrieds Namen auch eine Minute dauern kann, ist einfach vergeudete Liebesmüh. Wenn es sich um instantan getriggerte Aktionen handelt ("wenn auf Server A etwas passiert, dann teile das sofort Server B mit"), sieht die Sache natürlich anders aus - allerdings sollte dann in erster Linie die Datenmenge auf ein Minimum reduziert werden, sofern das möglich ist.

    Cheatah

    1. Hallo,

      Auf CPAN (http://www.cpan.org/) findest Du das Modul Config::IniFiles, welches AFAIK exakt leisten kann, was Du brauchst.

      Nicht ganz;-) Es sieht nur ähnlich aus, ist es aber nicht. Abgesehen davon glaube ich, daß dieses Modul auch schon zu übermotorisiert wäre, wenn es denn wenigstens passen würde.

      Ansonst: full ack.

      Grüße
        Klaus

      1. Hallo!

        Ansonst: full ack.

        OK. Über 1000 ist wirklich nur da um nach längerer Zeit(Urlaub...) noch eine Synchronsiation durchführen zu können, es soll halt funktionieren und dabei nicht an Limits des gehosteten Servers stoßen, denn hier sind CPU-Zeit, RAM und Laufzeit beschränkt.
        Im Alltagsgebrauch soll die Synchronisation durchaus automatisiert gebraucht werden, nur wird es sich hier meist um deutlich unter 100 Datensätze handeln, halt je öfter desto weniger.
        Da ich evtl. vor/nach bestimmten Schreibvorgängen automatisch synchronisieren will, sollte das dann aber keine 40 Sekunden dauern!
        Die zu übertragenden Daten habe ich so gut wie möglich eingegrenzt.
        Warum ich über die 2. Möglichkeit(multipart/form-data) nachdenke, ist da ich die Daten hier als Binästring übertragen kann, z.B. gzip, was über 80% Einsparung bedeuten würde. Mit der ersten Variante ist das leider nicht ohne weiteres möglich. Und wenn das Übertragungsvolumen schon doppelt oder drei mal so groß ist, ist das eine ziemliche Zeitverschwendung, vor allem wenn der eine Server nur über DSL oder ISDN angebunden ist.
        Also meint Ihr man kann das Anfangs gepostete Script nicht mehr beschleunigen? Ich will ja nichts großartiges machen, aber meist gibt es ja recht einfache Wege dieses performanter zu gestaltetn, auf die ich halt nicht komme. Vor allem erst ein split, dann ein foreach über den geschaffenen Array, und dann halt lauter Reguläre Ausdrücke... aber vielleicht ist es so auch schon das beste, vielleicht hätte ja jemand eine Idee gehabt.
        Nur was noch dazu kommt, die aufgerufene Import-Funktion muß ja auch jedesmal erst mit einem SELECT ermitteln ob der Datensatz vorhanden ist und dann wieder in einer foreach-Schleife aus dem Daten-Array ein UPDATE oder ein INSERT machen und ausführen, da wird das ganze schon erheblich langsamer!

        Viele Grüße
        Andreas

        1. Hallo,

          OK. Über 1000 ist wirklich nur da um nach längerer Zeit(Urlaub...) noch eine Synchronsiation durchführen zu können, es soll halt funktionieren und dabei nicht an Limits des gehosteten Servers stoßen, denn hier sind CPU-Zeit, RAM und Laufzeit beschränkt.

          Na, irgendwie habe ich das Gefühl, daß bei so einer Anwendung ein Massenhoster nicht gerade geeignet ist. imho wäre es Zeit sich zu überlegen, ob man nicht mit einem eigenen Server dann schon besser fährt. xDSL und Co. sind ja auch nicht mehr sooo teuer, daß man diesen Schritt nicht überlegen sollte.

          Wenn die Synchronisation nahc längerer Offlinezeit läänger braucht, dann sollte das eigentlich jedem klar sein. Und wenn Resourcen wirklich ein Problem darstellen, könntets DU ja in Deinem Synchronisationsschript Grenzen einbauen, sagen wir mal immer in 200er Schüben (wobei diese Zahl jetzt willkürlich ist), und die Synchronisation so oft wiederholen, bis alles da ist.

          Warum ich über die 2. Möglichkeit(multipart/form-data) nachdenke, ist da ich die Daten hier als Binästring übertragen kann, z.B. gzip, was über 80% Einsparung bedeuten würde.

          Wobei Du bedenken mußt, daß die Verkleinerung der übertragenen Daten gleichzeitig auch erhöhten Rechenaufwand (de-/komprimieren) und Speicherplatz (komprimierte Daten und entpackte) bedeutet. Es ist eben abzuwägen, was mehr schmerzt.

          Also meint Ihr man kann das Anfangs gepostete Script nicht mehr beschleunigen?

          Irgendwie geht immer noch etwas. Aber ich vertrete da eher den Standpunkt, daß man sich erst kratzen soll, wenn es juckt.
          Versuche einen Protottypen zu schreiben, der mit Beispieldaten handiert, um zu sehen, wie lange es wirklich dauert, und ob Du damit leben kannst.

          Nur was noch dazu kommt, die aufgerufene Import-Funktion muß ja auch jedesmal erst mit einem SELECT ermitteln ob der Datensatz vorhanden ist und dann wieder in einer foreach-Schleife aus dem Daten-Array ein UPDATE oder ein INSERT machen und ausführen, da wird das ganze schon erheblich langsamer!

          Gut an dem kommst Du wahrscheinlich sowieso nicht vorbei, egal wie Du es anstellst, wozu sich also darüber Gedanken machen.

          Grüße
            Klaus

          1. Hallo!

            Na, irgendwie habe ich das Gefühl, daß bei so einer Anwendung ein Massenhoster nicht gerade geeignet ist. imho wäre es Zeit sich zu überlegen, ob man nicht mit einem eigenen Server dann schon besser fährt. xDSL und Co. sind ja auch nicht mehr sooo teuer, daß man diesen Schritt nicht überlegen sollte.

            Ja, das ist schon richtig, zum ienen Muß der Server der Webseite mit eingebunden werden, und ob man unbedingt den Webserver einer Internetseite selbst betreiben sollte ist fraglich, denn dazu braucht es erheblich mehr als die Hardware und einen SDSL-Zugang!

            Wenn die Synchronisation nahc längerer Offlinezeit läänger braucht, dann sollte das eigentlich jedem klar sein. Und wenn Resourcen wirklich ein Problem darstellen, könntets DU ja in Deinem Synchronisationsschript Grenzen einbauen, sagen wir mal immer in 200er Schüben (wobei diese Zahl jetzt willkürlich ist), und die Synchronisation so oft wiederholen, bis alles da ist.

            Gute Idee! So werde ich das zur Not machen, muß aber estmal mit "live-Daten" testen udn mir dann was entsprechendes überlegen.

            Warum ich über die 2. Möglichkeit(multipart/form-data) nachdenke, ist da ich die Daten hier als Binästring übertragen kann, z.B. gzip, was über 80% Einsparung bedeuten würde.

            Wobei Du bedenken mußt, daß die Verkleinerung der übertragenen Daten gleichzeitig auch erhöhten Rechenaufwand (de-/komprimieren) und Speicherplatz (komprimierte Daten und entpackte) bedeutet. Es ist eben abzuwägen, was mehr schmerzt.

            Naja, das komprimieren dauert wirkich nur Sekundenbruchteile. Was schon etwas langsamer ist ist die zusätzliche Verschlüsselumng die ich auch noch einbauen muß, vermutlich läuft es auf symetrisches GPG hinaus. Wenn bei der Übertragung 10 KB eingespart werden, sind das bei  ISDN 1-2 Sekunden, das die engste Stelle denke ich, daher sollte ich wohl auch hier ansetzen.

            Also meint Ihr man kann das Anfangs gepostete Script nicht mehr beschleunigen?

            Irgendwie geht immer noch etwas. Aber ich vertrete da eher den Standpunkt, daß man sich erst kratzen soll, wenn es juckt.
            Versuche einen Protottypen zu schreiben, der mit Beispieldaten handiert, um zu sehen, wie lange es wirklich dauert, und ob Du damit leben kannst.

            Ja, daran arbeite ich. Ich hatte nur gedacht, wenn ich denn von vorn herein schon ein möglichst performantes Script verwende ist das ja auch nicht verkehrt!

            Nur was noch dazu kommt, die aufgerufene Import-Funktion muß ja auch jedesmal erst mit einem SELECT ermitteln ob der Datensatz vorhanden ist und dann wieder in einer foreach-Schleife aus dem Daten-Array ein UPDATE oder ein INSERT machen und ausführen, da wird das ganze schon erheblich langsamer!

            Gut an dem kommst Du wahrscheinlich sowieso nicht vorbei, egal wie Du es anstellst, wozu sich also darüber Gedanken machen.

            Naja, eine Idee habe ich schon:
            Ich speichere ein Flag zu jedem Datensatz in jeder Tabelle, und zwar ob bereits auf Master eingetragen oder nicht. Meinetwegen 0 heißt "noch nicht gespeichert" und 1 entsprechend.
            Wenn dieses Flag auf 1 Steht generiere ich ein Update, wenn es auf 0 steht generiere ich ein Insert. Gleichzeitig generiere ich mit den Inserts Updates für den Client, das die Flags auf 1 gestellt werden, wenn die Inserts erfolgreich waren.  Die Inserts und Updates schreibe ich erstmal in einen String, den ich dann mit dem Kommandozeilentool mysql im Stapelmodus an MySQL schicke.

            So kann ich das ganze zumindest auf dem Master erheblich beschleunigen. Leider geht das nicht anders herum, da der Master nicht über jeden Client so viele Informationen speichern kann.
            Das ganze Problem könnte ich lösen, wenn ich einfach eine Spalte mit "Erstellzeitpunkt" einfügen würde, denn mit dieser Information, + der ClientID + Timestamp + Zeitpunkt der letzten Synchronisation habe ich alle Infos die ich brauche.
            Das habe ich eigentlich anfangs ausgeschlossen, da ich bei "normalen" INSERTs in der Anwendung keine Rücksicht auf die Synchronisation nehmen wollte. Aber das funktioniert sowieso nicht, zumindest in meinem Fall werde ich die IDs(CHAR, bestehend aus ClientID.DatensatzID) ja mit einem anschließenden Update einfügen, um eindeutige IDs zu generieren, dazu würde ich dann eh eine Insert-Funktion schreiben, und da könnte ich auch direkt die Erstellzeit mit einfügen.
            Das Problem ist das das ganze alles nur in meinem Kopf ist, ich bisher nur kleine Teile ausprobiert habe, da ich Angst habe alles doppelt und dreifach zu machen. Aber langsam sollte ich wohl mal eine Version umsetzen, denn nur daran kann ich sehen ob das im Betrieb gut funktioniert und einfach zu implementieren ist.

            Viele Grüße
            Andreas

            1. Hallo,

              Ja, das ist schon richtig, zum ienen Muß der Server der Webseite mit eingebunden werden, und ob man unbedingt den Webserver einer Internetseite selbst betreiben sollte ist fraglich, denn dazu braucht es erheblich mehr als die Hardware und einen SDSL-Zugang!

              So kompliziert ist nun auhc wieder nicht, einen Webserver vernünftig aufzusetzen. Außerdem gibt's auch dafür jede Menge Unterstützung;-)

              Also meint Ihr man kann das Anfangs gepostete Script nicht mehr beschleunigen?

              Ich hab' mal einen schnellen Tets gemacht, und an die 50000 Datensätze auf der mir beschriebenen Weise eingelesen (== 10MB bei meinen vorhandenen Daten). Rein nur Öfnnen und analysieren duaert auf meinem Notebook (P3 800 unter Win2K) ca. 7 Sekunden.

              Gut an dem kommst Du wahrscheinlich sowieso nicht vorbei, egal wie Du es anstellst, wozu sich also darüber Gedanken machen.

              Naja, eine Idee habe ich schon:
              Ich speichere ein Flag zu jedem Datensatz in jeder Tabelle, und zwar ob bereits auf Master eingetragen oder nicht. Meinetwegen 0 heißt "noch nicht gespeichert" und 1 entsprechend.

              Naja, ich hatte kurz auch über so etwas nachgedacht, allerdings muß dann gewährleistet sein, daß die zentrale Datenbank nicht inkosistent wird. Mit der Überprüfung auf Existenz kannst DU eigentlich auch ein Fallbacksystem integrieren, das die Daten aus den Filialen im zentralen Datnebestand rekonstruieren kann. Außerdem kannst DU dann gleich mit zwei ID's in den Filialen arbeiten, dann brauchst DU DIr keine GEdanken mehr über die Nummerngeneratoren machen;-)

              Grüße
                Klaus

        2. Hi Andreas,

          Nur was noch dazu kommt, die aufgerufene Import-
          Funktion muß ja auch jedesmal erst mit einem SELECT
          ermitteln ob der Datensatz vorhanden ist und dann
          wieder in einer foreach-Schleife aus dem Daten-Array
          ein UPDATE oder ein INSERT machen und ausführen

          wie ist die Wahrscheinlichkeit für diese beiden Ereignisse?
          Ich tippe, es geht schneller, auf Verdacht das wahrscheinlichere Ereignis zu wählen und ggf. eine Fehlerbehandlung durchzuführen, also den anderen Befehl in diesem Fall auch noch hinterher zu schießen (der erste hatte dann ja keine Wirkung).

          Nur wenn die Wahrscheinlichkeit zwischen 30 und 70% liegt, dürfte das vorherige SELECT schneller sein.
          Je schiefer die Verteilung, desto besser ist Raten.

          Viele Grüße
                Michael

          1. Hi Michael!

            wie ist die Wahrscheinlichkeit für diese beiden Ereignisse?

            Das ist sehr verschieden. Normalerweise liegt der Schwepunkt deutlich bei den Updates, aber manchmal wird eine größere Anzahl an Datensätzen hintereinander angelegt, wodurch das Verhältnis dann auch mal kippen kann.

            Ich tippe, es geht schneller, auf Verdacht das wahrscheinlichere Ereignis zu wählen und ggf. eine Fehlerbehandlung durchzuführen, also den anderen Befehl in diesem Fall auch noch hinterher zu schießen (der erste hatte dann ja keine Wirkung).

            Aber wie stellst Du Dir das vor? Nur mal so angenommen ich nehme jetzt die UPDATES als Standard, dann generiere ich rstmal ein UPdate und führe  es aus. Wenn der Datensatz nicht vorhanden war, erhalte ich von mysql einen Fehler und kann dann ein INSERT generieren und ausführen, oder wie stellst Du Dir das vor?
            Aber ich hatte ja überlegt das Statement nicht in jedem Schleifendurchlauf einzelnd auszuführen, sondern alle in einen String schreiben und alle komplett an das Kommandozeilentool zu schicken. Da ist es ja dann schon zu spät für sowas, oder? Aber ich muß noch testen was genau wieviel Zeit kostet, leider hänge ich noch an der Übertragung der Daten als binären gz-String, wenn das denn endlich mal funktioniert werde ich mal eine Testversion umsetzen und ein wenig probieren!

            Nur wenn die Wahrscheinlichkeit zwischen 30 und 70% liegt, dürfte das vorherige SELECT schneller sein.

            Ich hatte erst überlegt die Tabelle vorher abzufragen und alle notwendigen Daten in einen Array zu schreiben, um mir die vielen DB-Zugriffe zu sparen. Aber ich müßte dann irgendwie die Datensätze filtern, damit der Zugriff und der entstehende nicht unnötig groß bzw. zu groß werden kann. Evtl. könnte ich es auch anders herum machen und die geparsten Daten kopmplett in einen Array schreiben und dann die DB entsprechend abfragen, danach entsprechend INSERTs und UPDATEs generieren und alle Zusammen an mysql(batch) schicken, oder was meinst Du dazu?

            Viele Grüße
            Andreas