Zum Inhalt springen

Systemdesign für Dateisynchronisation: So funktioniert Sync im Stil von Dropbox

Entwirf einen Sync-Dienst wie Dropbox: Chunking, Content-Hashes, ein Metadaten-Journal mit Cursorn, Benachrichtigungen, Konflikte und Uploads nur geänderter Blöcke.

· Gründer & Entwickler

Veröffentlicht am

Ein Laptop committet eine Blockliste an den Metadaten-Service, der pro Namespace ein Journal in Postgres führt und fehlende Hashes nennt. Der Laptop lädt sie zum Block-Service und Objektspeicher hoch. Ein Benachrichtigungsdienst weckt das Handy; es listet Änderungen und lädt Blöcke.

Du speicherst eine 300-MB-Präsentation auf deinem Laptop, und eine Minute später öffnet sie sich auf deinem Smartphone. Dann korrigierst du einen Tippfehler auf einer Folie, und das Update dauert eine Sekunde, weil nur der geänderte Teil übertragen wird. Ein Dateisync-Dienst lässt das einfach aussehen, aber darunter steckt eine sorgfältige Trennung zwischen zwei sehr unterschiedlichen Arten von Daten, plus ein Protokoll, das wacklige Netze, Offline-Änderungen und Geräte verkraftet, die erst Tage später aufwachen.

Dieser Beitrag entwirft einen Sync-Dienst im Stil von Dropbox: Anforderungen und Zahlen, die Trennung zwischen Metadaten und Inhalt, Chunking und Content-Hashes, das Upload- und Commit-Protokoll, das Metadaten-Journal und Cursor, Benachrichtigungen, Konflikte und Garbage Collection, mit einem Architekturdiagramm.

Anforderungen

  • Dateien, die auf einem Gerät gespeichert werden, erscheinen innerhalb von Sekunden bis Minuten auf den anderen Geräten des Nutzers.
  • Nur geänderte Daten werden hoch- und heruntergeladen.
  • Offline vorgenommene Änderungen synchronisieren sich, sobald das Gerät wieder verbunden ist, und gleichzeitige Änderungen überschreiben einander nie stillschweigend.
  • Geteilte Ordner funktionieren über Konten hinweg.
  • Niemals Datenverlust; ein Versionsverlauf zur Wiederherstellung.

Kapazität

Nimm 10 Millionen aktive Geräte an, die jeweils 20 Änderungen pro Tag synchronisieren, mit durchschnittlich 256 KB neuen Daten pro Änderung nach der Deduplizierung:

Größe Rechnung Ergebnis
Änderungen pro Tag 10M × 20 200 Millionen
Metadaten-Commits 200.000.000 / 86.400 s ~2.300 pro Sekunde
Neue Blockdaten 200M × 256 KB ~51 TB pro Tag

Die Metadatenrate ist bescheiden; das Datenvolumen ist groß, aber einfach zu speichern. Diese Asymmetrie bestimmt das Design.

Metadaten vom Inhalt trennen

Die wichtigste Entscheidung ist, Dateiinhalte und Dateimetadaten als zwei verschiedene Systeme zu behandeln:

  • Inhalt wird als unveränderliche Blöcke gespeichert, adressiert über den Hash ihrer Bytes. Blöcke sind groß, werden einmal geschrieben und lassen sich im Objektspeicher leicht replizieren.
  • Metadaten halten fest, welche Datei unter welchem Pfad in welcher Version aus welchen Blöcken besteht. Sie sind klein, ändern sich ständig und brauchen Transaktionen und eine strikte Reihenfolge.

Dropbox beschrieb diese Trennung in seinem Beitrag zu Streaming Sync: Jede Datei wird in 4-MB-Blöcke aufgeteilt, jeder Block wird mit SHA-256 gehasht, und der Inhalt einer Datei wird durch die Liste dieser Hashes identifiziert, ihre Blockliste. Die öffentliche API bietet dieselbe Idee bis heute als Content-Hash an: SHA-256 jedes 4-MB-Blocks, dann SHA-256 der aneinandergehängten Block-Hashes.

Chunking: fest oder inhaltsbasiert

