Vous enregistrez une présentation de 300 Mo sur votre portable, et une minute plus tard elle s'ouvre sur votre téléphone. Puis vous corrigez une coquille sur une diapositive, et la mise à jour prend une seconde, parce que seule la partie modifiée voyage. Un service de synchronisation de fichiers donne l'impression que c'est facile, mais en dessous se cache une séparation soignée entre deux types de données très différents, plus un protocole qui supporte les réseaux instables, les modifications hors ligne et les appareils qui se réveillent des jours plus tard.
Cet article conçoit un service de synchronisation façon Dropbox : exigences et chiffres, séparation entre métadonnées et contenu, découpage et hash de contenu, protocole d'envoi et de validation, journal de métadonnées et curseurs, notifications, conflits et ramasse-miettes, avec un diagramme d'architecture.
Exigences
- Les fichiers enregistrés sur un appareil apparaissent sur les autres appareils de l'utilisateur en quelques secondes à quelques minutes.
- Seules les données modifiées sont envoyées et téléchargées.
- Les modifications faites hors ligne se synchronisent quand l'appareil se reconnecte, et des modifications concurrentes ne s'écrasent jamais en silence.
- Les dossiers partagés fonctionnent entre comptes.
- Aucune perte de données, jamais ; un historique des versions pour la récupération.
Capacité
Supposons 10 millions d'appareils actifs, chacun synchronisant 20 changements par jour, avec en moyenne 256 Ko de nouvelles données par changement après déduplication :
| Grandeur | Calcul | Résultat |
|---|---|---|
| Changements par jour | 10M × 20 | 200 millions |
| Validations de métadonnées | 200 000 000 / 86 400 s | ~2 300 par seconde |
| Nouvelles données de blocs | 200M × 256 Ko | ~51 To par jour |
Le débit de métadonnées est modeste ; le volume de données est important mais simple à stocker. Cette asymétrie guide la conception.
Séparer les métadonnées du contenu
La décision la plus importante consiste à traiter le contenu et les métadonnées des fichiers comme deux systèmes différents :
- Le contenu est stocké sous forme de blocs immuables, adressés par le hash de leurs octets. Les blocs sont gros, écrits une seule fois et faciles à répliquer dans un stockage objet.
- Les métadonnées enregistrent quel fichier, à quel chemin, dans quelle version, se compose de quels blocs. Elles sont petites, changent sans cesse et exigent des transactions et un ordre strict.
Dropbox a décrit cette séparation dans son article sur le streaming sync : chaque fichier est découpé en blocs de 4 Mo, chaque bloc est haché en SHA-256, et le contenu d'un fichier est identifié par la liste de ces hash, sa liste de blocs. L'API publique expose toujours la même idée sous la forme d'un hash de contenu : le SHA-256 de chaque bloc de 4 Mo, puis le SHA-256 de la concaténation des hash de blocs.
Le découpage : fixe ou selon le contenu
Les blocs de taille fixe sont simples et rapides à calculer. Leur faiblesse apparaît quand des octets sont insérés près du début d'un fichier : chaque frontière de bloc après l'insertion se décale, et chaque hash de bloc change.
Le découpage selon le contenu (content-defined chunking) place les frontières là où un hash glissant calculé sur le contenu correspond à un motif, comme le font la somme de contrôle glissante de rsync et des algorithmes plus récents tels que FastCDC. Les frontières se déplacent avec le contenu, si bien qu'une insertion ne modifie que les morceaux qui l'entourent.
| Insérer 10 octets au début d'un fichier de 100 Mo | Blocs fixes de 4 Mo | Selon le contenu, ~4 Mo en moyenne |
|---|---|---|
| Morceaux modifiés | Les 25 | Environ un |
| Données à envoyer | ~100 Mo | ~4 Mo |
| Coût CPU du découpage | Faible | Plus élevé : un hash glissant sur chaque octet |
Pour les fichiers surtout complétés en fin ou remplacés d'un bloc, les blocs fixes suffisent. Pour les fichiers modifiés en leur milieu, comme les gros documents, les images disque ou les bases de données, le découpage selon le contenu économise bien plus de bande passante.
Le protocole d'envoi et de validation
- Le client de bureau remarque un changement grâce au mécanisme de surveillance de fichiers du système d'exploitation, attend que le fichier cesse de changer, puis le découpe et le hache.
- Il envoie une validation (commit) au service de métadonnées : ce chemin, cette nouvelle liste de blocs, sur la base de la version parente N.
- Le service de métadonnées vérifie quels hash il stocke déjà et répond avec ceux dont il a besoin.
- Le client n'envoie que ces blocs au service de blocs, en parallèle, chacun vérifié par son hash. Un envoi interrompu reprend à partir des blocs manquants.
- Le client relance la validation. Comme tous les blocs existent désormais, le service de métadonnées ajoute une nouvelle version du fichier au journal.
L'ordre est la propriété de sûreté : une version qui référence des blocs n'est jamais validée tant qu'ils ne sont pas tous stockés. Un plantage à n'importe quel moment laisse soit l'ancienne version, soit la nouvelle, jamais un fichier qui pointe vers des données manquantes.
L'adressage par contenu apporte aussi la déduplication gratuitement. Un fichier copié dans trois dossiers, ou ramené à une version antérieure, n'envoie rien de nouveau. La déduplication entre comptes économise davantage d'espace, mais elle peut révéler que quelqu'un d'autre a stocké le même fichier ; beaucoup de conceptions ne dédupliquent donc qu'au sein d'un compte ou d'un dossier partagé.
Le journal de métadonnées et les curseurs
Regroupez les fichiers en namespaces, comme l'arborescence personnelle d'un utilisateur ou un dossier partagé. Pour chaque namespace, le service de métadonnées tient un journal en ajout seul : chaque changement reçoit le numéro de séquence suivant, avec le chemin, la version et la liste de blocs.
Chaque client stocke un curseur par namespace : le dernier numéro de séquence qu'il a appliqué. La synchronisation descendante se résume alors à une seule question : « donne-moi tout ce qui suit mon curseur dans ce namespace ». Le journal rend cela peu coûteux, ordonné et reprenable. Un appareil resté hors ligne une semaine pose la même question et reçoit une semaine de changements. Partitionnez les métadonnées par namespace, car les changements d'un namespace n'ont jamais besoin d'une transaction avec un autre.
Les notifications : dire aux appareils de se synchroniser
Interroger le journal depuis chaque appareil inactif serait du gaspillage. À la place, les clients de bureau inactifs gardent une connexion long poll ouverte vers un service de notification. Quand le journal d'un namespace avance, le service répond aux requêtes en attente par « changements disponibles » ; chaque client liste alors les changements qui suivent son curseur et télécharge les blocs qui lui manquent. L'API publique de Dropbox propose le même modèle avec list_folder/longpoll et list_folder/continue.
Les applications mobiles ne peuvent pas garder de connexions ouvertes en arrière-plan : elles s'appuient sur les notifications push de la plateforme et se synchronisent à l'ouverture de l'application.
Côté client
Le client de bureau représente la moitié du système. Il tient une base locale du dernier état synchronisé de chaque fichier : chemin, version, liste de blocs, taille et date de modification. La comparaison de trois états décide de chaque action :
| Fichier local / dernier état synchronisé | Distant / dernier état synchronisé | Action |
|---|---|---|
| Modifié | Inchangé | Envoyer |
| Inchangé | Modifié | Télécharger |
| Modifié | Modifié | Conflit |
| Inchangé | Inchangé | Rien |
Un cache local de blocs évite de télécharger des blocs que l'appareil possède déjà dans d'autres fichiers.
Les conflits
Deux appareils modifient le même fichier hors ligne. Tous deux valident plus tard sur la base de la version parente N. Le service de métadonnées accepte la première validation, qui crée la version N+1, et rejette la seconde, car sa version parente n'est plus à jour. Plutôt que de fusionner des fichiers binaires qu'il ne comprend pas, le client perdant enregistre sa version à côté de l'original sous forme de copie en conflit et synchronise les deux. L'utilisateur tranche.
C'est de la concurrence optimiste : chaque validation nomme la version sur laquelle elle se fonde. La même vérification traite les opérations sur les dossiers, plus délicates en pratique : un renommage sur un appareil et une modification sur un autre, ou une suppression en concurrence avec une modification. Écrivez ces cas noir sur blanc ; c'est là que les moteurs de synchronisation perdent des données.
Historique des versions et ramasse-miettes
Les anciennes versions conservent leur liste de blocs : restaurer un fichier revient simplement à valider une ancienne liste de blocs comme version la plus récente. Les blocs ne deviennent des déchets que lorsqu'aucune version de la période de rétention ne les référence plus. Exécutez le ramasse-miettes en mark-and-sweep avec un délai de grâce, jamais comme une suppression immédiate à la disparition de la dernière référence, car un client est peut-être sur le point de valider une liste de blocs qui réutilise un bloc que le serveur croit orphelin.
Le diagramme de synchronisation de fichiers
Contenu du diagramme
- Laptop (writer)
- Phone (reader)
- Sync backend
- API gateway
- Metadata service
- Notification service
- Block service
- Journal per namespace
- journal events
- Blocks by SHA-256 (4 MB)
- 1, 3, 4
- 5. changes available
- 6. list after cursor, get blocks
- 1. commit (parent v7)
- 2. missing hashes
- 3. upload missing blocks
- 5. answer long polls
- 4. append v8
- namespace changed
- wake
- put by hash
Comment le lire :
- Le portable à gauche valide une liste de blocs auprès du service de métadonnées et reçoit en retour les hash qui manquent au serveur.
- Les envois de blocs vont au service de blocs et aboutissent dans le stockage objet, indexés par hash.
- Le service de métadonnées ajoute la nouvelle version au journal du namespace dans Postgres et publie le changement.
- Le service de notification répond au long poll du téléphone ; le téléphone liste les changements qui suivent son curseur et ne télécharge que les blocs manquants.
- Le bloc Markdown liste les six étapes du protocole dans l'ordre.
Dessiner clairement les systèmes de synchronisation
- Dessinez métadonnées et contenu comme deux chemins séparés. Stockages différents, passage à l'échelle différent, modes de panne différents.
- Numérotez les étapes du protocole sur les arêtes. Validation, hash manquants, envoi, nouvelle validation : l'ordre est l'argument de correction.
- Montrez le second appareil. La synchronisation concerne autant le lecteur que l'auteur ; un diagramme à un seul client cache les notifications et les curseurs.
- Étiquetez la clé de partitionnement : le namespace pour les métadonnées, le hash pour les blocs.
Pour une approche générale, voyez l'exemple de diagramme de conception de système. Le côté push ressemble à la conception d'un système de notifications, les envois découpés vers un stockage objet apparaissent aussi dans le diagramme d'architecture de streaming vidéo, et le partitionnement des métadonnées suit le diagramme de sharding de base de données.
Le construire dans Archboard
- Connectez-vous avec Google ou GitHub et créez un diagramme.
- Ajoutez deux nœuds Appareil du pack Réseau : un portable et un téléphone.
- Ajoutez un Cadre du pack Base pour le backend de synchronisation, avec une Passerelle API du pack Cloud en bordure.
- Dans le cadre, ajoutez trois nœuds Instance de calcul du pack Cloud pour les services de métadonnées, de blocs et de notification, un nœud Postgres du pack Données pour le journal, un nœud Stockage objet du pack Cloud pour les blocs et un nœud Kafka du pack Données pour les événements du journal.
- Numérotez les arêtes dans l'ordre du protocole : validation, hash manquants, envoi, nouvelle validation, notification, liste et téléchargement.
- Ajoutez un bloc Markdown du pack Base avec le protocole et la capacité.
L'exemple utilise 11 nœuds de diagramme et reste dans les limites de l'offre Free.
FAQ
Pourquoi les services de synchronisation découpent-ils les fichiers en blocs ? Les blocs permettent au client de n'envoyer que ce qui a changé, de reprendre les transferts interrompus et de dédupliquer les données identiques. Le hash d'un bloc est son identité : le serveur sait immédiatement quels blocs il possède déjà.
Faut-il un découpage de taille fixe ou selon le contenu ? Les blocs de taille fixe sont plus simples et moins coûteux à calculer. Le découpage selon le contenu garde des frontières stables quand des octets sont insérés au milieu d'un fichier, ce qui économise de la bande passante pour les fichiers modifiés sur place.
Comment un client sait-il ce qui a changé sur le serveur ? Il garde un curseur par namespace et demande au service de métadonnées toutes les entrées du journal qui le suivent. Un long poll ou une notification push lui indique quand demander.
Comment les modifications en conflit sont-elles gérées ? Chaque validation nomme la version sur laquelle elle se fonde. Si un autre appareil a validé en premier, la validation échoue, et le client conserve sa modification sous forme de copie en conflit au lieu d'écraser l'autre.
La déduplication fonctionne-t-elle entre utilisateurs ? Elle le peut, mais elle risque de révéler qu'un autre compte a stocké le même fichier, et elle ne fonctionne pas avec un chiffrement propre à chaque utilisateur. Beaucoup de services limitent la déduplication à un compte ou à un dossier partagé.