Redis-Probleme beheben für mehr Stabilität und Geschwindigkeit
Manchmal läuft bei Transloadit etwas schief. Wenn das passiert, möchten wir transparent damit umgehen und zu unseren Fehlern stehen. In diesem Sinne lege ich ein Problem offen, das letzte Woche 81 unserer Kunden betroffen hat.
Anfang letzter Woche gingen Meldungen über eine erhöhte Anzahl von Fehlern des Typs
WORKER_JOB_ERROR ein. Das zeigt uns, dass es ein Kommunikationsproblem zwischen unseren
Uploadern (den Maschinen, die Dateien entgegennehmen und Assemblies orchestrieren) und unseren Drones
(den Maschinen, die Encoding-Aufgaben ausführen) gibt.
Wir haben das Problem auf Twitter und transloaditstatus.com geteilt und mit der Untersuchung begonnen.
Als Autoren des retry-Moduls haben wir bei Transloadit für viele Dinge Retries implementiert. Das bedeutet auch, dass sich nahezu alle Assemblies ohne Datenverlust erneut ausführen lassen. In einigen Fällen ließen sich Assemblies jedoch nicht erneut ausführen, und in vielen anderen Fällen waren sie schlicht deutlich langsamer, weil bei Fehlern Retries mit exponentiellem Back-off einsetzten.
Nachdem wir unsere Logs durchforstet und den Code debuggt hatten, zeigte sich, dass wir Jobs verloren, die im regionalen Redis-Cluster us-east in der Queue liegen, ebenso wie Heartbeats zwischen den erwähnten Uploadern und Drones. Durch das Überwachen der Heartbeats können wir einen Job an eine andere Drone übergeben, falls die ursprüngliche Drone keine mehr sendet.
Wir haben sofort eine Notlösung angewendet, indem wir verloren gegangene Nachrichten erneut hinzugefügt und unser Heartbeat-Monitoring gelockert haben. Dieser Ansatz bekämpfte jedoch nur die Symptome, statt die Wurzel des Problems anzugehen. Zudem ging er zulasten längerer Assembly-Ausführungszeiten. Mit anderen Worten: Wir mussten mehr tun.
Der Aktionsplan
Das Team kam also zusammen, und verschiedene Mitglieder wurden auf unterschiedliche Wege geschickt, um die Ursache des Problems zu ermitteln. Konkret haben wir:
- Geprüft, ob wir kritische Redis-Patches verpasst hatten, und auf die neueste stabile Version aktualisiert.
- Ein vollständiges Code-Audit unserer clientseitigen Bibliothek durchgeführt und bewertet, wie wir sie einsetzen.
- Unser Monitoring, unsere Metriken, Alerts und Log-Einträge verbessert. Zahlreiche Metriken werden bereits überwacht, aber wir können nur für eine begrenzte Anzahl sinnvolle Alerts einrichten. Möglicherweise war unsere Auswahl unzureichend.
Als das Team wieder zusammenkam, um die Ergebnisse zu teilen, hatten sich an mehreren Fronten Probleme gezeigt:
- Wir setzten nicht die neueste Version von Redis ein, in der eine Reihe alarmierender Bugs behoben
worden waren, von denen einige mit
[FIX] Fixed data lossmarkiert waren. - Das Team fand keine Probleme in der aktuellen Version der clientseitigen Bibliothek, die wir verwendeten. ✅
- Wir hatten unter anderem die Anzahl der Redis-Befehle, die Verbindungen und die Swap-Nutzung verfolgt, nicht jedoch die Bandbreite. Wir betrieben in den USA ein Cluster mit 3 Knoten, und es schien unwahrscheinlich, dass wir alle ihre Netzwerkkarten auslasten würden. Bei genauerem Hinsehen stellte sich jedoch heraus, dass genau das der Fall war.

Sobald klar wurde, dass wir durch die Bandbreite gedrosselt wurden, haben wir die Größe unseres Clusters verdoppelt. Das sollte die Last auf sechs Redis-Knoten verteilen und einen Gesamtlesedurchsatz von ~6Gbit/s ergeben. Wir waren überzeugt, dass das sicher ausreichen würde.

Danach bemerkten wir einen sofortigen Anstieg des Bandbreitenverbrauchs:

