Verzögertes Schreiben schneller als direktes Schreiben

Ja, ich weiß, dass der Titel 1a gelungen ist ????
Dieser Beitrag soll erläutern, wie Leseoperationen in Web-Anwendungen möglichst hoch priorisiert werden können, um die Latenzzeit beim Seitenaufbau nicht unnötig zu erhöhen. Genauer geht es darum, wie man mit „unwichtigen“ Update-Querys umgeht, damit diese die für den Seitenaufbau wichtigeren SELECT-Anfragen nicht stören.

Ausgangspunkt ist eine Anwendung mit MyISAM-Tabellen mit jeder Menge SELECT-Anweisungen aber auch einigen UPDATE-, INSERT- und DELETE-Querys. Die 3 letztgenannten möchte ich als Update-Operationen im Folgenden bezeichnen.
MyISAM (als MySQL-Standard-Tabellenformat) hat die Eigenschaft, dass während einer Update-Operation auf eine bestimmte Tabelle diese gelockt wird. Das bedeutet: So lange die Operation läuft, keine andere Operation an der Tabelle möglich.

Das wird erst problematisch, wenn es um Webseiten geht, wo die Besucher einen schnellen Seitenaufbau erwarten. Denn dann blockieren mitunter unwichtige Update-Operationen den Seitenaufbau an anderer Stelle.

1. Fall: Zuerst möchte ich den Fall betrachten, dass mitgeloggt wird, welche Seiten der Benutzer besucht hat, damit ihm diese Information an anderer Stelle unter „Bisher angesehene Seiten“ wieder präsentiert werden kann. Das geschieht ganz einfach beim Aufruf einer bestimmten Seite durch eine INSERT-Anweisung, die die URLund den zugehörigen Usernamen in eine Tabelle schreibt.

2. Fall: Ein anderer Fall ist z.B. das Inkrementieren eines Counters, um später anzeigen zu können „Die Seite wurde bisher xxx mal aufgerufen“. Dies wird über ein einfaches UPDATE-Statement bewerkstelligt, indem man den Wert des entsprechenden Datensatzes um 1 erhöht.

Und nun kommt der Aspekt ins Spiel, dass die Besucher möglichst kurze Warte- bzw. Ladezeiten sehen wollen.
Obige beiden Update-Operationen sind also umringt von jeder Menge SELECT-Anweisungen. In einem Online-Shop müsste beispielsweise der Beschreibugstext geladen werden, eventuell wie viele Artikel im Bestand sind, einige User-Rezensionen und noch einige Produktbilder.
Das Ziel muss es ja sein, all diese SELECTs in möglichst kurzer Zeit durchzuprügeln.

Und plötzlich kommt da das in Fall 2 beschriebene UPDATE in den Weg. Da Update-Operationen standardmäßig höher priorisiert sind als Select-Anweisungen, hat das zur Folge, dass sämtliche andere SELECTs auf diese Tabelle erstmal gesperrt werden bis das UPDATE ausgeführt wurde. Das ist auch sinnvoll (die MySQL-Entwickler haben sich ja was dabei gedacht), allerdings ist es hier fraglich, was wichtiger ist: dass der User (und andere User, die Scripte aufrufen, die auf die gesperrte Tabelle zugreifen) die Seite möglichst schnell angezeigt bekommt oder doch eher dass der Besuchercounter inkrementiert wird. Ich denke ersteres.

Ganz ähnlich verhält es sich im 1. oben beschriebenen Fall. Das INSERT behindert dort genauso sämtliche anderen SELECTs auf diese Tabelle (und naturgemäß sind Update-Operationen meist langsamer als SELECTs, weil neben den Daten ja noch ggf. Indizes bearbeitet werden müssen). Und auch hier die Frage nach der Priorität: schneller Seite ausliefern oder lieber zuerst eine Information in die DB einfügen, die man erst später benötigt (wenn der User danach überhaupt auf eine Seite geht, auf der die bisher besuchten Webseiten angezeigt werden – eventuell war das INSERT auch ganz umsonst). Auch hier ist erstgenanntes wohl wichtiger.

