CV laden
Zurück zu Business Cases

Cache

Memcached für HTML, Redis für den Lock

Zwei Stores, je eine Aufgabe. Seiten liegen in Memcached. Der Rebuild-Lock liegt in Redis.

Der Listen-Cache lief für alle gleichzeitig ab. Memcached warf den Key, zweihundert Requests gingen an MySQL, der Katalog sah aus wie ein Traffic-Spike. Es war ein Uhr-Spike. Jeder TTL stand auf 60 Sekunden ab demselben Write.

Der Stack hatte schon zwei Speicher. CACHE_STORE zeigte auf Memcached. QUEUE_CONNECTION und SESSION_DRIVER zeigten auf Redis. Ich habe aufgehört, sie als austauschbar zu behandeln, und jedem die Arbeit gegeben, die er kann.

Das Problem war ein Thundering Herd auf einem synchronen TTL. Shop-Besucher warteten auf MySQL. Ich brauchte einen Rebuild, einen veralteten Zwillings-Key für den Rest und einen Lock, den HTML-Eviction nicht wirft.

HTML liegt in Memcached. Der Rebuild-Lock liegt in Redis. MySQL sieht einen Schreiber.
HTML liegt in Memcached. Der Rebuild-Lock liegt in Redis. MySQL sieht einen Schreiber.

Warum nicht ein Store

Memcached ist ein Slab-Allocator. Er ist schnell bei get/set von Blobs und schlecht bei Locks. Ein GET mit Miss kann nicht versprechen, dass nur ein Aufrufer neu baut. Redis kann das. SET key NX EX 5 ist ein Lock mit TTL, und dieselbe Redis-Instanz betreibt bereits Horizon.

HTML in Redis hätte funktioniert. Es hätte Seiten-Blobs, Queue-Payloads und Session-Hashes unter einer Eviction-Policy gemischt. Wenn Redis an maxmemory stößt, können eine Katalogseite und ein reservierter Job zusammen verschwinden. So bekommt man Stampede und hängende Queue am selben Nachmittag.

Der Lock

Bei einem Miss macht der Worker SET lock:rebuild:{key} NX EX 5 in Redis. Bekommt er den Lock, liest er MySQL, schreibt Memcached, löscht den Lock. Bekommt er ihn nicht, liefert er das vorherige HTML aus einem Zwillings-Key, der erst nach dem nächsten erfolgreichen Write stirbt. Zwei Sekunden veraltet schlägt einen Stampede.

TTLs auf Listen-Keys sind verzittert. Der Write addiert ein paar zufällige Sekunden, damit das Raster nicht an einem Uhr-Tick stirbt. Der Zwillings-Key ist das letzte gute HTML. Er ist keine zweite Wahrheit. MySQL bleibt die Quelle.

Ein Request baut neu. Die anderen behalten das letzte HTML. Ohne Lock trifft jeder Miss MySQL.
Ein Request baut neu. Die anderen behalten das letzte HTML. Ohne Lock trifft jeder Miss MySQL.

nginx davor

Anonyme Hits laufen zusätzlich durch einen Zwei-Sekunden-Microcache in nginx. Der Cache-Key ist Scheme, Host, URI und Locale. Query-Strings ohne Einfluss auf die Seite gehören nicht in den Key. Requests mit Cookie auth_token umgehen diesen Cache, ein eingeloggter Nutzer bekommt also kein öffentliches Fragment.

Die Trefferquote der Liste stieg von einstelligen Werten auf die Mehrheit der Requests. Die MySQL-CPU auf dem Host wurde zwischen Writes zur Linie. Der Code liegt in einem Helper. Neue gecachte Reads bekommen Lock und Zwillings-Key ohne neues Design.

Was ich daraus mitnehme

Zwei Speicher sind keine Redundanz. Es sind zwei Jobs. Blobs in Memcached. Locks und Queues in Redis. Eine Eviction-Policy für beides ist Stampede und hängendes Horizon am selben Tag.

Ein synchroner TTL ist ein geplanter Ausfall. Jitter und ein Lock schlagen einen längeren TTL, der trotzdem für alle gleichzeitig stirbt.

Zwei Sekunden veraltetes HTML nehme ich statt zweihundert identischer SELECTs.

Zurück zu Business Cases