Blöcke fester Größe sind einfach und schnell zu berechnen. Ihre Schwäche zeigt sich, wenn nahe am Dateianfang Bytes eingefügt werden: Jede Blockgrenze nach der Einfügung verschiebt sich, und jeder Block-Hash ändert sich.

Inhaltsbasiertes Chunking (Content-defined Chunking) setzt Grenzen dort, wo ein Rolling Hash über den Inhalt einem Muster entspricht, wie es rsyncs rollende Prüfsumme und neuere Algorithmen wie FastCDC tun. Die Grenzen wandern mit dem Inhalt, sodass eine Einfügung nur die Chunks um sie herum ändert.

10 Bytes am Anfang einer 100-MB-Datei einfügen Feste 4-MB-Blöcke Inhaltsbasiert, im Schnitt ~4 MB
Geänderte Chunks Alle 25 Etwa einer
Hochzuladende Daten ~100 MB ~4 MB
CPU-Kosten fürs Chunking Gering Höher: ein Rolling Hash über jedes Byte

Für Dateien, an die meist nur angehängt wird oder die komplett ersetzt werden, reichen feste Blöcke. Für Dateien, die in der Mitte bearbeitet werden, etwa große Dokumente, Disk-Images oder Datenbanken, spart inhaltsbasiertes Chunking deutlich mehr Bandbreite.

Das Upload- und Commit-Protokoll

  1. Der Desktop-Client bemerkt eine Änderung über den Dateibeobachter des Betriebssystems, wartet, bis sich die Datei nicht mehr ändert, und zerlegt und hasht sie dann.
  2. Er schickt einen Commit an den Metadaten-Service: dieser Pfad, diese neue Blockliste, basierend auf der Elternversion N.
  3. Der Metadaten-Service prüft, welche Hashes er schon speichert, und antwortet mit denen, die er braucht.
  4. Der Client lädt nur diese Blöcke parallel zum Block-Service hoch, jeden anhand seines Hashes geprüft. Ein unterbrochener Upload setzt bei den fehlenden Blöcken wieder an.
  5. Der Client wiederholt den Commit. Jetzt existiert jeder Block, also hängt der Metadaten-Service eine neue Version der Datei an das Journal an.

Die Reihenfolge ist die Sicherheitseigenschaft: Eine Version, die auf Blöcke verweist, wird nie committet, bevor alle davon gespeichert sind. Ein Absturz an beliebiger Stelle hinterlässt entweder die alte oder die neue Version, nie eine Datei, die auf fehlende Daten zeigt.

Inhaltsadressierung bringt außerdem Deduplizierung gratis mit. Eine Datei, die in drei Ordner kopiert oder auf eine frühere Version zurückgesetzt wird, lädt nichts Neues hoch. Kontoübergreifende Deduplizierung spart noch mehr Platz, kann aber verraten, dass jemand anderes dieselbe Datei gespeichert hat; deshalb deduplizieren viele Designs nur innerhalb eines Kontos oder eines geteilten Ordners.

Das Metadaten-Journal und Cursor

Gruppiere Dateien in Namespaces, etwa den eigenen Ordnerbaum eines Nutzers oder einen geteilten Ordner. Für jeden Namespace führt der Metadaten-Service ein Append-only-Journal: Jede Änderung bekommt die nächste Sequenznummer, zusammen mit Pfad, Version und Blockliste.

Jeder Client speichert pro Namespace einen Cursor: die letzte Sequenznummer, die er angewendet hat. Herunterladen ist dann eine einzige Frage: „Gib mir alles in diesem Namespace nach meinem Cursor.“ Das Journal macht das günstig, geordnet und wiederaufnehmbar. Ein Gerät, das eine Woche offline war, stellt dieselbe Frage und bekommt eine Woche an Änderungen. Sharde die Metadaten nach Namespace, denn Änderungen in einem Namespace brauchen nie eine Transaktion mit einem anderen.

Benachrichtigungen: Geräten sagen, dass sie syncen sollen

Das Journal von jedem untätigen Gerät aus abzufragen, wäre Verschwendung. Stattdessen halten untätige Desktop-Clients eine Long-Poll-Verbindung zu einem Benachrichtigungsdienst. Rückt das Journal eines Namespace vor, beantwortet der Dienst die wartenden Requests mit „Änderungen verfügbar“; jeder Client listet dann die Änderungen nach seinem Cursor auf und lädt die Blöcke herunter, die ihm fehlen. Die öffentliche API von Dropbox bietet dasselbe Muster als list_folder/longpoll und list_folder/continue an.

