21.
Dezember
webvariants wünscht ein frohes Fest
von Michael Kühle 2 Kommentare

Für unsere Agentur geht ein sehr erfolgreiches und ereignisreiches Jahr 2009 zu Ende. Im vergangenen Jahr schrieb Chris in seinem Blog Beitrag über die ersten Monate unserer Agentur und über den Ausblick für 2009. Nun ist das Jahr 2009 fast zu Ende und es ist trotz der allgemeinen Wirtschaftskrise ein sehr erfolgreiches geworden. Dies hatte niemand so erwarten können, auch wenn es aus unserer Einschätzung möglich war.
So konnten wir in diesem Jahr weitere Kunden hinzugewinnen, unser erstes größeres Projekt mit pharmatching gewinnen. Unser Team ist im Einklang mit der Entwicklung des Unternehmens ebenfalls gesund gewachsen, so sind in den letzten Monaten mit Christian, Martin, Jens, Robert und letztendlich Miriam hinzugekommen.
Das kommende Jahr 2010, wird aus meiner Sicht, wieder ein sehr ereignisreiches Jahr werden. Es gibt viele Ideen von uns, es gibt viele Möglichkeiten und der Markt wird sich sicherlich im kommenden Jahr nach dem durchschreiten der Talsohle auch weiterhin positiv entwickeln.
In wenigen Tagen ist nun Weihnachten und Silvester folgt danach. Aus diesem Grund möchte das gesamte Team von webvariants allen Kunden, Partnern, freien Mitarbeitern und all unseren Freunden sowie Blog Lesern ein schönes, ruhiges und erholsames Weihnachtsfest wünschen sowie einen guten Rutsch in das kommende Jahr 2010.
In diesem Sinne, Frohe Weihnachten.

