Exkurs: Zipf's Law und die Grenzen des Cachings

Caching ist der erste Reflex vieler Entwickler, wenn Services eine hohe Auslastung haben. Und in aller Regel hilft es — dramatisch sogar. Ein Suchdienst, der bei jedem Request die Datenbank befragt, schafft vielleicht 200 Anfragen pro Sekunde. Derselbe Service mit einem Cache für häufige Suchanfragen schafft 1.000. Faktor fünf, ohne eine einzige Zeile Geschäftslogik zu ändern.

Das klingt nach einer Skalierungslösung. Ist es aber nicht. Der Faktor fünf ist geborgt — er gehört nicht dem System, sondern dem Traffic-Muster – und die ändern sich.

Warum Caching so gut funktioniert

Die Erklärung dafür, das Caching oft eine gute Idee ist liefert ein Gesetz, das der Harvard-Linguist George Kingsley Zipf 1949 formulierte. Zipf untersuchte die Häufigkeit von Wörtern in Texten und fand ein erstaunlich regelmäßiges Muster: Das häufigste Wort kommt doppelt so oft vor wie das zweithäufigste, dreimal so oft wie das dritthäufigste, und so weiter.

Allgemein: Das k-meistangefragte Item wird proportional zu $\frac{1}{k^\alpha}$ angefragt, wobei $\alpha$ typischerweise nahe 1 liegt (gemessen wurden Werte zwischen 0,6 und 1,2, je nach Domäne).

Dieses Muster taucht merkwürdigerweise an allen möglichen Stellen auf — zum Beispiel auch in der Verteilung von Suchanfragen oder Seitenaufrufen. Breslau et al. zeigten 1999, dass Web-Traffic einer Zipf-Verteilung folgt. Die Konsequenz: Ein winziger Bruchteil aller möglichen Anfragen macht den Großteil des Traffics aus.

Ein Zahlenbeispiel: Ein Online-Shop hat 500.000 verschiedene Suchanfragen pro Tag. Die Top-5.000 (also 1%) generieren 400.000 davon. Ein Cache, der diese 5.000 Einträge enthält — ein paar Megabyte Speicher — reduziert die Datenbank-Last um 80%. Das reduziert nicht zuletzt auch Infrastrukturkosten.

Wenn die Datenbank der Engpass ist und 200 Queries pro Sekunde schafft, sieht die Rechnung so aus: Bei 80% Hit-Rate treffen nur noch 20% der Requests auf die Datenbank. Statt 200 Anfragen pro Sekunde bedient das System jetzt 1.000 — denn nur jede fünfte Anfrage braucht die Datenbank. Das ist die Formel dahinter:

\[\text{Kapazität}_\text{eff} = \frac{\text{Kapazität}_\text{DB}}{1 - \text{Hit-Rate}}\]

Beeindruckend. Aber der entscheidende Term in dieser Formel ist nicht die Datenbankkapazität — die kontrollierst du. Es ist die Hit-Rate. Und die kontrollierst du nicht.

Der Faktor ist geborgt

Hit-Rates von 80–95% sind in der Praxis üblich — aber sie sind kein Verdienst guter Implementierung. Sie sind eine Folge davon, dass sich echte Nutzer zipf-verteilt verhalten: Viele suchen dasselbe, wenige suchen Seltenes. Die Kapazität des Systems hängt damit nicht davon ab, wie viel Traffic kommt, sondern welcher.

Das ist der Unterschied zwischen Performance-Optimierung und Skalierung. Performance-Optimierung sagt: Unter normalen Bedingungen ist das System schneller. Skalierung sagt: Doppelte Ressourcen, doppelte Kapazität — egal was kommt.

Ein gecachtes System ist auf eine Traffic-Verteilung optimiert, nicht auf ein Traffic-Volumen. Und das macht es fragil.

Der Bot, der den Shop zur Strecke brachte

Ich habe einmal erlebt, wie ein einzelner Bot einen Online-Shop in die Knie zwang — nicht durch Volumen, sondern durch sein Zugriffsmuster. Der Bot war kein Angreifer, sondern gehörte zu einer Preisvergleichsseite: Er rief systematisch alle Varianten eines Produkts ab. Jede Farbe, jede Größe, jede Kombination. In hoher Frequenz, aber pro Variante nur einmal.