Mobile Apps können im Hintergrund keine Verbindungen offen halten, also setzen sie auf Push-Benachrichtigungen der Plattform und synchronisieren, wenn die App geöffnet wird.

Die Client-Seite

Der Desktop-Client ist die Hälfte des Systems. Er führt eine lokale Datenbank mit dem zuletzt synchronisierten Zustand jeder Datei: Pfad, Version, Blockliste, Größe und Änderungszeit. Der Vergleich dreier Zustände entscheidet jede Aktion:

Lokale Datei vs. zuletzt synchronisiert Remote vs. zuletzt synchronisiert Aktion
Geändert Unverändert Hochladen
Unverändert Geändert Herunterladen
Geändert Geändert Konflikt
Unverändert Unverändert Nichts

Ein lokaler Block-Cache verhindert, dass Blöcke heruntergeladen werden, die das Gerät schon in anderen Dateien hat.

Konflikte

Zwei Geräte bearbeiten offline dieselbe Datei. Beide committen später mit der Elternversion N. Der Metadaten-Service akzeptiert den ersten Commit, wodurch Version N+1 entsteht, und lehnt den zweiten ab, weil dessen Elternversion nicht mehr aktuell ist. Statt Binärdateien zusammenzuführen, die er nicht versteht, speichert der unterlegene Client seine Version als Konfliktkopie neben dem Original und synchronisiert beide. Der Nutzer entscheidet.

Das ist optimistische Nebenläufigkeit: Jeder Commit nennt die Version, auf der er basiert. Dieselbe Prüfung behandelt Verzeichnisoperationen, die in der Praxis schwieriger sind: eine Umbenennung auf einem Gerät und eine Änderung auf einem anderen oder ein Löschen, das mit einer Änderung um die Wette läuft. Schreib diese Fälle explizit auf; dort verlieren Sync-Engines Daten.

Versionsverlauf und Garbage Collection

Alte Versionen behalten ihre Blocklisten, also heißt eine Datei wiederherzustellen nur, eine alte Blockliste als neueste Version zu committen. Blöcke werden erst zu Müll, wenn keine Version innerhalb der Aufbewahrungsfrist mehr auf sie verweist. Betreib die Garbage Collection als Mark-and-Sweep mit Karenzzeit, nie als eifriges Löschen beim letzten Verweis, denn ein Client könnte gerade dabei sein, eine Blockliste zu committen, die einen Block wiederverwendet, den der Server für verwaist hält.

Das Dateisync-Diagramm

Ein Laptop committet eine Blockliste an den Metadaten-Service, der pro Namespace ein Journal in Postgres führt und fehlende Hashes nennt. Der Laptop lädt sie zum Block-Service und Objektspeicher hoch. Ein Benachrichtigungsdienst weckt das Handy; es listet Änderungen und lädt Blöcke.

11 Komponenten · 11 Verbindungen

In Archboard öffnen
Diagramminhalt
  • 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
Ein Sync-Backend im Stil von Dropbox. Clients zerlegen Dateien in gehashte Blöcke, laden nur die Blöcke hoch, die dem Server fehlen, und committen eine Blockliste in ein Metadaten-Journal; andere Geräte erfahren per Long Poll von der Änderung und laden nur die fehlenden Blöcke.

So liest du es:

  • Der Laptop links committet eine Blockliste an den Metadaten-Service und bekommt die Hashes zurück, die dem Server fehlen.
  • Block-Uploads gehen an den Block-Service und landen, nach Hash geschlüsselt, im Objektspeicher.
  • Der Metadaten-Service hängt die neue Version an das Namespace-Journal in Postgres an und veröffentlicht die Änderung.
  • Der Benachrichtigungsdienst beantwortet den Long Poll des Smartphones; das Smartphone listet die Änderungen nach seinem Cursor auf und lädt nur fehlende Blöcke herunter.
  • Der Markdown-Block listet die sechs Protokollschritte der Reihe nach auf.

