Postmortem-Bibliothek

Bluesky: Loopback-Death-Spiral und fehlende Concurrency Limits

Dieser Artikel analysiert den Bluesky-Ausfall im April 2026, bei dem ein fehlendes Concurrency Limit in einem internen RPC-Handler zu Ephemeral Port Exhaustion, Logging-Überlastung und wiederkehrenden AppView-Crashes führte. Wir zeigen, wie große Batch-Requests eine Backend-„Death Spiral“ auslösten, wie das Engineering-Team den Service stabilisierte und welche Learnings sich daraus für Concurrency Management, Observability und Failure Isolation ableiten lassen.

Unternehmen und Produkt

Bluesky ist eine dezentrale Social-Media-Plattform, die auf dem AT Protocol basiert. Die Plattform betreibt ein offenes Ökosystem, in dem Nutzer portable Identitäten über verschiedene Service Provider hinweg verwalten können. Eine zentrale Komponente dieser Infrastruktur ist AppView, die globale Netzwerkdaten aggregiert und daraus Nutzer-Feeds sowie Profile bereitstellt.

Um die enorme Skalierung zu bewältigen, setzt AppView auf eine High-Performance-Data-Plane, die auf ScyllaDB und einer Memcached-Schicht basiert. Diese Caching-Strategie minimiert die Datenbanklast und stellt sicher, dass Millionen gleichzeitiger Requests mit einer Latenz im Sub-Millisekunden-Bereich verarbeitet werden können.

Was war passiert?

Der Incident wurde durch einen neu bereitgestellten internen Service ausgelöst, der begann, GetPostRecord-Requests mit Batches von 15.000 bis 20.000 URIs zu senden. Obwohl die Request-Frequenz gering war (weniger als drei Requests pro Sekunde), überforderte die schiere Größe der Batches das Connection-Management des Systems.

Da für den betreffenden RPC-Handler kein Concurrency-Limit definiert war, versuchte das System, pro Request 20.000 gleichzeitige Goroutines zu starten, um memcached abzufragen. Dadurch wurde folgende Ereigniskette ausgelöst:

  • Tausende memcached-Verbindungen wurden in kurzer Zeit geöffnet und wieder geschlossen, wodurch sämtliche verfügbaren TCP-Ports erschöpft wurden.
  • Sockets blieben im TCP-TIME_WAIT-Status hängen, sodass keine neuen Verbindungen mehr aufgebaut werden konnten.
  • Das System begann, diese Fehler mit mehreren Millionen Log-Einträgen pro Sekunde zu protokollieren.
  • Die Go-Runtime erzeugte Tausende OS-Threads, um blockierende Logging-Syscalls zu verarbeiten. Das führte zu massiven Garbage-Collection-(GC)-Pausen und schließlich zu Out-of-Memory-(OOM)-Crashes.

Der Service geriet dadurch in einen Kreislauf: Er lief jeweils etwa 30 Minuten stabil, bevor er durch einen OOM-Crash neu gestartet wurde — nur um anschließend sofort wieder vollständig ausgelastete Connection-Pools vorzufinden.

Die eigentliche Ursache war eine einzelne fehlende Codezeile in einer internen Go-Library. Konkret:

  • Fehlende Concurrency Guard: Der GetPostRecord-Endpoint war der einzige RPC-Handler im gesamten System, bei dem ein Aufruf von errgroup.SetLimit() fehlte.
  • Logging-Overhead: Das System nutzte blockierende write(2)-Syscalls für das Logging. Bei mehreren Millionen Fehlern pro Sekunde führte das zu einer explosionsartigen Zunahme der Thread-Anzahl, wodurch die Go-Runtime überlastet wurde.
  • Aggressives Tuning: Die Umgebungsvariablen für GOGC und GOMEMLIMIT waren so aggressiv konfiguriert, dass kein ausreichender Puffer für den plötzlichen Anstieg von OS-Threads und Memory-Pressure vorhanden war.

Timeline

  • 3. April, 22:16 UTC: Erste „address already in use“-Fehler in den Backend-Logs registriert.
  • 4. April (Samstag): Erste Alarmierung ausgelöst. Die Engineers vermuteten zunächst ein Problem im Network Transit.
  • 5. April (Sonntag): Während das Troubleshooting weiterlief, kam es weiterhin zu intermittierenden Service-Einbrüchen.
  • 6. April (Montag): Der Incident wurde auf SEV-1 hochgestuft; 50 % der Nutzer waren von intermittierenden Ausfällen über einen Zeitraum von acht Stunden betroffen.
  • 6. April, 23:00 UTC: Emergency-„Band-Aid“-Fix ausgerollt (Loopback-IP-Rotation); der Service stabilisierte sich.
  • 8. April (Mittwoch): Root Cause identifiziert und permanentes Concurrency-Limit

Time to Detect (TTD): 2 Stunden (ab dem ersten größeren Ausfall am Samstag)

Time to Resolve (TTR): 50 Stunden (bis zur vollständigen Stabilisierung)

Wer war betroffen?