Aus Sicht des Caches war das katastrophal. Normale Kunden suchen nach „Nike Air Max” — ein Cache-Hit nach dem ersten Request. Der Bot suchte nach „Nike Air Max” — und klickte danach jede Größe, Farbe, Variante an, bevor er zum nächsten Produkte ging. Kombinationen, die der Zipf-Verteilung eben nicht folgten – und daher fast alles Cache-Misses. Jeder ging voll auf die Datenbank.

Aber es kam schlimmer. Der Shop verwendete einen LRU-Cache mit begrenzter Größe. Die Bot-Requests verdrängten die populären Einträge, so dass der Cache nach dem Durchlauf des Bots für den “normalen Traffic” wirkungslos war. Die Antwortzeiten stiegen, die Queues liefen voll, und der Shop wurde unbenutzbar. Nicht weil zu viel Traffic kam, sondern weil der falsche Traffic kam.

Der Long Tail

Die Bot-Geschichte ist ein Extremfall, aber sie illustriert ein grundsätzliches Problem. Auch ohne Bots verteilen sich die restlichen 20% der Requests auf eine riesige Menge verschiedener Anfragen. In einem Shop mit einer Million Produkten werden die meisten Produkte selten oder nie gesucht. Diese Anfragen — der sogenannte Long Tail — sind nicht cachebar. Nicht weil der Cache zu klein wäre, sondern weil sich das Caching nicht lohnt: Ein Eintrag, der einmal pro Stunde abgefragt wird, belegt Speicher, ohne jemals einen zweiten Hit zu erzeugen.

Und genau diese Long-Tail-Anfragen sind die teuren. Kein Cache-Hit, also volle Datenbankabfrage. Oft komplexere Queries — Facettensuchen, seltene Filterkombinationen, Volltextsuchen nach obskuren Begriffen. Die Servicezeiten streuen stärker als bei den populären Anfragen, die brav aus dem Cache kommen.

In der Sprache von Kingman: Der Cache macht den Durchschnitt schneller — aber er macht die Verteilung breiter. Die schnellen Cache-Hits (30 Millisekunden) und die langsamen Datenbank-Queries (300 Millisekunden) erzeugen genau die bimodale Mischung, die bei hoher Auslastung zu überproportionalen Wartezeiten führt. Die Variabilität V steigt, also steigt die Empfindlichkeit gegenüber Auslastung. Der Bereich zwischen „läuft prima” und „bricht zusammen” wird schmaler.

Wer nur Median und P90 betrachtet, hält Caching für Magie — die große Mehrheit der Requests kommt in 30 statt 300 Millisekunden. Aber das P99 erzählt die Wahrheit: Die langsamsten 1% der Requests sind die Long-Tail-Anfragen, die unter hoher Last aus 300 Millisekunden schnell 3 Sekunden machen. Kapazitätsplanung wird schwieriger, weil die tatsächliche Belastung davon abhängt, welche Requests gerade ankommen — nicht nur wie viele.

Cache-Stampede

Es gibt einen zweiten Effekt, der Caching unter Last gefährlich machen kann. Wenn ein populärer Cache-Eintrag abläuft — etwa der für ein gerade viel beworbenes Produkt — und gleichzeitig hundert Requests für genau diesen Eintrag eintreffen, schlagen alle hundert auf die Datenbank durch. Kurzzeitig explodiert die Last, als gäbe es keinen Cache.

Das ist die Cache-Stampede, auch Thundering Herd genannt. Bei niedriger Auslastung kein Problem — die Datenbank verkraftet den kurzen Burst. Bei hoher Auslastung kann dieser Burst genügen, um das System über den Sättigungspunkt zu schieben. Und weil die Antwortzeiten dann für alle steigen, dauert es länger, den Cache wieder zu füllen — eine Rückkopplung, die sich selbst verstärkt. Im Kern ist das ein Backpressure-Problem: Der Cache fällt als Schutzmechanismus weg, und das System hat keine zweite Verteidigungslinie.

Gegenmaßnahmen existieren: Cache-Lock (nur ein Thread füllt den Eintrag, die anderen warten), stochastisches Vorab-Erneuern (der Eintrag wird erneuert, bevor er abläuft), oder gestaffelte TTLs. Alles lösbar, aber nur wenn man weiß, dass das Problem existiert. Der Preis ist allerdings eine massive Erhöhung der Komplexität, eine weitere Fehlerquelle und ein schwer vorherzusehendes Verhalten des Systems.

Wenn der Cache zur Krücke wird

Es gibt ein Szenario, das Teams meistens erst entdecken, wenn es zu spät ist: Ein System, das auf warme Caches angewiesen ist, um seine Last zu bewältigen, kann mit leeren Caches nicht starten.