16.
Dezember
Lastspitzen in Memcached vermeiden
von Christoph Kommentieren
Um unseren Kunden wirklich die optimale Performance zu gewährleisten, setzen wir uns seit einiger Zeit mit In-Memory-Caches wie dem bekannten Memcached auseinander. Dabei werden die Daten zwischen einzelnen Seitenaufrufen im Speicher vorgehalten, um sie beim nächsten Request direkt auszulesen und verwenden zu können. Traditionelle Implementierungen für derartige Caches legen diese Daten meist im Dateisystem oder einer Datenbank ab. Das bringt zwar einige Vorteile in der Verwaltung (in Datenbanken kann man suchen, in Memcached und seinen Kollegen hingegen nicht), ist aber auch merklich langsamer. Zumal beide Speichersysteme eher für persistente Speicherung ausgelegt sind.
Da man nie genau weiß, welche Bedingungen man auf dem Webspace/Server des Kunden antrifft, wollen wir möglichst viele Caches unterstützen:
Alle diese Caches bieten in etwa die gleiche Schnittstelle an:
- setzeWert (name, wert)
- holeWert (name)
- löscheWert (name)
- plus einiger zusätzlicher Funktionen zum Inkrementieren/Dekrementieren von Werten und zur Status-Abfrage
Beim Lesen der Dokumentation (insbesondere die FAQ von Memcached) sind wir auf ein Problem aufmerksam geworden, das sich bisher noch nicht wirklich zeigte. Dennoch haben wir uns die Mühe gemacht, zu untersuchen, wie relevant es ist und wie wir es am besten umgehen können. Das Problem tritt bei großen Mengen gleichzeitiger Requests auf. Man könnte also sagen, dass es erst auf stark frequentierten Seiten problematisch wird.
Lastspitzen
Stellen wir uns einmal vor, auf unserer Website surfen gleichzeitig 200 Besucher. Nichts ungewöhnliches für große Seiten. Nehmen wir auch an, dass sich bereits alle wichtigen Objekte im Cache befinden und daher all unsere Scripts glücklich sind, weil sie fast alle Informationen zum Aufbau der Seite direkt von dort beziehen können. Es scheint also die Sonne an einem wolkenlosen blauen Himmel im Blümchen-Caching-Land.
Jetzt geht der Admin der Seite daher und ändert ein Objekt. Beispielsweise einen Teasertext auf der Startseite, weil er sich vorher vertippt hat und „Tischlampen“ doch nicht nach dem „Ti“ trennen sollte. Das Objekt wird also aus dem Cache entfernt (oder als ungültig markiert oder mit einem Dirty-Bit versehen oder wie auch immer man dieses Streichen im Endeffekt implementiert). Fakt ist, dass das Objekt von jetzt an nicht mehr verfügbar ist.
Was wird passieren? Nun, nehmen wir an, dass das Neu-Cachen des Teasertexts 50ms dauert. Natürlich hämmern nicht alle 200 Besucher wirklich gleichzeitig auf den Server ein, sondern vielleicht nur 50 bis 75. Aber diese 50 kommen gleichzeitig am Server an und wollen die Startseite. Alle 50 merken in etwa gleichzeitig, dass der Teasertext fehlt. Und alle 50 generieren ihn neu und speichern ihn ab, denn keiner der Requests weiß von den jeweils anderen.
Wir haben also 50x den Teasertext erzeugt. Das sind allein die 50 Requests, die gleichzeitig direkt nach dem Löschen auf den Server einschlugen. Dazu kommen noch die Requests, die während des 50ms-Zeitfensters einschlagen, in dem die ersten 50 bereits das Objekt generieren. Runden wir auf, sind das nochmal 50. Bis diese letzten 50 ihre Arbeit getan haben, liegt das Objekt längst wieder im Speicher.
Fassen wir diese wirren Äußerungen zusammen: Das Objekt wird einmal gelöscht und aufgrund des hohen Besucheraufkommens 100x neu generiert und 100x neu gecached. Davon sind 99 % unnötige Zeit- und v.a. Ressourcenverschwendung.
Locks
Wie die FAQ schon richtig erwähnen, sollte man für derartige Fälle Locks verwenden. Um portabel zu bleiben, kann man Locks durch den ADD-Befehl simulieren, falls der Cache keinen expliziten Lock anbietet. Den Unterschied in der Verwendung möchte ich hier kurz demonstrieren. Der Code ist recht frisch und hat sicherlich noch Optimierungspotential, aber zu Demozwecken sollte es reichen.
Beispiel: Kein Locking
$memcached = connect(); // primitive Routine zum Herstellen der Verbindung
$key = 'myobj';
// Versuche, Objekt zu holen
$obj = $memcached->get($key);
$memcached->increment('tries');
// Falls das Objekt nicht vorhanden ist, erzeugen wir (mittels Schwerstarbeit!)
// ein neues Objekt und speichern es ab. Andernfalls haben wir einen Cache-Hit.
if ($obj === false) {
$obj = work($memcached); // work simuliert Arbeit für 40ms
$memcached->set($key, $obj, 0, 0);
}
else {
$memcached->increment('hits');
}
// Wir simulieren eine Löschaktion. Dieser Fall könnte auftreten, wenn im
// Backend Daten des Objekts geändert wurden.
randomlyDeleteObject($memcached, $key); // löscht in 0,1 % aller Requests
// Verbindung schließen
$memcached->close();
Würden wir diesen Code mittels ApacheBench 10.000x mit einer Concurrency von 250 ausführen lassen, ergäbe sich folgendes Bild:
- Aufrufe insgesamt: 10.000
- Cache-Treffer: 9.387
- Cache-Misses: 613 (so oft wurde Arbeit ausgeführt und das Objekt insgesamt neu erzeugt)
- Deletes: 8 (so oft wurde das Objekt im Speicher gelöscht)
Ein übles Ergebnis: Nur weil 8x das Objekt gelöscht wurde, wurde es 613x neu erzeugt, wovon wieder 605 unnütz sind. Wären es rechenintensiver, das Objekt zu erzeugen, könnte sich das als Problem herausstellen. Dazu kommt, dass die 613 Requests nicht nacheinander kamen, sondern jeweils sprunghaft kurz nachdem das Objekt aus dem Cache entfernt wurde.
Beispiel: Mit Locking
Wir verbessern nun unser Konzept und verwenden einen Lock pro Objekt. Der Algorithmus ist nicht perfekt, aber schon ziemlich gut. Wenn wir das Objekt nicht abrufen können, versuchen wir es, exklusiv zu sperren. Gelingt dies, erzeugen wir ein neues Objekt und cachen es. Kriegen wir keinen Lock, heißt das, dass bereits ein anderer Prozess an dem Objekt werkelt. In unserem Fall warten wir einfach, bis der andere Prozess fertig ist oder die Sperrzeit ausläuft. In PHP sieht das Ganze dann in etwa so aus:
$memcached = connect();
$key = 'myobj';
$obj = $memcached->get($key);
$memcached->increment('tries');
if ($obj === false) {
if (lock($memcached, $key)) {
$obj = work($memcached);
// Objekt speichern & freigeben
$memcached->set($key, $obj, 0, 0);
unlock($memcached, $key);
}
else {
// Wir warten genau einmal die vorgegebenen 3 Sekunden. Wenn wir dann
// immer noch kein Objekt haben, beißen wir in den sauren Apfel und
// erzeugen eben ein neues.
$waitResult = waitForLockRelease($memcached, $key);
$obj = $memcached->get($key);
if ($obj === false) {
$obj = work($memcached);
// Da offensichtlich ein anderer Request das Objekt gelockt
// hat, um es aufzubauen, cachen wir dieses hier nicht.
}
else {
$memcached->increment('hits');
}
}
}
else {
$memcached->increment('hits');
}
// Wir simulieren eine Löschaktion. Dieser Fall könnte auftreten, wenn im
// Backend Daten des Objekts geändert wurden.
randomlyDeleteObject($memcached, $key);
$memcached->close();
Lassen wir dieses Script auch mit ApacheBench auswerten, erhalten wir folgendes Ergebnis:
- Aufrufe insgesamt: 10.000
- Cache-Treffer: 9.984
- Cache-Misses: 16 (so oft wurde Arbeit ausgeführt und das Objekt insgesamt neu erzeugt)
- Deletes: 11 (so oft wurde das Objekt im Speicher gelöscht)
Ergebnis
Wir haben die Belastung für den Server von 613 auf 16 Arbeitseinheiten verringert, also auf weniger als 3%. Das ist schon ziemlich ordentlich. Gleichzeitig haben wir die Belastung für den Entwickler von 4 auf gut 10 Zeilen Code erhöht. Bleibt die Frage, was wertvoller ist: Systemressourcen oder Mannstunden.
Fazit
Für komplexe Objekte, deren Erstellung etwas Zeit in Anspruch nimmt, sollte man definitiv Locks verwenden. Wären sie nicht so aufwändig zu implementieren, man könnte sie direkt überall nutzen
Das von uns vorgestellte Konzept kann natürlich beliebig erweitert werden. So kann statt einmal max. 3 Sekunden zu warten auch einfach ewig gewartet werden. Oder man wartet maximal 3x. Wichtig ist dabei, dass man sich nicht in Abhängigkeit vom Cache begibt, das heißt: irgendwann muss auch mal Schluss mit dem Warten sein. Immerhin kann es passieren, dass der Cache ganz weg oder gar deaktiviert ist. Schließlich ist der Cache kein Muss und so sollten wir ihn auch behandeln: Als freundlichen, aber unzuverlässigen Helfer.
Wenn alles nach unserer Planung verläuft, wird varisale 2.1 bereits die o.g. In-Memory-Caches unterstützen. Durch die Implementierung in den Developer Utils kommen diese neuen Möglichkeiten dann direkt allen unserer AddOns zugute.
Der Testcode ist online in einem Mercurial-Repository bei Bitbucket.org verfügbar und steht unter keinen speziellen Lizenz (do whatever you like). Dort lässt er sich auch ohne Mercurial-Client herunterladen.
ApacheBench wurde wie folgt genutzt:
ab -c 250 -n 10000 http://localhost/memcached_test/test_minimal.php
Es ist empfehlenswert, in einem anderen Fenster die status.php auszuführen, die alle 0,5 Sekunden den Memcache-Status ausgibt. Die Ausgabe beginnt erst, wenn tries > 0 ist. Gleichzeitig setzt die status.php die Zähler beim Start wieder auf 0 zurück.
$ php status.php
success rate: 100,00 % (tries: 18 hits: 18 works: 0 wins: 0 deletes: 0)
success rate: 100,00 % (tries: 527 hits: 527 works: 0 wins: 0 deletes: 0)
success rate: 100,00 % (tries: 1054 hits: 1054 works: 0 wins: 0 deletes: 0)
success rate: 100,00 % (tries: 1572 hits: 1572 works: 0 wins: 0 deletes: 0)
success rate: 99,90 % (tries: 2079 hits: 2077 works: 1 wins: 0 deletes: 1)
success rate: 99,74 % (tries: 2717 hits: 2710 works: 1 wins: 0 deletes: 1)
success rate: 99,97 % (tries: 3308 hits: 3307 works: 1 wins: 0 deletes: 1)
success rate: 99,95 % (tries: 3839 hits: 3837 works: 2 wins: 0 deletes: 2)
success rate: 99,95 % (tries: 4361 hits: 4359 works: 2 wins: 0 deletes: 2)