Super Slurper est l'outil de migration de données de Cloudflare conçu pour faciliter les transferts de données à grande échelle entre les fournisseurs de stockage d'objets cloud et Cloudflare R2. Depuis son lancement, des milliers de développeurs ont utilisé Super Slurper pour transférer des pétaoctets de données d'AWS S3, de Google Cloud Storage et d'autres services compatibles S3 vers R2.
Nous avons toutefois vu une opportunité de le rendre encore plus rapide. Nous avons intégralement remanié Super Slurper à l'aide de notre plateforme pour développeurs (fondée sur Cloudflare Workers, Durable Objects et Queues) et amélioré la vitesse de transfert jusqu'à cinq fois. Dans cet article, nous allons étudier l'architecture originale, les goulets d'étranglement en matière de performances que nous avons identifiés, la manière dont nous les avons résolus et l'incidence réelle de ces améliorations.
Les goulots d'étranglement initiaux en matière d'architecture et de performances
À l'origine, Super Slurper partageait son architecture avec SourcingKit, un outil conçu pour importer en masse des images d'AWS S3 dans Cloudflare Images. SourcingKit a été déployé sur Kubernetes et s’est exécuté parallèlement au service Images. Lorsque nous nous sommes lancés dans le développement de Super Slurper, nous l'avons séparé dans son propre espace de noms Kubernetes et avons introduit quelques nouvelles API pour faciliter son utilisation dans le cadre du stockage d'objets. Cette configuration s'est avérée efficace et a aidé des milliers de développeurs à transférer leurs données vers R2.
Cependant, cela ne s’est pas fait sans difficulté. SourcingKit n'a pas été conçu pour gérer l'échelle nécessaire aux transferts de grande ampleur, de l'ordre de plusieurs pétaoctets. SourcingKit (et par extension Super Slurper) fonctionnait sur des clusters Kubernetes situés dans l'un de nos datacenters principaux. Il devait donc partager les ressources de calcul et la bande passante avec le plan de contrôle, les outils d'analyse et d'autres services de Cloudflare. À mesure que le nombre de migrations augmentait, ces contraintes en matière de ressources sont devenues un goulot d'étranglement évident.
Pour un service de transfert de données entre fournisseurs de stockage d'objets, la tâche est simple : dressez la liste des objets depuis la source, copiez-les dans la destination et recommencez. C'est exactement ainsi que fonctionnait le Super Slurper d'origine. Nous avons répertorié les objets du bucket source, transmis cette liste à une file d'attente basée sur Postgres (pg_queue
), puis nous avons procédé à l'extraction de cette file d'attente à un rythme constant pour copier les objets. Compte tenu de l'ampleur des migrations de stockage d'objets, l'utilisation de la bande passante était inévitablement élevée. Cela rendait la mise à l'échelle difficile.
Pour répondre aux contraintes de bande passante avec une exécution exclusivement dans notre datacenter principal, nous avons introduit Cloudflare Workers dans la combinaison. Au lieu de gérer la copie des données dans notre datacenter principal, nous avons commencé à demander à un Worker de procéder à la copie proprement dite :
À mesure que l'utilisation de Super Slurper a augmenté, la consommation de ressources de Kubernetes en a fait de même. Au cours des transferts de données, nous avons consacré beaucoup de temps à attendre les E/S ou le stockage réseau, sans avoir à effectuer de tâches gourmandes en puissance de calcul. Nous n'avions donc pas besoin de plus de mémoire ou de processeur, il nous fallait plus de concurrence.
Pour répondre à la demande, nous avons continué à augmenter le nombre de réplicas. Mais finalement, nous avons fini par nous heurter à un mur. Nous étions confrontés à des problèmes d'évolutivité lorsque nous exécutions des dizaines de pods, alors que nous voulions disposer de plusieurs ordres de grandeur.
Nous avons décidé de repenser l'ensemble de la procédure, en partant de principes fondamentaux, plutôt que de nous reposer sur l'architecture dont nous avions hérité. En une semaine environ, nous avons élaboré une démonstration de faisabilité approximative en utilisant Cloudflare Workers, Durable Objects et Queues. Nous avons répertorié les objets du bucket source, les avons transférés dans une file d'attente, puis avons écoulé les messages de la file d'attente pour lancer les transferts. Bien que cela semble très semblable à ce que nous avons fait lors de la mise en œuvre originale, le fait de développer sur notre plateforme pour développeurs nous a permis d'évoluer automatiquement dans un ordre de grandeur plus élevé qu'auparavant.
Cloudflare Queues : cette fonctionnalité permet les transferts d'objets asynchrones et la mise à l'échelle automatique afin de répondre au nombre d'objets à migrer.
Cloudflare Workers : exécute des tâches de calcul légères sans la surcharge provoquée par Kubernetes et optimise l’endroit où chaque partie du processus s’exécute dans le monde, pour une latence plus faible et de meilleures performances.
Durable Objects (DO) soutenus par SQLite : cette solution se comporte comme une base de données totalement distribuée, éliminant les limitations liées à une instance PostgreSQL unique.
Hyperdrive : assure un accès rapide aux données historiques sur les tâches issues de la base de données PostgreSQL originale, en les conservant comme un magasin d'archives.
Nous avons effectué quelques tests et avons constaté que notre démonstration de faisabilité était plus lente que la mise en œuvre originale pour les petits transferts (quelques centaines d'objets), mais qu'elle égalait, voire a fini par dépasser les performances de la version originale à mesure que les transferts s'étendaient à des millions d'objets. C'était le signe que nous devions investir le temps nécessaire pour déployer notre démonstration de faisabilité en production.
Nous avons supprimé nos solutions de démonstration de faisabilité, travaillé sur la stabilité et trouvé de nouvelles façons d'évoluer vers une concurrence encore plus élevée. Après quelques itérations, nous sommes arrivés à une solution qui nous satisfait.
Nouvelle architecture : Workers, Queues et Durable Objects
Couche de traitement : gérer le flux de migration
Au cœur de notre couche de traitement se trouvent les files d'attente, les consommateurs et les instances Workers. Le processus se présente comme suit :
Lancement d'une migration
Lorsqu'un client déclenche une migration, tout commence par l'envoi d'une requête à notre API Workers. Cette instance Workers prend les détails de la migration, les stocke dans la base de données, puis ajoute un message à la file d'attente de liste pour lancer le processus.
Liste des objets du bucket source
Le consommateur de file d'attente de listes est l'endroit où les choses commencent à se mettre en place. Il extrait les messages de la file d'attente, récupère les listes d'objets du bucket source, applique les filtres nécessaires et stocke les métadonnées importantes dans la base de données. Ensuite, il crée de nouvelles tâches en mettant en file d'attente les messages de transfert d'objets dans la file d'attente de transfert.
Nous mettons immédiatement en file d'attente les nouveaux lots de travail, afin de maximiser la concurrence. Un mécanisme de limitation intégré nous empêche d'ajouter de nouveaux messages à nos files d'attente lorsque des défaillances inattendues se produisent, à l'image de la défaillance de systèmes dépendants. Cette opération permet de maintenir la stabilité et d'éviter les surcharges lors des perturbations.
Transferts d'objets efficaces
Les Workers de consommateur de la file d'attente de transferts procèdent à l'extraction des messages de transfert d'objets de la file d'attente, et garantissent ainsi que chaque objet n'est traité qu'une seule fois, en verrouillant la clé d'objet dans la base de données. Une fois le transfert terminé, l'objet est déverrouillé. Pour les objets plus volumineux, nous les divisons en blocs gérables et les transférons sous forme de téléchargements multiparties.
Traiter les défaillances avec grâce
Les défaillances sont inévitables dans tout système distribué, et nous devions faire en sorte d'en tenir compte. Nous avons mis en œuvre des tentatives automatiques pour les défaillances transitoires, afin que les problèmes n'interrompent pas le flux de migration. Mais si un problème ne peut être résolu avec de nouvelles tentatives, le message est transféré vers la file d'attente des lettres mortes (DLQ), où il est consigné pour examen et résolution ultérieurs.
Achèvement des tâches et gestion du cycle de vie
Une fois que tous les objets sont répertoriés et que les transferts sont en cours, le consommateur de file d'attente du cycle de vie garde un œil sur tout. Il surveille les transferts en cours, afin de vérifier qu'aucun objet n'est oublié. Lorsque tous les transferts sont terminés, la tâche est marquée comme terminée et le processus de migration se termine.
Couche de base de données : stockage durable et récupération des données traditionnelles
Lors de l'élaboration de notre nouvelle architecture, nous savions que nous avions besoin d'une solution robuste, capable de gérer d'immenses ensembles de données, tout en garantissant la récupération des données historiques sur les tâches. C'est là que notre combinaison de Durable Objects (DO) et d'Hyperdrive est entrée en jeu.
Durable Objects
Nous avons attribué à chaque compte un Durable Object dédié au suivi des tâches de migration. Le DO de chaque tâche stocke des détails essentiels, tels que les noms de bucket, les options utilisateur et l'état de la tâche. Ainsi, tout reste organisé et facile à gérer. Pour prendre en charge les migrations de grande ampleur, nous avons également ajouté une instance de DO par lot qui gère tous les objets en file d'attente pour transfert, en stockant l'état du transfert, les clés d'objet et les éventuelles métadonnées supplémentaires.
Alors que les migrations nécessitaient jusqu'à plusieurs milliards d'objets, nous avons dû faire preuve de créativité pour ce qui est du stockage. Nous avons mis en œuvre une stratégie de partage pour répartir les charges de requêtes, afin d'éviter les goulets d'étranglement et contourner la limite de stockage de 10 Go de Durable Object SQLite. Lorsque les objets sont transférés, nous effectuons un nettoyage de leurs détails, ce qui permet d'optimiser l'espace de stockage au passage. Il est surprenant de constater combien de stockage un milliard de clés objet peut nécessiter !
Hyperdrive
Puisque nous étions en train de reconstruire un système avec des années d'historique des migrations, nous avions besoin d'un moyen de préserver et d'accéder à chaque détail des migrations passées. Hyperdrive sert de pont vers nos systèmes existants, permettant une récupération fluide des données historiques sur les tâches, depuis notre base de données PostgreSQL principale. Il ne s'agit pas uniquement d'un mécanisme de récupération de données, mais également d'une archive pour les scénarios de migration complexes.
Résultats : Super Slurper transfère désormais les données vers R2 jusqu'à 5 fois plus rapidement
Après tout cela, avons-nous réellement atteint notre objectif visant à accélérer les transferts ?
Nous avons effectué un test de migration de 75 000 objets d'AWS S3 vers R2. Avec la mise en œuvre originale, le transfert a pris 15 minutes et 30 secondes. Après nos améliorations de performances, il a suffi de 3 minutes et 25 secondes pour finaliser la même migration.
Lorsque les migrations de production ont commencé à utiliser le nouveau service au mois de février, nous avons constaté des améliorations encore plus importantes dans certains cas, notamment en fonction de la répartition des objets. Super Slurper existe depuis environ deux ans. Mais l'amélioration des performances lui a permis de déplacer beaucoup plus de données : 35 % de tous les objets copiés par Super Slurper l'ont été au cours des deux derniers mois.
Défis
L'une des plus grandes difficultés auxquelles nous avons été confrontés avec la nouvelle architecture était la gestion des messages en double. Il se produit des doublons pour plusieurs raisons :
Queues assure une diffusion « at-least-once » (au moins une fois), ce qui signifie que les consommateurs peuvent recevoir le même message plus d'une fois pour vérifier la livraison.
Les échecs et les nouvelles tentatives peuvent également créer des doublons apparents. Par exemple, si une requête adressée à un Durable Object échoue alors que l'objet a déjà été transféré, la nouvelle tentative peut traiter à nouveau le même objet.
Si l'opération n'est pas effectuée correctement, le même objet peut être transféré plusieurs fois. Pour résoudre ce problème, nous avons mis en œuvre plusieurs stratégies visant à faire en sorte que chaque objet soit précisément pris en compte et transféré une seule fois :
Le listage étant séquentiel (par exemple, pour obtenir l'objet 2, vous avez besoin du jeton de poursuite de l'objet 1 du listage), nous attribuons un ID de séquence à chaque opération de listage. Cela nous permet de détecter les listes en double et d'empêcher plusieurs processus de démarrer simultanément. Cela s'avère particulièrement utile car nous n'attendons pas la fin des opérations de base de données et de mise en file d'attente avant de dresser la liste du lot suivant. Si la liste 2 échoue, nous pouvons réessayer, et si la liste 3 a déjà commencé, nous pouvons court-circuiter les tentatives inutiles.
Chaque objet est verrouillé lorsque son transfert commence, ce qui empêche les transferts parallèles du même objet. Une fois le transfert réussi, l'objet est déverrouillé par la suppression de sa clé dans la base de données. Si un message pour cet objet réapparaît plus tard, nous pouvons partir du principe qu'il a déjà été transféré si la clé n'existe plus.
Nous nous appuyons sur les transactions de base de données pour garantir la précision de nos calculs. Si un objet ne se déverrouille pas, son nombre reste inchangé. De même, si une clé d'objet ne parvient pas à être ajoutée à la base de données, le nombre n'est pas mis à jour et une nouvelle tentative sera effectuée ultérieurement.
Comme dernière mesure de protection, nous vérifions si l'objet existe déjà dans le bucket cible et s'il a été publié après le démarrage de notre migration. Si c'est le cas, nous supposons qu'il a été transféré par notre processus (ou un autre) et nous l'ignorons en toute sécurité.
Quelle est la suite pour Super Slurper ?
Nous explorons continuellement les moyens de rendre Super Slurper plus rapide, plus évolutif et encore plus facile à utiliser, et ce n'est que le début.
Nous avons récemment lancé la possibilité de migrer depuis n'importe quel fournisseur de stockage compatible S3 !
Les migrations de données sont toujours limitées à trois migrations simultanées par compte, mais nous souhaitons élever cette limite. Cela permettra de diviser les préfixes objets en migrations distinctes et procéder à des exécutions en parallèle, ce qui augmentera considérablement la vitesse à laquelle un bucket peut être migré. Pour plus d'informations sur Super Slurper et sur la procédure de migration des données depuis un stockage d'objets existant vers R2, consultez notre documentation.
PS Dans le cadre de cette mise à jour, nous avons beaucoup simplifié l'interaction avec l'API, afin que les migrations puissent désormais être gérées de manière programmatique!