Super Slurper ist das Datenmigrationstool von Cloudflare, das entwickelt wurde, um groß angelegte Datenübertragungen zwischen Anbietern von Cloud-Objektspeicher und Cloudflare R2 einfach zu machen. Seit der Einführung haben Tausende von Entwicklerinnen und Entwicklern Super Slurper verwendet, um Petabytes an Daten von AWS S3, Google Cloud Storage und anderen S3-kompatiblen Diensten nach R2 zu verschieben.
Aber wir sahen eine Möglichkeit, sie noch schneller zu machen. Wir haben Super Slurper von Grund auf mit unserer Entwicklerplattform – die auf Cloudflare Workers, Durable Objects und Queues aufbaut – neu konzipiert und die Übertragungsgeschwindigkeit um bis zu 5x verbessert. In diesem Beitrag werfen wir einen Blick auf die ursprüngliche Architektur, die identifizierten Performance-Engpässe, unsere Lösungsansätze und die praktischen Auswirkungen dieser Verbesserungen.
Ursprüngliche Architektur- und Performance-Engpässe
Super Slurper teilte ursprünglich seine Architektur mit SourcingKit, einem Tool, das für den Massenimport von Bildern aus AWS S3 in Cloudflare Images entwickelt wurde. SourcingKit wurde auf Kubernetes implementiert und lief zusammen mit dem Images-Dienst. Als wir mit der Entwicklung von Super Slurper begannen, teilten wir es in einen eigenen Kubernetes-Namespace auf und führten ein paar neue APIs ein, um es für den Anwendungsfall der Objektspeicherung zu erleichtern. Dieses Setup hat gut funktioniert und Tausenden von Entwicklern geholfen, Daten auf R2 zu verlagern.
Allerdings hatte sie auch ihre Tücken. SourcingKit wurde nicht für den Umfang entwickelt, der für große Transfers im Petabyte-Bereich erforderlich ist. SourcingKit und damit auch Super Slurper wurden auf Kubernetes-Clustern betrieben, die sich in einem unserer Kernrechenzentren befanden. Das bedeutete, dass es sich Rechenressourcen und Bandbreite mit der Steuerungsebene, den Analytics und anderen Diensten von Cloudflare teilen musste. Als die Anzahl der Migrationen zunahm, wurden diese Ressourcenbeschränkungen zu einem eindeutigen Engpass.
Die Aufgabe eines Dienstes zur Datenübertragung zwischen Objektspeicher-Anbietern ist eigentlich recht simpel: Objekte am Quellspeicher auflisten, zum Ziel übertragen – und den Vorgang kontinuierlich wiederholen. Genau so hat der ursprüngliche Super Slurper funktioniert. Wir haben Objekte aus dem Quell-Bucket aufgelistet, diese Liste in eine Postgres-basierte Warteschlange (pg_queue
) verschoben und dann in einem gleichmäßigen Tempo aus dieser Warteschlange herausgezogen, um Objekte zu kopieren. Bei der Größenordnung solcher Objektspeicher-Migrationen war klar, dass die Bandbreitennutzung stark ausfallen würde. Das erschwerte die Skalierung.
Um die Bandbreitenbeschränkungen durch den alleinigen Betrieb in unserem Core-Rechenzentrum zu umgehen, haben wir Cloudflare Workers in die Architektur integriert. Das eigentliche Kopieren der Daten erfolgt nun nicht mehr im Core-Rechenzentrum, sondern wird von einem extern aufgerufenen Worker übernommen.
Mit steigender Nutzung von Super Slurper stieg auch der Ressourcenverbrauch für Kubernetes. Ein erheblicher Teil der Zeit während der Datenübertragung wurde durch das Warten auf Netzwerk-I/O oder Speicherzugriffe verbraucht – nicht durch rechenintensive Aufgaben. Wir brauchten also weder mehr Arbeitsspeicher noch mehr CPU-Leistung – sondern mehr Gleichzeitigkeit.
Wir haben die Zahl der Replikate immer weiter erhöht, um die Nachfrage zu bewältigen. Irgendwann war aber Schluss: Bereits bei einigen Dutzend Pods stießen wir auf Skalierungsprobleme – obwohl wir eigentlich in ganz anderen Größenordnungen arbeiten wollten.
Wir entschieden uns, den gesamten Ansatz von Grund auf neu zu durchdenken, anstatt weiterhin auf der übernommenen Architektur aufzubauen. Innerhalb von etwa einer Woche haben wir einen groben Proof of Concept mit Cloudflare Workers, Durable Objects und Queues erstellt. Wir listeten die Objekte aus dem Quell-Bucket auf, legten sie in eine Warteschlange und verarbeiteten anschließend die Nachrichten aus der Warteschlange, um die Übertragungen zu starten. Auch wenn dies der ursprünglichen Implementierung sehr ähnlich klingt, ermöglichte uns die Nutzung unserer Developer Platform eine automatische Skalierung um eine Größenordnung höher als zuvor.
Cloudflare Queues: Ermöglicht asynchrone Objektübertragungen und automatische Skalierung, um der Anzahl der zu migrierenden Objekte gerecht zu werden.
Cloudflare Workers: Führt schlanke Datenverarbeitungsaufgaben ohne den Kostenaufwand von Kubernetes aus und optimiert, wo auf der Welt die einzelnen Teile des Prozesses ausgeführt werden, um eine geringere Latenz und eine bessere Performance zu erzielen.
SQLite-gestützte Durable Objects (DOs): Fungiert als vollständig verteilte Datenbank und eliminiert die Einschränkungen einer einzelnen PostgreSQL-Instanz.
Hyperdrive: Bietet schnellen Zugriff auf historische Auftragsdaten aus der ursprünglichen PostgreSQL-Datenbank, die als Archivspeicher aufbewahrt wird.
Wir haben ein paar Tests durchgeführt und festgestellt, dass unser Proof-of-Concept bei kleinen Übertragungen (einige hundert Objekte) langsamer war als die ursprüngliche Implementierung, aber bei der Skalierung der Übertragungen in die Millionen von Objekten entsprach und diese schließlich übertraf. Das war der Startpunkt, den wir brauchten, um die Zeit zu investieren und unseren Proof of Concept in den Produktivbetrieb zu überführen.
Wir haben unsere Proof-of-Concept-Hacks entfernt, an der Stabilität gearbeitet und neue Wege gefunden, um Übertragungen auf noch höhere Gleichzeitigkeit zu skalieren. Nach einigen Iterationen hatten wir schließlich eine Lösung, mit der wir zufrieden waren.
Neue Architektur: Workers, Queues und Durable Objects
Verarbeitungsschicht: Verwaltung des Migrationsflusses
Das Herzstück unserer Verarbeitungsschicht sind Warteschlangen, Verbraucher und Worker. So sieht der Prozess aus:
Die Migration wird in die Wege geleitet
Wenn ein Client eine Migration auslöst, beginnt er mit einer Anfrage, die an unseren API Worker gesendet wird. Dieser Worker nimmt die Details der Migration auf, speichert sie in der Datenbank und fügt eine Nachricht zur List Queue hinzu, um den Prozess zu starten.
Auflisten der Objekte im Quell-Bucket
Mit der List Queue Consumer geht es dann richtig los. Sie zieht Nachrichten aus der Warteschlange, ruft Objektlisten aus dem Quell-Bucket ab, wendet alle notwendigen Filter an und speichert wichtige Metadaten in der Datenbank. Dann erstellt sie neue Aufgaben, indem sie Objekttransfernachrichten in die Transfer Queue einreiht.
Neue Arbeitspakete werden sofort in die Warteschlange gestellt, um die Gleichzeitigkeit zu maximieren. Ein integrierter Drosselungsmechanismus verhindert das Hinzufügen weiterer Nachrichten zur Warteschlange, wenn unerwartete Fehler auftreten – etwa durch den Ausfall abhängiger Systeme. So wird die Stabilität gewahrt und eine Überlastung bei Störungen vermieden.
Effiziente Objektübertragung
Die Transfer Queue Consumer-Workers ziehen Objekttransfernachrichten aus der Warteschlange und stellen durch das Sperren des Objektschlüssels in der Datenbank sicher, dass jedes Objekt nur einmal verarbeitet wird. Wenn die Übertragung abgeschlossen ist, wird das Objekt entsperrt. Größere Objekte zerlegen wir in handlichere Einheiten und übertragen sie als mehrteilige Uploads.
Souveräner Umgang mit Ausfällen
Ausfälle sind in jedem verteilten System unvermeidlich, und wir mussten sicherstellen, dass wir dies berücksichtigen. Wir haben automatische Wiederholungsversuche für vorübergehende Fehler implementiert, damit Probleme den Migrationsfluss nicht unterbrechen. Wenn etwas jedoch nicht bei Wiederholungsversuchen gelöst werden kann, geht die Nachricht in die Dead Letter Queue (DLQ), wo sie zur späteren Überprüfung und Lösung protokolliert wird.
Auftragsabwicklung und Lebenszyklusverwaltung
Sobald alle Objekte aufgelistet sind und die Übertragungen im Gange sind, behält der Lifecycle Queue Consumer alles im Auge. Er überwacht die laufenden Übertragungen und stellt sicher, dass kein Objekt zurückbleibt. Wenn alle Übertragungen abgeschlossen sind, wird der Auftrag als abgeschlossen markiert und der Migrationsprozess abgeschlossen.
Datenbankebene: Dauerhafter Speicher und Abruf von Altdaten
Bei der Entwicklung unserer neuen Architektur wussten wir, dass wir eine robuste Lösung benötigten, um riesige Datensätze zu verarbeiten und gleichzeitig den Abruf früherer Auftragsdaten zu gewährleisten. Hier kam unsere Kombination aus Durable Objects (DOs) und Hyperdrive ins Spiel.
Durable Objects
Wir haben jedem Konto ein dediziertes Durable Object gegeben, um Migrationsaufträge zu verfolgen. Im DO jedes Jobs werden wichtige Details gespeichert, z. B. Bucket-Namen, Benutzeroptionen und Jobstatus. Dadurch blieb alles organisiert und leicht zu verwalten. Um große Migrationen zu unterstützen, haben wir außerdem ein Batch-DO hinzugefügt, das alle für die Übertragung in der Warteschlange befindlichen Objekte verwaltet und ihren Übertragungsstatus, Objektschlüssel und alle zusätzlichen Metadaten speichert.
Als Migrationen auf Milliarden von Objekten skaliert wurden, mussten wir bei der Speicherung kreativ werden. Wir haben eine Sharding-Strategie implementiert, um die Anfragelasten zu verteilen, Engpässe zu verhindern und das 10-GB-Speicherlimit von SQLite DO zu umgehen. Während der Übertragung von Objekten bereinigen wir ihre Details und optimieren dabei den Speicherplatz. Man unterschätzt, wie viel Speicherplatz eine Milliarde Objektschlüssel benötigen kann!
Hyperdrive
Da wir ein System mit jahrelanger Migrationsgeschichte neu aufbauten, benötigten wir eine Möglichkeit, alle Details der vergangenen Migration beizubehalten und darauf zuzugreifen. Hyperdrive dient als Brücke zu unseren Altsystemen und ermöglicht einen nahtlosen Abruf von vergangenen Auftragsdaten aus unserer zentralen PostgreSQL-Datenbank. Es ist nicht nur ein Mechanismus zum Abruf von Daten, sondern ein Archiv für komplexe Migrationsszenarien.
Ergebnisse: Super Slurper überträgt Daten jetzt bis zu 5x schneller auf R2
Haben wir nach all dem also unser Ziel, Übertragungen schneller zu machen, tatsächlich erreicht?
Wir haben eine Testmigration von 75.000 Objekten von AWS S3 zu R2 durchgeführt. Bei der ursprünglichen Implementierung dauerte die Übertragung 15 Minuten und 30 Sekunden. Nach unseren Performance-Verbesserungen wurde dieselbe Migration in nur 3 Minuten und 25 Sekunden abgeschlossen.
Seitdem die Migrationen im Produktivbetrieb im Februar über den neuen Dienst laufen, haben wir in manchen Fällen sogar noch bessere Ergebnisse erzielt – vor allem je nach Größenverteilung der Objekte. Super Slurper gibt es seit etwa zwei Jahren. Aber die verbesserte Performance hat dazu geführt, dass es viel mehr Daten bewegen kann – 35 % aller von Super Slurper kopierten Objekte sind allein in den letzten zwei Monaten kopiert worden.
Herausforderungen
Eine der größten Herausforderungen bei der neuen Architektur war der Umgang mit doppelten Nachrichten. Duplikate konnten auf verschiedene Arten entstehen:
Queues bietet eine mindestens einmalige Zustellung, was bedeutet, dass die Verbraucher die gleiche Nachricht mehr als einmal erhalten können, um die Zustellung zu garantieren.
Fehlschläge und Wiederholungsversuche könnten auch zu vermeintlichen Duplikaten führen. Wenn beispielsweise eine Anfrage an ein Durable Object fehlschlägt, nachdem das Objekt bereits übertragen wurde, könnte der erneute Versuch dasselbe Objekt erneut verarbeiten.
Wenn es nicht richtig gehandhabt wird, kann dies dazu führen, dass dasselbe Objekt mehrmals übertragen wird. Um dieses Problem zu lösen, haben wir mehrere Strategien implementiert, um sicherzustellen, dass jedes Objekt korrekt erfasst und nur einmal übertragen wurde:
Da die Auflistung sequentiell ist (um beispielsweise Objekt 2 zu erhalten, benötigt man das Token von Auflistungsobjekt 1), weisen wir jedem Auflistungsvorgang eine Sequenz-ID zu. So können wir doppelte Einträge erkennen und verhindern, dass mehrere Prozesse gleichzeitig gestartet werden. Dies ist besonders nützlich, weil wir nicht warten müssen, bis die Datenbank- und Warteschlangenvorgänge abgeschlossen sind, bevor wir den nächsten Stapel auflisten. Wenn Liste 2 fehlschlägt, können wir es erneut versuchen, und wenn Liste 3 bereits begonnen hat, können wir unnötige Wiederholungsversuche ersparen.
Jedes Objekt wird zu Beginn seiner Übertragung gesperrt, sodass parallele Übertragungen desselben Objekts verhindert werden. Nach erfolgreicher Übertragung wird das Objekt entsperrt, indem sein Schlüssel aus der Datenbank gelöscht wird. Wenn eine Nachricht für dieses Objekt später wieder erscheint, können wir davon ausgehen, dass es bereits übertragen wurde, da der Schlüssel nicht mehr existiert.
Wir verlassen uns auf Datenbanktransaktionen, damit unsere Zählungen genau bleiben. Wenn ein Objekt nicht entsperrt werden kann, bleibt seine Anzahl unverändert. Wenn ein Objektschlüssel nicht zur Datenbank hinzugefügt wird, wird die Anzahl nicht aktualisiert und der Vorgang wird später wiederholt.
Als letzte Schutzmaßnahme (Failsafe) überprüfen wir, ob das Objekt bereits im Zielbucket existiert und nach Beginn unserer Migration veröffentlicht wurde. Wenn dem so ist, gehen wir davon aus, dass er von unserem Prozess (oder einem anderen) übertragen wurde und überspringen ihn bedenkenlos.
Wie geht es mit Super Slurper weiter?
Wir suchen ständig nach Möglichkeiten, Super Slurper schneller, skalierbarer und noch benutzerfreundlicher zu machen – dies ist erst der Anfang.
Vor kurzem haben wir die Möglichkeit eingeführt, von jedem S3-kompatiblen Speicheranbieter zu migrieren!
Datenmigrationen sind derzeit noch auf 3 gleichzeitige Migrationen pro Konto begrenzt, aber wir wollen diese Grenze erhöhen. Dadurch können Objekt-Präfixe in separate Migrationen aufgeteilt und parallel ausgeführt werden, was die Geschwindigkeit, mit der ein Bucket migriert werden kann, drastisch erhöht. Weitere Informationen zu Super Slurper und zur Migration von Daten von bestehenden Objektspeichern nach R2 finden Sie in unserer Dokumentation.
PS Im Rahmen dieses Updates haben wir die Interaktion mit der API viel einfacher gemacht, sodass Migrationen jetzt programmatisch verwaltet werden können!