Ihre Datenbank-CPU schießt um drei Uhr morgens auf 100 %. Der Traffic sieht normal aus. Die Anzahl der Anfragen ist nicht ungewöhnlich hoch. Dennoch bricht Ihr primärer Speicher zusammen, weil jede einzelne dieser Anfragen zur exakt gleichen Zeit genau dasselbe anfordert.

Dies ist das Thundering-Herd-Problem.

Stellen Sie sich ein Stadion nach einem Konzert vor. Im Laufe einer Stunde kann derselbe Ausgang bequem alle Menschen nach draußen lassen. Aber wenn die gesamte Menge beschließt, innerhalb desselben zehnsekündigen Zeitfensters durch diese eine Tür zu gehen, geht die Tür nicht kaputt. Sie kann einfach die Gleichzeitigkeit (Concurrency) nicht bewältigen. Das Gedränge ist der Fehler, nicht die Tür.

Wo die Herde entsteht

Dieses Muster tritt immer dann auf, wenn viele Prozesse bei einem Ereignis synchronisieren. Sie benötigen keinen bösartigen Traffic. Normale Systeme fügen sich das selbst zu.

Cache-Expiration. Ein häufig genutzter (hot) Cache-Key läuft ab. Es könnte ein Produktkatalog, ein Feature-Flag oder die Berechtigungsliste eines Benutzers sein. Tausende von Anwendungsservern bemerken gleichzeitig den leeren Slot. Jeder Server öffnet in gutem Glauben seine eigene Datenbankverbindung und führt dieselbe schwere Abfrage aus, um den Wert neu aufzubauen. Die Datenbank wurde nie darauf konfiguriert, dieselbe teure Frage zweitausendmal parallel zu beantworten. Ein abgelaufener Key reißt den gesamten Cluster mit sich.

Wakeups im Connection Pool. In einigen Architekturen blockieren viele Worker-Prozesse bei einer einzigen Bedingung und warten darauf, dass Arbeit eintrifft. Wenn diese Bedingung erfüllt ist, weckt das Betriebssystem jeden schlafenden Worker auf. Nur ein einziger Worker erhält tatsächlich die Verbindung oder die Aufgabe. Der Rest wacht auf, stellt fest, dass er das Rennen verloren hat, und geht wieder schlafen. Dieser Zyklus verbraucht CPU durch Context Switches und kann die eigentliche produktive Arbeit verhungern lassen.

Retry-Storms. Ein nachgelagerter Dienst (downstream service) hat eine kurze Störung. Jeder Client fängt den Timeout ab und wartet ein festes Intervall, sagen wir genau eine Sekunde, bevor er es erneut versucht. Wenn der Dienst sich erholt und wieder Traffic annimmt, trifft ihn jeder Client im selben Augenblick. Der Dienst, der noch kalt und im Erholungsmodus ist, geht sofort wieder unter. Der Zyklus wiederholt sich.

Cron-Kollisionen. Wenn Sie Wartungsarbeiten, die Berichterstellung oder das Cache-Warming so planen, dass sie über eine Flotte von Instanzen hinweg exakt um 00:00 UTC ausgeführt werden, erzeugen Sie mit Uhrwerkpräzision eine Traffic-Spitze. Die Last ist vorhersehbar, aber die Konzentration ist fatal.

Request Coalescing

Die effektivste Lösung für Cache-Stampedes besteht darin, die gleiche Frage nicht mehr als einmal zu beantworten. Wenn ein Cache-Miss auftritt, möchten Sie nicht, dass 2.500 Threads jeweils eine Datenbankabfrage ausführen. Sie möchten, dass ein Thread die Abfrage ausführt, während die anderen 2.499 auf dieses Ergebnis warten.

Dieses Muster wird oft als Request Coalescing bezeichnet. In Go bietet das singleflight-Paket in golang.org/x/sync eine kanonische Implementierung. Die erste Goroutine, die Do für einen bestimmten Key aufruft, startet die Arbeit. Nachfolgende Aufrufer mit demselben Key blockieren bei demselben Do-Aufruf. Wenn die Arbeit abgeschlossen ist, erhalten alle Wartenden das Ergebnis gleichzeitig. Sie haben eine einzige Abfrage ausgeführt und 2.500 Anfragen bedient.

Sie können ein ähnliches Verhalten mit einer In-Memory-Concurrent-Map aus Promises, Futures oder Channels aufbauen. Der Trick besteht darin, eine laufende Anfrage (in-flight request) atomar zu prüfen und zu registrieren. Wenn die Anfrage fehlschlägt, erhalten alle den Fehler, was in der Regel das richtige Verhalten ist. Wenn sie erfolgreich ist, erhalten alle den gecachten Wert, und nachfolgende Anfragen greifen direkt auf den Cache zu. Die Datenbanklast sinkt von einer vertikalen Spitze zu einer flachen Linie.

Probabilistische vorzeitige Expiration

Manchmal möchte man den Stampede ganz vermeiden, anstatt ihn nur zu verwalten. Hier kommt die probabilistische vorzeitige Expiration ins Spiel, die durch Algorithmen wie XFetch implementiert wird.

Anstatt einer harten Ablaufzeit trägt jeder gecachte Wert ein „weiches“ Ablaufzeitfenster (soft expiration window). Wenn eine Anfrage eintrifft, generiert die Anwendung eine Zufallszahl und führt eine einfache Wahrscheinlichkeitsprüfung durch. Wenn der Würfelwurf „Ja“ sagt, aktualisiert diese eine Anfrage den Cache vorzeitig. Wenn nicht, liefert die Anfrage den leicht veralteten Wert aus.

Da jede Anfrage ihre eigenen Würfel wirft, verteilen sich die Aktualisierungen über das weiche Zeitfenster. Ein Client aktualisiert vielleicht 40 Sekunden vor dem harten Ablauf, ein anderer nach 12 Sekunden, und die meisten anderen gar nicht. Die Arbeit verschiebt sich von einer einzelnen synchronisierten Spitze hin zu einem sanften