Der Verbrauch erreichte jedoch schnell neue Sättigungspunkte, und viele Assemblies blieben langsam. Zunächst dachten wir, dass wir die erforderliche Größe unseres Clusters immer noch unterschätzten. Wir waren durch die Grenzen von drei Redis-Knoten gedrosselt worden, und da auch sechs Redis-Knoten nicht auszureichen schienen, fügten wir weitere drei Redis-Knoten hinzu, um zu sehen, wie sich das auf den Bandbreitenverbrauch auswirken würde.
Zu unserer großen Überraschung schien das die Lage nur zu verschlimmern. Bei genauerem Hinsehen fiel uns auf, dass der Großteil des ausgehenden Traffics vom Cluster-Leader erzeugt wurde. Mit ~4Gbit/s gab er inzwischen weit mehr Traffic aus, als Amazon überhaupt als möglich bewirbt (was interessantes Material für einen späteren Blogbeitrag sein könnte), während der Traffic auf den anderen Redis-Knoten deutlich im Rahmen blieb.
Das war merkwürdig, denn ausgehender Traffic (Lesevorgänge) sollte sich über das gesamte Cluster verteilen. Warum erzeugte der Leader so viel davon? Abgesehen von der Verarbeitung von Schreibvorgängen und Pub/Sub sollte er nur etwa ein Neuntel des gesamten ausgehenden Traffics ausgeben.
Weniger ist mehr
Erst dann wurde uns klar: Replikation. Jeder Schreibvorgang ging an den Leader, und – statt die geschriebenen Bits auf zwei Redis-Knoten zu synchronisieren – musste er diese Daten nun auf acht Redis-Knoten synchronisieren. Da Redis kein ausgefeiltes Protokoll wie gossip einsetzt, musste alles, was unsere Drones übermittelten, nun auf acht statt auf zwei Knoten kopiert werden (was bereits problematisch war). Das ist viermal mehr Arbeit und ausgehender Traffic für diesen einzelnen Redis-Leader.
Besonders schmerzhaft ist, dass unsere Daten schreibintensiv und hochgradig flüchtig sind. Sie sind nur für einen kurzen Moment nützlich. Sobald die Daten kopiert waren, mussten sie also kurz darauf auch wieder gelöscht werden.
Da wir fehlende Daten erneut hinzufügen können, haben wir überdacht, ob die scheinbar gute Idee der Replikation überhaupt eine Anforderung war.
Idealerweise hätten wir gar keinen Replikations-Traffic und würden auf einen leeren Redis-Knoten umschalten. Bei AWS Elasticache ist Replikation für automatisches Failover jedoch zwingend erforderlich. Mit diesem neu gewonnenen Wissen haben wir die Zahl der Read Replicas auf eine reduziert. Unsere Logs zeigten sofort einen Rückgang bei Traffic und Fehlerraten, und auch die Assemblies wurden wieder schneller.
Durchatmen
Nachdem wir endlich die Ursache gefunden hatten, konnten wir wieder durchatmen und fragten uns, wie es dazu kommen konnte. Es war einigermaßen rätselhaft, denn wir konnten keine Änderungen an unserem Code oder unserer Infrastruktur finden, die die plötzliche Sättigung unserer Redis-Kapazität erklären würden.
Zwei Veränderungen im Umfeld fielen uns jedoch auf:
- Ein starker Anstieg des HLS-Traffics. HLS segmentiert große Videos in viele kleine Fragmente, sodass mobile Nutzer den nächsten Video-Chunk in der für ihre aktuelle Verbindung optimalen Qualität herunterladen können (wobei bei wirklich schlechter Verbindung alles elegant bis auf reines Audio heruntergestuft wird). Das kann leicht dazu führen, dass pro Video tausendmal mehr Metadaten ausgetauscht werden.
- Wir können mehr Drones online haben, um dasselbe Volumen in kürzerer Zeit zu verarbeiten. Das führt zwangsläufig zu stärker ausgelasteten zentralen Knoten
Es war die Kombination dieser beiden Veränderungen, die diesen Engpass in unserer Infrastruktur offenlegte. Anschließend trafen wir die falsche Entscheidung, die Zahl der Redis-Knoten zu erhöhen, um das Problem zu entschärfen, doch es stellte sich heraus, dass mehr Maschinen den Durchsatz tatsächlich belasteten. Um mehr Traffic bewältigen zu können, mussten wir weniger Knoten einsetzen.
Ausblick
Als Ergebnis unserer umfassenden Untersuchung sind nun alle oben genannten Softwarekomponenten aktualisiert, sodass wir außerhalb der aufgetretenen Bandbreitenprobleme nicht mehr anfällig für bekannte Bugs sind, die zum Verlust von Heartbeats oder Daten führen könnten. Wir haben außerdem zusätzliches Logging und zusätzliche Alarme eingerichtet, falls wir uns erneut Sättigungsgrenzen nähern sollten.
Der Traffic zwischen den Redis-Knoten und unseren Drones und Uploadern war legitim, und deshalb müssen wir beginnen, nach Wegen zu suchen, diesen Traffic zu dezentralisieren. Die Reduzierung des Replikations-Traffics hat uns vielleicht etwas Zeit verschafft, aber wir wollen ein Wachstum tragen können, das mit nur einem einzigen Redis-Knoten, der die Schreibvorgänge annimmt, nicht möglich ist. Selbst wenn wir vertikal skalieren (mit einem einzelnen größeren Redis-Knoten) und auf Replikation verzichten, rückt damit ein neuer Engpass unangenehm nah.
Wir haben darüber nachgedacht, Read Replicas von Read Replicas einzusetzen, damit der Replikations-Traffic verteilt wird, aber – wenn unser Wachstum anhält – würde uns auch das nur ein paar weitere Monate verschaffen. Da wir zudem schreibgebunden sind und wissen, dass diese Schreibvorgänge auf einer einzigen Maschine bleiben müssen, fällt diese Option vollständig weg.
Wir haben auch erwogen, auf einen anderen (verteilten) Datenspeicher umzusteigen, der genau für diesen Zweck optimiert ist, aber eine derart große Migration dürfte Wochen, wenn nicht Monate, dauern. Diese Zeit haben wir schlicht nicht. Datenspeicher im laufenden Betrieb auszutauschen, ist schon schwierig genug, erst recht, wenn man es eilig hat.
Sharding scheint derzeit unsere beste Option zu sein. Sharding erlaubt es, Daten in verschiedene logische Gruppen aufzuteilen und jede Gruppe auf separater Hardware zu betreiben (oder auf derselben, Sie entscheiden). Andere Unternehmen sharden häufig pro einzelnem Kunden. Wir sharden derzeit bereits pro Region und planen nun zusätzliches Sharding pro Uploader. Sharding pro Kunde scheint in unserem Setup weniger ideal, weil ein Kunde mit einem größeren Import Redis im Alleingang auslasten könnte. Wir skalieren Uploader jedoch bereits mit Traffic-Spitzen hoch, sodass wir mit zunehmendem Traffic schrittweise mehr Redis-Kapazität gewinnen. Traffic wird mit dem Uploader-Namen getaggt, sodass auch ersichtlich ist, welcher Redis-Knoten behandelt werden muss.
Die Arbeit daran hat bereits begonnen, und wir planen den Start für diese Woche. In der Zwischenzeit haben wir das Hochskalieren der Drones pro Region begrenzt. Das bedeutet, dass große Batch-Importe etwas länger dauern können als erwartet, das System aber stabil bleibt.
Es tut uns leid
Wir verstehen, dass langsame Ausführungszeiten sehr frustrierend sein können, ebenso wie die Notwendigkeit, Assemblies manuell erneut auszuführen, wenn das nicht automatisch geschehen ist. Wir möchten uns aufrichtig entschuldigen und denjenigen unter Ihnen, die davon stark betroffen waren, eine Woche kostenlosen Service anbieten. Melden Sie sich bitte bei uns, dann bringen wir das in Ordnung. Eine Rückerstattung Ihres Geldes ist natürlich vermutlich nicht das, worauf es Ihnen ankommt. Ein zuverlässiger Service schon. Dennoch hoffen wir, dass es den Ärger ein wenig lindert und zeigt, dass wir Probleme wie diese sehr ernst nehmen.