Sync-Systeme klar zeichnen

  • Zeichne Metadaten und Inhalt als getrennte Pfade. Unterschiedliche Speicher, unterschiedliche Skalierung, unterschiedliche Fehlerbilder.
  • Nummeriere die Protokollschritte an den Kanten. Commit, fehlende Hashes, Upload, erneuter Commit: Die Reihenfolge ist das Korrektheitsargument.
  • Zeig das zweite Gerät. Beim Sync geht es genauso um den Leser wie um den Schreiber; ein Diagramm mit einem Client versteckt Benachrichtigungen und Cursor.
  • Beschrifte den Partitionsschlüssel: Namespace für Metadaten, Hash für Blöcke.

Für einen allgemeinen Ansatz sieh dir das System-Design-Diagramm-Beispiel an. Die Push-Seite ähnelt dem Systemdesign für Benachrichtigungen, Uploads in Chunks zum Objektspeicher tauchen auch im Architekturdiagramm für Video-Streaming auf, und das Partitionieren der Metadaten folgt dem Datenbank-Sharding-Diagramm.

In Archboard nachbauen

  1. Melde dich mit Google oder GitHub an und leg ein Diagramm an.
  2. Füge zwei Gerät-Nodes aus dem Netzwerk-Pack hinzu: einen Laptop und ein Smartphone.
  3. Füge einen Rahmen aus dem Basis-Pack für das Sync-Backend hinzu, mit einem API-Gateway aus dem Cloud-Pack an seinem Rand.
  4. Füge im Rahmen drei Recheninstanz-Nodes aus dem Cloud-Pack für Metadaten-, Block- und Benachrichtigungsdienst hinzu, einen Postgres-Node aus dem Daten-Pack für das Journal, einen Objektspeicher-Node aus dem Cloud-Pack für Blöcke und einen Kafka-Node aus dem Daten-Pack für Journal-Events.
  5. Nummeriere die Kanten in Protokollreihenfolge: Commit, fehlende Hashes, Upload, erneuter Commit, Benachrichtigung, Auflisten und Herunterladen.
  6. Füge einen Markdown-Block aus dem Basis-Pack mit Protokoll und Kapazität hinzu.

Das Beispiel nutzt 11 Diagramm-Nodes und bleibt damit im Free-Plan.

FAQ

Warum zerlegen Sync-Dienste Dateien in Blöcke? Blöcke erlauben es dem Client, nur Geändertes hochzuladen, unterbrochene Übertragungen fortzusetzen und identische Daten zu deduplizieren. Der Hash eines Blocks ist seine Identität, sodass der Server sofort erkennt, welche Blöcke er schon hat.

Sollte ich Chunking mit fester Größe oder inhaltsbasiertes Chunking nutzen? Blöcke fester Größe sind einfacher und günstiger zu berechnen. Inhaltsbasiertes Chunking hält Chunk-Grenzen stabil, wenn Bytes in der Mitte einer Datei eingefügt werden, was bei direkt bearbeiteten Dateien Bandbreite spart.

Woher weiß ein Client, was sich auf dem Server geändert hat? Er führt pro Namespace einen Cursor und fragt den Metadaten-Service nach jedem Journal-Eintrag danach. Ein Long Poll oder eine Push-Benachrichtigung sagt ihm, wann er fragen soll.

Wie werden widersprüchliche Änderungen behandelt? Jeder Commit nennt die Version, auf der er basiert. Hat ein anderes Gerät zuerst committet, schlägt der Commit fehl, und der Client behält seine Änderung als Konfliktkopie, statt die andere zu überschreiben.

Funktioniert Deduplizierung über Nutzer hinweg? Das kann sie, aber sie kann verraten, dass ein anderes Konto dieselbe Datei gespeichert hat, und sie funktioniert nicht mit nutzerbezogener Verschlüsselung. Viele Dienste beschränken Deduplizierung auf ein Konto oder einen geteilten Ordner.

Kostenlos starten — ein Seat, keine Kreditkarte.

Ähnliche Beiträge

← Zurück zum Blog

In einer Minute ein Canvas öffnen

Melden Sie sich mit Google oder GitHub an. Kostenlos für einen Sitzplatz. Upgraden Sie, wenn das Team dazukommt.