Bei OTTO war das 2010 einer der Ausgangspunkte für die Neuentwicklung. Das Altsystem hing so stark am Cache, dass ein Neustart unter Produktionslast unmöglich war — die Datenbank hätte den Traffic ohne Cache-Schutz nicht überlebt. Ein einfacher Restart wurde zum Planungsproblem: Wann ist genug Traffic-Pause, um den Cache wieder aufzuwärmen? „Startup-Fähigkeit unter Last” war deshalb eine der wichtigsten Anforderungen an das neue System — nicht weil es eine elegante Architekturidee war, sondern weil das Fehlen dieser Fähigkeit monatelang Schmerzen bereitet hatte.

Das Cold-Start-Problem ist die Nagelprobe: Wenn der Cache ausfällt oder leer ist und das System deshalb nicht mehr funktioniert, dann ist der Cache keine Optimierung mehr — er ist Infrastruktur. Und Infrastruktur muss anders betrieben werden als ein Best-Effort-Speedup. Ein System, das ohne Cache nicht starten kann, ist ein System, dessen Kapazität nicht auf seinen Ressourcen beruht, sondern auf seinem Zustand.

“There are only two hard things…”

Phil Karlton wird das Zitat zugeschrieben: “There are only two hard things in Computer Science: cache invalidation and naming things.” Cache-Invalidierung ist deshalb so schwer, weil sie ein Konsistenzproblem ist.

Ein gecachter Wert ist eine Kopie. Sobald sich das Original ändert — ein Preis wird aktualisiert, ein Produkt ausverkauft — ist die Kopie falsch. Die Frage ist: Wie lange darf sie falsch sein?

Bei einer TTL von fünf Minuten zeigt der Shop bis zu fünf Minuten einen falschen Preis an. Bei ereignisbasierter Invalidierung (die Datenbank-Änderung löst ein Event aus, das den Cache leert) ist die Inkonsistenz kürzer — aber nie null. Und die Infrastruktur für Event-basierte Invalidierung hat ihren eigenen Preis: Messaging-Systeme, Eventual Consistency, zusätzliche Fehlerquellen.

Je mehr Caching-Schichten ein System hat — Browser-Cache, CDN, Application-Cache, Datenbank-Cache —, desto mehr Stellen gibt es, an denen veraltete Daten stecken können. Und desto schwerer wird es, eine Änderung zuverlässig bis zum Nutzer durchzureichen.

Das ist im Kern ein Amdahl-Problem. Jede aktive Invalidierung — ob per Event, per API-Call oder per Broadcast — ist ein serieller Anteil: eine Koordination, die mit jeder weiteren Cache-Schicht und jedem weiteren Consumer wächst. TTL-basierte Invalidierung umgeht die Kopplung, erkauft sich das aber mit Inkonsistenz. Event-basierte Invalidierung reduziert die Inkonsistenz, erhöht aber den seriellen Anteil. Beides gleichzeitig geht nicht — das ist derselbe Trade-off, den PACELC beschreibt.

Performance-Werkzeug, kein Skalierungsprinzip

Caching macht ein System schneller und preiswerter im Betrieb. Das ist wertvoll — keine Frage. Aber es macht ein System nicht skalierbarer, weil die gewonnene Kapazität nicht auf Ressourcen beruht, sondern auf einer Annahme über das Verhalten der Nutzer.

Die Kapazität eines gecachten Systems ist eine Funktion der Hit-Rate. Die Hit-Rate ist eine Funktion des Traffic-Musters. Und das Traffic-Muster kontrollierst du nicht. Ein Bot mit dem “falschen” Zugriffsmuster, ein kalter Cache nach einem Deployment, eine Cache-Stampede zur falschen Zeit — jedes dieser Szenarien kann die effektive Kapazität auf einen Bruchteil reduzieren, ohne dass sich am Volumen etwas geändert hat.

Echte Skalierbarkeit heißt: Kapazität ist eine Funktion der Ressourcen — doppelte Ressourcen, doppelte Kapazität. Dafür braucht es architektonische Maßnahmen: funktionale Dekomposition, Partitionierung der Daten, Entkopplung der Services. Genau darum geht es in dieser Serie.

Caching ist ein mächtiger erster Hebel — aber wer sein System darauf baut, baut auf Sand.


Quellen

  • Breslau et al. (1999) — Web Caching and Zipf-like Distributions: Evidence and Implications. IEEE INFOCOM 1999.
  • Zipf (1949)Human Behavior and the Principle of Least Effort. Addison-Wesley.

Kommentare