Der Ausfall betraf hauptsächlich Nutzer, die über ein bestimmtes Rechenzentrum bedient wurden, in dem der neue interne Service aktiv war.

  • User Impact: Etwa 50 % der gesamten Bluesky-Nutzerbasis waren von intermittierenden Verbindungsproblemen und Fehlern beim Laden des Feeds betroffen.
  • Service Impact: Die AppView-Data-Plane war von wiederkehrenden OOM-Crashes und extremer Latenz während der GC-Pausen betroffen.
  • Developer Impact: Interne Teams konnten sich für die serviceübergreifende Datenaggregation nicht mehr zuverlässig auf den GetPostRecord-RPC verlassen.

Wie reagierte Bluesky?

Bluesky reagierte mit der Implementierung eines äußerst unkonventionellen „Band-Aid“-Fixes, um die Death Spiral zu durchbrechen. Die Engineers modifizierten den memcached-Client so, dass er einen benutzerdefinierten Dialer verwendete, der für jede Verbindung zufällig eine Loopback-IP-Adresse aus dem Bereich 127.0.0.0/8 auswählte. Dadurch wurde der verfügbare ephemere Portbereich von etwa 65.000 auf mehrere Millionen erweitert, sodass der Service den TIME_WAIT-Bottleneck umgehen konnte.

Nach dem Incident kündigte Bluesky folgende Maßnahmen an:

  • Einführung verpflichtender errgroup-Limits für sämtliche RPC-Handler.
  • Umstellung von Blocking-Logging auf Prometheus-basierte Metriken und OTEL-Tracing für High-Scale-Errors.
  • Verbesserung der Client-spezifischen Observability, um „schwere“ interne Requests sofort identifizieren zu können.

Wie kommunizierte Bluesky?

Die Kommunikation erfolgte über die offizielle Statusseite und einen ausführlichen technischen Blogbeitrag von Jim Calabro. Obwohl das Team das Problem aufgrund irreführender Traceroute-Daten zunächst fälschlicherweise als Fehler eines Drittanbieters identifizierte, korrigierte es die Angaben umgehend und lieferte eine transparente Aufschlüsselung des internen Programmierfehlers.

Wichtige Erkenntnisse für Teams

  • Concurrency begrencen: Gehen Sie nicht davon aus, dass Batch-Größen dauerhaft klein bleiben. Definieren Sie für netzwerkgebundene Tasks immer feste Limits für die Erstellung von Goroutines.
  • Logging überprüfen: Hochfrequentes Logging in Error-Paths kann schnell zu einem Performance-Bottleneck werden, der die gesamte Runtime zum Absturz bringt.
  • Metriken erweitern: Implementieren Sie Client-spezifische Observability, um allgemeine Traffic-Spikes von einer durch einzelne Quellen verursachten Ressourcenerschöpfung unterscheiden zu können.
  • Vorsicht bei TIME_WAIT: In High-Throughput-Umgebungen kann der standardmäßige Bereich ephemerer Ports schnell zum stillen Killer werden, wenn Verbindungen nicht korrekt recycelt werden.

Kurzfassung

Bluesky erlitt einen SEV-1-Ausfall, der durch ein fehlendes Concurrency-Limit in einem Batch-Request-Handler ausgelöst wurde. Dadurch kam es zu einer Erschöpfung der verfügbaren TCP-Ports sowie zu einer Logging-„Death Spiral“, die die Go-Runtime praktisch lahmlegte. Der Incident konnte mithilfe eines Emergency-Hacks zur Randomisierung von Loopback-IPs sowie eines anschließenden Code-Patches behoben werden.

So kann ilert helfen

Bei einem komplexen „Death-Spiral“-Incident zählt jede Minute. ilert hilft Teams dabei, Ausfallzeiten zu minimieren, und zwar durch:

  • Erweiterte Alarmierung: Sofortige Benachrichtigung der zuständigen Backend-Engineers per Anruf oder SMS bei Traffic-Dips — ohne Abhängigkeit von stillen Dashboards..
  • Incident-Kommunikation: Transparente Statusseiten in Echtzeit halten Nutzer kontinuierlich informiert und reduzieren gleichzeitig das Volumen an Support-Tickets.
  • Integration von Observability: Ihre Metriken werden direkt mit den Bereitschaftsdiensten verknüpft, sodass Alarmierungen zu „Port-Erschöpfung“ sofort den richtigen Techniker erreichen.
Weitere Postmortems finden:
Bereit, dein Incident-Management zu verbessern?
Danke! Deine Einreichung ist eingegangen!
Hoppla! Beim Absenden des Formulars ist etwas schief gelaufen.
Unsere Cookie-Richtlinie
Wir verwenden Cookies, um Ihre Erfahrung zu verbessern, den Seitenverkehr zu verbessern und für Marketingzwecke. Erfahren Sie mehr in unserem Datenschutzrichtlinie.
Open Preferences
Danke! Deine Einreichung ist eingegangen!
Hoppla! Beim Absenden des Formulars ist etwas schief gelaufen.
Danke! Deine Einreichung ist eingegangen!
Hoppla! Beim Absenden des Formulars ist etwas schief gelaufen.