Hans: Einfacher Counter performance touch()

hallo,

wenn jemand beginnt PHP zu lernen, gehört oft am Anfang die Erstellung eines einfachen Counters dazu. Überwiegend wird dann auf die klassische Methode verwiesen:

=> Eine Datei => Zahl rein => Datei auslesen => Zahl++ => Datei mit neuer Zahl speichern

Ich habe mir nun eine andere Methode überlegt und wüsste gerne, ob diese schlechter wäre, bzw. welche im Innenleben von PHP besser/schlechter verarbeitet wird?

=> Eine Datei Dateiname = 0.txt => Dir auslesen um Datei zu finden => Touch()Dateiname++ Also 1.txt, 2.txt, usw...

Meine zweite Frage wäre: Gibt es noch andere Alternativen für diesen Zweck, als diese Beiden?

  1. Moin,

    wenn jemand beginnt PHP zu lernen, gehört oft am Anfang die Erstellung eines einfachen Counters dazu.

    ach, ist das so?

    => Eine Datei => Zahl rein => Datei auslesen => Zahl++ => Datei mit neuer Zahl speichern

    Und oft wird dann vergessen, auf die Stolperfallen hinzuweisen. Zum Beispiel den konkurrierenden Zugriff auf die Datei durch fast zeitgleich ablaufende Requests. Das mag bei kleinen privaten Sites mit einer Handvoll Zugriffen pro Tag noch keine Rolle spielen, zumal es da auch kein Beinbruch ist, wenn der Zählerwert falsch wird. Trotzdem sollte man sich der Problematik widmen.

    Ich habe mir nun eine andere Methode überlegt und wüsste gerne, ob diese schlechter wäre, bzw. welche im Innenleben von PHP besser/schlechter verarbeitet wird?
    => Eine Datei Dateiname = 0.txt => Dir auslesen um Datei zu finden => Touch()Dateiname++ Also 1.txt, 2.txt, usw...

    Insgesamt hast du eine Dateioperation weniger, nämlich das tatsächliche Lesen/Schreiben des Dateiinhalts. Eventuell könnte die Performance hier "a Muggasäckle"[*] besser sein als bei der zuerst beschriebenen Lösung. Aber spielt das eine Rolle?
    Zumal deine alternative Methode nach und nach das Verzeichnis mit nutzlosen Dateien zumüllt, die man eigentlich auch irgendwann wieder entsorgen sollte - und schon ist der Vorteil "eine Dateioperation weniger" wieder dahin.
    Die Problematik mit konkurrierenden Zugriffen besteht auch weiter.

    Meine zweite Frage wäre: Gibt es noch andere Alternativen für diesen Zweck, als diese Beiden?

    Das beste ist IMO immer noch, auf derartige Counter zu verzichten und für Statistiken die Log-Dateien zu verwenden, die der Webserver ohnehin führt. Das ist für den Server kein zusätzlicher Aufwand, und du bekommst eventuell noch wesentlich mehr Informationen.

    Deutlich mehr Aufwand (aus Rechnersicht) ist, den Zähler in einer DB zu halten. Dort hast du dann auch den Vorteil, dass du dich um gleichzeitige Zugriffe nicht mehr kümmern musst; das kann die DB für dich sauber aufdröseln.

    Schönen Sonntag noch,
     Martin

    [*] "Muggasäckle": Schwäbisch-volkstümlicher Ausdruck für "ein ganz kleines bisschen"

    --
    They say hard work never killed anyone, but I figure, why take the risk?
      (Ronald Reagan, US-Präsident 1981-1989)
    Selfcode: fo:) ch:{ rl:| br:< n4:( ie:| mo:| va:) de:] zu:) fl:{ ss:) ls:µ js:(
    1. Hallo Martin,

      Zumal deine alternative Methode nach und nach das Verzeichnis mit nutzlosen Dateien zumüllt, die man eigentlich auch irgendwann wieder entsorgen sollte - und schon ist der Vorteil "eine Dateioperation weniger" wieder dahin.
      Die Problematik mit konkurrierenden Zugriffen besteht auch weiter.

      Verzeichnis zugemüllt? Wie kommst Du darauf, nur eine Datei erhält ständig einen neuen Namen:
      touch()

      Ja, das mit den gleichzeitigen Zugriff betrifft beide Methoden und bringt mich mal zu einer neuen Frage: Wieviel  exakt ( auf die Microsekunde genau) gleichzeitige Zugriffe kann PHP verkraften und geht wie damit um?

      > Das beste ist IMO immer noch, auf derartige Counter zu verzichten und für Statistiken die Log-Dateien zu verwenden, die der Webserver ohnehin führt. Das ist für den Server kein zusätzlicher Aufwand, und du bekommst eventuell noch wesentlich mehr Informationen.

      Es geht mir immer noch um Anfängerscripts, also kleine Fingerübungen um langsam reinzukommen oder würdest du beim Führerscheinerwerb auch direkt mit einem LKW Fahrstunden machen wollen?

      Deutlich mehr Aufwand (aus Rechnersicht) ist, den Zähler in einer DB zu halten. Dort hast du dann auch den Vorteil, dass du dich um gleichzeitige Zugriffe nicht mehr kümmern musst; das kann die DB für dich sauber aufdröseln.

      Wenn die einzige speicherrelevante Funktion ein Counter sein soll, wäre zb. MYSQl schon etwas überdimensioniert und natürlich auch nicht für die ersten Scripts eines Anfängers angedacht.

      Gruss
      Hans

      1. Tach!

        Ja, das mit den gleichzeitigen Zugriff betrifft beide Methoden und bringt mich mal zu einer neuen Frage: Wieviel  exakt ( auf die Microsekunde genau) gleichzeitige Zugriffe kann PHP verkraften und geht wie damit um?

        Das kommt auf die Leistung des Servers und die Art der Einbindung PHPs in den Webserver an. Da gibt es garantiert Artikel über Messungen im Web.

        Es geht mir immer noch um Anfängerscripts, also kleine Fingerübungen um langsam reinzukommen oder würdest du beim Führerscheinerwerb auch direkt mit einem LKW Fahrstunden machen wollen?

        Es gibt andere Fingerübungen, die nützlicher sind, wie beispielsweise ein Affenformular. Das direkte Lesen und Schreiben von Dateien ist meiner Ansicht nach nur selten erforderlich. SQLite und andere Datenbanken nehmen einem diese Low-Level-Arbeit ab bieten einen besseren Zugriff auf die Daten. Wenn Daten häufig geschrieben werden, würde ich keine Datenhaltung in Dateien nehmen. Das (Nur-)Lesen von Konfigurationsdateien (ini, XML, wasauchimmer) ist etwas anderes, beim Lesen gibts die TOCTTOU-Probleme nicht.

        Wenn die einzige speicherrelevante Funktion ein Counter sein soll, wäre zb. MYSQl schon etwas überdimensioniert und natürlich auch nicht für die ersten Scripts eines Anfängers angedacht.

        Es gibt SQLite, und mit einem DBMS muss man früher oder später sowieso umgehen lernen.

        dedlfix.

      2. Hallo,

        Zumal deine alternative Methode nach und nach das Verzeichnis mit nutzlosen Dateien zumüllt, die man eigentlich auch irgendwann wieder entsorgen sollte - und schon ist der Vorteil "eine Dateioperation weniger" wieder dahin.
        Verzeichnis zugemüllt? Wie kommst Du darauf, nur eine Datei erhält ständig einen neuen Namen:
        touch()

        ja, du würdest ständig Dateien mit immer neuen Namen erzeugen, die vorhandenen bleiben aber liegen.
        Bedenke, was touch() wirklich macht:
         * falls die Datei schon existiert, wird ihr Modifikationsdatum auf "jetzt" gesetzt
         * falls die Datei noch nicht existiert, wird sie mit 0 Byte neu angelegt

        Ja, das mit den gleichzeitigen Zugriff betrifft beide Methoden und bringt mich mal zu einer neuen Frage: Wieviel  exakt ( auf die Microsekunde genau) gleichzeitige Zugriffe kann PHP verkraften und geht wie damit um?

        Die Frage stellt sich so nicht. Wirklich gleichzeitig kommen die Requests normalerweise nicht, denn selbst auf einem Mehrprozessorsystem gibt es irgendwo Flaschenhälse, die einzelne Operationen zwangsläufig serialisieren. Die Frage ist eher: Hat die Verarbeitungskette eine Stelle, an der sie durch einen anderen Prozess unterbrochen werden könnte?
        Machen wir ein Beispiel mit der klassischen Methode, die du zuerst genannt hast. Zwei Requests, hier A und B, werden kurz hintereinander bearbeitet, so dass sie sich zeitlich überschneiden.

        Request A                   Request B
        ---------------------------------------
        Öffne Datei zum Lesen       n/a
        Lies den Dateiinhalt        n/a
          -> "42"
        ------- Task-Switch -------------------
                                    Öffne Datei zum Lesen
                                    Lies den Dateiinhalt
                                      -> "42"
                                    Wandle String in Zahl
                                      -> 42
                                    Erhöhe den Zahlenwert
                                      -> 43
        ------- Task-Switch -------------------
        Wandle String in Zahl
          -> 42
        Erhöhe den Zahlenwert
          -> 43
        Wandle Zahl in String
          -> "43"
        Überschreibe Dateiinhalt
        Schließe Datei
         - fertig -
        ------- Task-Switch -------------------
                                    Wandle Zahl in String
                                      -> "43"
                                    Überschreibe Dateiinhalt
                                    Schließe Datei
                                     - fertig -

        So, in deiner Counter-Datei steht jetzt der Wert 43, obwohl zwei Zugriffe stattfanden und der Zählerwert eigentlich schon 44 sein müsste. Erkennst du das Problem?

        [...] oder würdest du beim Führerscheinerwerb auch direkt mit einem LKW Fahrstunden machen wollen?

        Wenn ich mit "Null" anfange, aber den festen Vorsatz habe, demnächst mit einem LKW zu fahren ... ja. Dann würde ich mich direkt für den LKW-Führerschein anmelden, aber vermutlich würde dann die Fahrschule trotzdem darauf bestehen, dass ich auch einige Stunden mit einem PKW absolviere - schon allein deshalb, weil man mit einem LKW niemals mehr als 80km/h fahren wird (bzw. darf) und daher gar kein Gefühl für höhere Geschwindigkeiten entwickeln kann.

        Andererseits fand ich es damals, vor der Reform der Führerscheinklassen, schon erschreckend. Man stelle sich vor, da hat jemand Fahrstunden auf einem Golf gemacht, und mit dem Auto auch die Prüfung abgelegt. Nun hatte derjenige den "3er" in der Hand - und was durfte er damit fahren? Ich hab mal das erlaubte Maximum zusammengestellt:
          Ein LKW (7.49t) mit einem Anhänger (Tandemachse, 11t)
        Da fährt also der einstige Golf-Fahrschüler nun mit einem Zug von rund 18t, 2.50m Breite und 18m Länge durch die Gegend - der alte Klasse-3-Führerschein gab's her. Auch einen Bagger hätte man mit dem alten 3er auf der Straße fahren dürfen (allerdings nur einen mit Rädern, nicht mit Ketten, und nur mit gesicherter Schaufel), oder vergleichbare große Baumaschinen. Faszinierend.

        Aber ich schweife ab. Was wollte ich sagen? ... Ach ja: Wenn ich mit einem Thema neu anfange, werde ich mich sicher erst einmal mit einfachen Aufgaben beschäftigen. Aber wenn ich bereits "große" Ziele vor Augen habe, werde bestimmt auch schon in einem frühen Stadium mal einen Blick in die Ferne werfen - zum Beispiel um zu erkennen, dass die eine oder andere "einfache" Lösung sich später als unzureichend für die meisten Anwendungsfälle erweisen wird, und man sie daher gar nicht weiter verfolgen sollte.

        Deutlich mehr Aufwand (aus Rechnersicht) ist, den Zähler in einer DB zu halten. Dort hast du dann auch den Vorteil, dass du dich um gleichzeitige Zugriffe nicht mehr kümmern musst; das kann die DB für dich sauber aufdröseln.
        Wenn die einzige speicherrelevante Funktion ein Counter sein soll, wäre zb. MYSQl schon etwas überdimensioniert und natürlich auch nicht für die ersten Scripts eines Anfängers angedacht.

        Ja, das sehe ich auch so. Ich wollte es trotzdem als "saubere" Methode mit erwähnt haben.

        Ciao,
         Martin

        --
        Lieber Hahn im Korb, als Tiger im Tank.
        Selfcode: fo:) ch:{ rl:| br:< n4:( ie:| mo:| va:) de:] zu:) fl:{ ss:) ls:µ js:(
        1. Hallo,

          ja, du würdest ständig Dateien mit immer neuen Namen erzeugen, die vorhandenen bleiben aber liegen.

          ja hast du recht, muss natürlich noch unlink() auf die alte Datei,  bzw. anstatt touch() lieber gleich rename();

          Machen wir ein Beispiel mit der klassischen Methode, die du zuerst genannt hast. Zwei Requests, hier A und B, werden kurz hintereinander bearbeitet, so dass sie sich zeitlich überschneiden.

          Request A                   Request B

          Öffne Datei zum Lesen       n/a
          Lies den Dateiinhalt        n/a
            -> "42"
          ------- Task-Switch -------------------
                                      Öffne Datei zum Lesen
                                      Lies den Dateiinhalt
                                        -> "42"

          Sehr anschaulich, danke ;-)
          Bedeutet dann wohl, dass dateibasierenden Speichermechanismen (bezogen auf den gleichen Dateinamen) grundsätzlich immer zu misstrauen sind, insbesondere bei starker Frequentierung.

          Also bleiben als Alternativen:

          Sqlite: Je nach Webspace Anbieter nicht vorhanden und Versionskonflikte(hatte ich selber schon)
          * genauer: Hatte viele MYSQL Scripte in unzähligen Stunden umgearbeitet auf SQLITE, weil ich das nach wie vor eine tolle DB finde und so handlich. Dann kam die neue Version, die nicht nur Fehler produzierte, sondern durch die alten Scripte sogar nachhaltige Serverstörungen produzierte. Da auch die neue Funktionsreferenz und das Handling komplett schwieriger wurde, überlegte ich noch mich da einzuarbeiten, aber die Angst über kurz oder lang wieder vor dem Nichts, aufgrund einer neuen Version zu stehen, benutze ich heute sicherheitshalber wieder MYSQL, auch wenn ich SQLITE bevorzugen würde. Schade eigentlich.

          Mysql: überdimensioniert für solche Kleinigkeiten

          Logfiles: Vielleicht doch die beste Lösung

          Fazit: Anfänger im Kurs Counter auf Textbasis erläutern, egal welche Variante und auf Gefahren hinweisen, zu fortgeschrittenem Kursverlauf die Logfilemethode darstellen.

          Dank euch

          1. Tach!

            Bedeutet dann wohl, dass dateibasierenden Speichermechanismen (bezogen auf den gleichen Dateinamen) grundsätzlich immer zu misstrauen sind, insbesondere bei starker Frequentierung.

            Nur wenn man das Sperren von Dateien vernachlässigt.

            dedlfix.

            1. Hallo,

              Nur wenn man das Sperren von Dateien vernachlässigt.

              wollte jetzt nicht so weit ausholen, aber ok. Bei der klassischen Variante nutze ich natürlich eine Sperrung, allerdings nur beim schreiben in etwa so:

              file_put_contents($file,file_get_contents($file)+1,LOCK_EX);

              Was bedeutet das nun, wenn ich Martins anschauliches Beispiel zu Rate ziehe?

              Ein gleichzeitiger Zugriff hat den gleichen Ausgangswert und somit geht ein Zugriff aufgrund der gleichen Zahl verloren, richtig?

              Wenn ich nun aber die komplexere Version mit FLOCK benutze und auch die Datei fürs Lesen sperre, dann würde PHP einen der beiden gleichzeitigen Aufrufe in Wartestellung setzen bis die Datei wieder entsperrt wurde und es wäre absolut Verlass auf die Eintragungen, richtig?

              Falls dem so ist, wäre auch meine Anfangsfrage gelöst, denn dann kann ein Counter auf Basis touch(), oder rename() nicht richtig funktionieren, weil ein klare Sperrung nicht machbar wäre. Denn dann ändert sich ja der Dateiname und eine Lesesperre wäre nur aktiv für eine Datei die nun aktuell gar nicht mehr existiert, richtig?

              Gruss
              Hans

              1. Tach!

                Bei der klassischen Variante nutze ich natürlich eine Sperrung, allerdings nur beim schreiben in etwa so:
                file_put_contents($file,file_get_contents($file)+1,LOCK_EX);

                Dann fummelt dir während des Schreibprozesses keiner dazwischen. Insbesondere können lesende Zugriffe keine erst halbgeschriebenen Dateien lesen - wenn sie beim Lesen ebenfalls den Sperrmechanismus berücksichtigen, denn der ist freiwillig.

                Was bedeutet das nun, wenn ich Martins anschauliches Beispiel zu Rate ziehe?

                Zwischen Lesen und Schreiben können dir andere Prozesse dazwischengrätschen. Der ganze Vorgang des Lesens, Ändern und Schreibens muss atomar sein.

                Ein gleichzeitiger Zugriff hat den gleichen Ausgangswert und somit geht ein Zugriff aufgrund der gleichen Zahl verloren, richtig?

                Ja.

                Wenn ich nun aber die komplexere Version mit FLOCK benutze und auch die Datei fürs Lesen sperre, dann würde PHP einen der beiden gleichzeitigen Aufrufe in Wartestellung setzen bis die Datei wieder entsperrt wurde und es wäre absolut Verlass auf die Eintragungen, richtig?

                So der Plan.

                Falls dem so ist, wäre auch meine Anfangsfrage gelöst, denn dann kann ein Counter auf Basis touch(), oder rename() nicht richtig funktionieren, weil ein klare Sperrung nicht machbar wäre. Denn dann ändert sich ja der Dateiname und eine Lesesperre wäre nur aktiv für eine Datei die nun aktuell gar nicht mehr existiert, richtig?

                Weiß nicht, hab mich mit dem Aspekt der Frage nicht weiter beschäftigt ... mach's nicht zu kompliziert. Nimm lieber bewährte Lösungen statt unausgereiftem Eigenbau, wenn du zum Ziel gelangen willst und dieses nicht "der Weg" heißt.

                dedlfix.

              2. Hallo Hans,

                Nur wenn man das Sperren von Dateien vernachlässigt.
                wollte jetzt nicht so weit ausholen, aber ok. Bei der klassischen Variante nutze ich natürlich eine Sperrung, allerdings nur beim schreiben in etwa so:

                file_put_contents($file,file_get_contents($file)+1,LOCK_EX);

                betrachten wir mal, was bei dieser Anweisung tatsächlich nacheinander passiert:

                1. Aufruf von file_get_contents, Ergebnis (String) landet in einem temporären internen
                    Speicher (nennen wir ihn "R" wie Register)
                 2. Der Inhalt von "R" wird in eine Zahl umgewandelt, dann 1 dazu addiert und wieder
                    abgelegt (jetzt als Zahlenwert)
                 3. Aufruf von file_put_contents() mit dem Inhalt von "R" und der Konstante LOCK_EX als
                    Parameter

                Genaugenommen ist nur in Schritt 3 die Datei gegen andere Zugriffe gesperrt. Das ist aber unnötig, denn AFAIK arbeiten file_get_contents() und file_put_contents() auch schon von sich aus "atomar", also nicht unterbrechbar, auch ohne dass man sich explizit drum kümmert. Zwischen den genannten Schritten kann der Prozess aber immer noch unterbrochen werden.

                Was bedeutet das nun, wenn ich Martins anschauliches Beispiel zu Rate ziehe?
                Ein gleichzeitiger Zugriff hat den gleichen Ausgangswert und somit geht ein Zugriff aufgrund der gleichen Zahl verloren, richtig?

                Richtig, das Risiko besteht immer noch.

                Wenn ich nun aber die komplexere Version mit FLOCK benutze und auch die Datei fürs Lesen sperre, dann würde PHP einen der beiden gleichzeitigen Aufrufe in Wartestellung setzen bis die Datei wieder entsperrt wurde und es wäre absolut Verlass auf die Eintragungen, richtig?

                Ja. Wobei man sich dann durch Denk- und Verständnisfehler auch "zu Tode sperren" kann. ;-)

                Falls dem so ist, wäre auch meine Anfangsfrage gelöst, denn dann kann ein Counter auf Basis touch(), oder rename() nicht richtig funktionieren, weil ein klare Sperrung nicht machbar wäre. Denn dann ändert sich ja der Dateiname und eine Lesesperre wäre nur aktiv für eine Datei die nun aktuell gar nicht mehr existiert, richtig?

                Richtig.

                Ciao,
                 Martin

                --
                Zwei Mäuse treiben's miteinander. Sagt der Mäuserich: "Hoffentlich ist nicht wieder alles für die Katz."
                Selfcode: fo:) ch:{ rl:| br:< n4:( ie:| mo:| va:) de:] zu:) fl:{ ss:) ls:µ js:(
    2. Tach!

      Deutlich mehr Aufwand (aus Rechnersicht) ist, den Zähler in einer DB zu halten. Dort hast du dann auch den Vorteil, dass du dich um gleichzeitige Zugriffe nicht mehr kümmern musst; das kann die DB für dich sauber aufdröseln.

      Ja, aber auch hier muss man TOCTTOU beachten. Also nicht Lesen, Erhöhen, Schreiben in Einzelschritten, sondern nur ein Update mit x=x+1. (Oder den Vorgang in einer Transaction kapseln, was aber deutlich mehr Aufwand ist.)

      dedlfix.

    3. Tach

      wenn jemand beginnt PHP zu lernen, gehört oft am Anfang die Erstellung eines einfachen Counters dazu.

      ach, ist das so?

      "Oft."

      Ich habe die Idee mal getestet... WARNUNG! Den folgenden Code nicht übernehmen, der hat definitiv das TOCTTOU-Problem!

      Versuch 1:

        
      <?php  
      $start=microtime(true);  
      for ($i=0; $i<100000; $i++) {  
          payload();  
      }  
      echo "\n\nbenötigte Zeit: ", (microtime(true)-$start) , "s\n\n";  
        
      function payload() {  
          file_put_contents('zaehler.txt', 1 + file_get_contents('zaehler.txt'), LOCK_EX);  
      }  
      ?>  
      
      

      Braucht bei mir ca. 12-13 Sekunden.

      Versuch 2:

        
      <?php  
      <?php  
      $start=microtime(true);  
      for ($i=0; $i<100000; $i++) {  
          payload('/local_mounts/data/tmp/');  
      }  
      echo "\n\nbenötigte Zeit: ", (microtime(true)-$start) , "s\n\n";  
        
      function payload($d) {  
          $z=0;  
          $ar=scandir($d);  
          sort($ar, SORT_NUMERIC);  
          foreach ($ar as $f) {  
              if ( '..' != $f && '.' !=$f ) {  
                  unlink("$d$f");  
                  $z=$f+1;  
              }  
          }  
          touch("$d$z");  
      }  
      ?>  
      
      

      Braucht bei mir stabil knapp 4 Sekunden. Geht also wesentlich schneller. Die Zeitdifferenz von 8 Sekunden ist ja auch durch 100.000 zu teilen. Das macht pro Zugriff also 0.00008 Sekunden oder 0.08 Millisekunden aus. Das ist dann wieder nicht mehr so viel... Auch sehe ich die Behebung des TOCTTOU-Problems als schwierig an. Allenfalls käme wohl so etwas ein Blockfile oder shared memory in Betracht. Das betrifft beide Tests.

      Wenn man dann daran herumdoktort kann man tatsächlich gleich eine Datenbank oder sowas wie sqlite nehmen. Grund: Hat man einmal einen Zähler, dann kommen schon bald Erweiterungswünsche...

      Jörg Reinholz

  2. Anfänger einfachen Counter erstellen lassen.
    Mehrere (u.a. parallele) Requests auf Ziel abfeuern.
    Anfänger Ergebnis (log) des einfachen Counter auswerten bzw. anhand der Requests vergleichen lassen.
    => Anfänger Ergebnis auswerten lassen, Anfänger Problem finden lassen,
    => Verbesserungsvorschläge