Doch wie ist es möglich unwichtige Update-Operationen auch als solche zu kennzeichnen, damit MySQL weiß, dass deren Ausführung auch noch etwas Zeit hat? Mein erster Gedanke ging in Richtung des Schlüsselworts LOW_PRIORITY. Dadurch blockiert die betreffende Anfrage keine anderen SELECT-Anfragen mehr, die auf die gleiche Tabelle gehen, sondern wartet bis keine höher priorisierten Statements mehr vorliegen. Das Problem dabei ist, dass es dafür die Abarbeitung des aktuellen Scripts seitens PHP behindert, da mysql_query() ja einen Rückgabewert liefert, ob der SQL-Befehl erfolgreich ausgeführt werden konnte. Und dieser kann erst geliefert werden, sobald die Query ausgeführt wurde. So kann man leicht das ganze System ausbremsen, denn jeder, der auf Script abc.php zugreift, worin viele SELECTs gemacht werden und nur wenige „UPDATE LOW_PRIORITY“-Statements, muss warten bis die SELECTs des jeweils anderen ausgeführt worden sind.

Schlauer ist demzufolge die Höher-Priorisierung von SELECT-Statements per HIGH_PRIORITY. Dadurch bewirkt man, dass alle SELECT-Abfragen vor den Update-Operationen ausgeführt werden.

Bei INSERT-Statements gibts neben LOW_PRIORITY noch eine niedrigere Priorität namens DELAYED. Das ist eigentlich noch besser, denn die Operation wird einfach in den MySQL-Puffer geschrieben und später ausgeführt, wenn keine anderen Operationen vorliegen. Der Rückgabewert von mysql_query() ist dadurch aber nicht mehr aussagefähig (immer true). Dadurch wird der Seitenaufbau nicht behindert. Wenn das INSERT ausgeführt wird und es kommt zwischenzeitlich eine andere Operation für diese Tabelle, dann wird es abgebrochen und zuerst das SELECT (oder eine andere Query) ausgeführt. Auch hier wird deutlich, dass unter bestimmten Umständen das INSERT seeeehr lang warten muss. Man muss also entscheiden, wie unwichtig das INSERT wirklich ist bzw. mal in die Statistiken schauen, wie häufig es vorkommt, dass auf eine Tabelle zugegriffen wird. Gibt es nämlich ständig Anfragen in der Warteschlange für diese Tabelle, wird das INSERT-DELAYED erstmal gar nicht ausgeführt.

Schön auch, dass INSERT-DELAYED-Statements der gleichen Tabelle zusammengefasst werden zu einer INSERT-Query, das beschleunigt die Sache zusätzlich.

So, jetzt bitte noch nicht losstürmen und die Schlüsselwörter einfügen, denn wie immer gibts einen Haken: In einem Benchmark von gleichen INSERTs – einmal mit DELAYED und einmal ohne – wird stets die Variante ohne schneller sein, denn bei DELAYED muss MySQL ja stets prüfen, ob nicht doch noch eine andere Operation reingekommen ist und das kostet natürlich Zeit.
Folge: DELAYED sollte nur angewendet werden, wenn der Durchsatz irrelevant und die Wertänderung wirklich vorerst unwichtig ist (wie z.B. bei Einzeloperationen wie in Fall 1 oben beschrieben).
Außerdem wird die zu tätigende Operation wirklich nur in den Arbeitsspeicher geschrieben, nicht auf die Festplatte. Das ist dann problematisch, wenn MySQL oder der Server an sich abstürzt. Also besser nur für unwichtige Daten verwenden (wie eben einen Counter, wo es nicht darauf ankommt, ob der nun 1 mehr oder weniger anzeigt).

Bei SELECT HIGH_PRIORITY gibt es solche Bedenken nicht. Sobald die Tabelle frei ist, wird diese Anfrage ausgeführt, da sie höher priorisiert ist als alle anderen Abfragen.

Prüft also immer (sowohl für HIGH_PRIORITY und noch mehr für DELAYED): Ist es wichtig, dass eine bestimmte Wertänderung sofort durchgeführt wird oder reicht es auch in ein paar Sekunden noch?!
Wenn ihr feststellt, dass die Update-Operation eher unwichtig ist für den weiteren Seitenaufbau oder auch die Informationsgewinnung an anderer Stelle, dann kann durch die beiden Schlüsselwörter der Seitenaufbau beschleunigt werden (um wie viel, hängt natürlich vom Aufwand für die Update-Operation ab).

Also probierts einfach mal aus und wie immer sind Kommentare sehr willkommen.



Deprecated: Directive 'allow_url_include' is deprecated in Unknown on line 0