# Backup Engine Dieses Dokument beschreibt den **aktuell implementierten** Stand nach Phase 4. ## Pipeline ```text Lesen → Chunking → Hash → Deduplizierung → Kompression → Verschlüsselung → Schreiben → Manifest → Commit ``` Die Aufteilung ist dreigeteilt und folgt der Natur der Arbeit: | Stufe | Nebenläufigkeit | Grund | | --- | --- | --- | | Leser + Chunker | **einfach** | Blockgrenzen hängen von den Vorgängerdaten ab | | Arbeiter | **N-fach** | hier liegt die Rechenarbeit (Hash, zstd, AES) | | Sammler | **einfach** | stellt die Reihenfolge über die Positionsnummer wieder her | **Backpressure** entsteht durch begrenzte Warteschlangen (Standardtiefe 4). Ist die Schlange voll, blockiert der Leser. Ohne diese Grenze läse der Chunker so schnell, wie die Quelle liefert, und füllte den Speicher mit unverarbeiteten Blöcken. ## Inhaltsabhängiges Chunking Blockgrenzen ergeben sich aus dem **Inhalt**, nicht aus festen Abständen — über einen Gear-Rolling-Hash mit 20-Bit-Maske (Zielgröße 1 MiB, Grenzen 256 KiB bis 4 MiB). Der Unterschied ist entscheidend: Wird mitten in einer Datei etwas eingefügt, verschiebt eine feste Aufteilung **alle** folgenden Blöcke und macht die Deduplizierung wirkungslos. Inhaltsabhängige Grenzen wandern mit dem Inhalt mit. Die Substitutionstabelle wird deterministisch erzeugt (splitmix64) und muss über alle Installationen identisch sein — sonst fänden zwei Systeme unterschiedliche Grenzen und könnten ihre Backups nicht gegenseitig deduplizieren. Der Rolling Hash dient **nur** der Grenzfindung. Die Chunk-Kennung entsteht weiterhin aus SHA-256. ## Deduplizierung trotz Verschlüsselung Das ist der heikelste Punkt der Engine, und er hat zwei Fallen: **Falle 1: Die Kennung.** Verschlüsselte Daten sind bei jedem Durchgang verschieden. Wäre die Chunk-Kennung der Hash des Geheimtextes, fänden zwei gleiche Ursprungsblöcke nie zusammen. Die Kennung ist deshalb der Hash des **Klartextes**; abgelegt wird die transformierte Form unter dieser Kennung. **Falle 2: Der Schlüssel.** Ein Datenschlüssel **je Backup** macht Deduplizierung über Backupgrenzen hinweg unmöglich — ein späteres Backup verwiese auf Blöcke, die mit einem fremden Schlüssel verschlüsselt und für es unlesbar wären. Der Schlüssel gehört deshalb zum **Repository** und liegt unter `metadata/data-key.json`, verschlüsselt mit dem übergeordneten Schlüssel. *Diese zweite Falle wurde von einem Test aufgedeckt, nicht beim Entwurf erkannt.* **Falle 3: Die Nonce.** Ein Zähler wäre eindeutig, ergäbe aber für denselben Klartext bei jedem Lauf einen anderen Geheimtext — die Deduplizierung liefe wieder ins Leere. Die Nonce wird deshalb deterministisch abgeleitet: ```text nonce = HMAC-SHA256(nonceKey, klartext)[:12] nonceKey = HMAC-SHA256(datenschlüssel, "syncova-chunk-nonce-derivation-v1") ``` Gleicher Klartext → gleiche Nonce → gleicher Geheimtext → deduplizierbar. Eine Nonce wiederholt sich genau dann, wenn auch der Klartext derselbe ist — dann ist der Geheimtext ohnehin identisch. Der bei GCM gefürchtete Fall (gleiche Nonce, **verschiedener** Klartext) tritt nicht ein. **Was das preisgibt:** dass zwei Blöcke gleich sind. Das verrät die inhaltsadressierte Ablage ohnehin — die Chunk-Kennung *ist* der Klartext-Hash. Es entsteht kein zusätzlicher Verlust. ## Ablageformat eines Blocks ```text [Nonce 12 B] [Marker 1 B] [Nutzdaten] [GCM-Schild 16 B] ``` Der Marker unterscheidet komprimiert von unkomprimiert. Ein Block, der sich nicht verkleinern liess, wird unkomprimiert abgelegt — bei Bildern und Archiven kostete Kompression sonst Platz statt zu sparen. **Reihenfolge:** erst komprimieren, dann verschlüsseln. Verschlüsselte Daten sind nicht von Zufall zu unterscheiden und liessen sich nicht mehr verkleinern. ## Integrität auf drei Ebenen | Ebene | Prüft | Braucht Schlüssel? | | --- | --- | --- | | `StoredDigest` im Manifest | abgelegte Form auf dem Datenträger | nein | | GCM-Authentifizierungsschild | Veränderung des Geheimtextes | ja | | Klartext-Hash gegen Kennung | Ergebnis der gesamten Rückgewinnung | ja | Die erste Ebene ist der Grund, warum `ChunkReference.StoredDigest` existiert: Ein Integritätslauf kann verschlüsselte Blöcke prüfen, **ohne** einen Schlüssel zu besitzen. Zusätzlich hält jeder Manifesteintrag den Hash seines Gesamtinhalts. Er deckt eine falsche Blockreihenfolge auf, die den Einzelprüfungen entginge. ## Gemessene Werte Auf einem Apple-Laptop (4 Arbeiter, zstd „balanced", AES-256-GCM), mit **nicht komprimierbaren** Zufallsdaten: | Datensatz | Dauer | Durchsatz | Heap-Spitze | Blöcke | | --- | --- | --- | --- | --- | | 128 MiB | 0,96 s | 134 MiB/s | 63 MiB | 114 | | 512 MiB | 3,40 s | 151 MiB/s | 75 MiB | 429 | | 1024 MiB | 6,42 s | 160 MiB/s | 103 MiB | 813 | | 2048 MiB | 13,51 s | 152 MiB/s | 122 MiB | 1637 | Wiederherstellung: 355 MiB/s, Ergebnis bitgenau identisch zur Quelle. **Zur Einordnung:** Die 16-fache Datenmenge führt zur 1,9-fachen Speicherspitze — deutlich sublinear, aber **nicht konstant**. Der Datensatz wird nachweislich nicht in den Speicher geladen; ein Rest wächst dennoch mit (siehe unten). Ein früherer Messlauf zeigte 1021-fache Kompression. Diese Zahl war wertlos: die Testdaten waren periodisch erzeugt und damit unrealistisch gut komprimierbar. Belastbar sind nur Messungen mit inkompressiblen Daten. ## Sicherungsart je Auftrag Der Standard ist **inkrementell**: Liegt ein Elternbackup vor, wird nur Geändertes gelesen. Zwei Abweichungen lassen sich je Auftrag einstellen. | Einstellung | Wirkung | | --- | --- | | `incremental` | Erster Lauf voll, danach inkrementell. Standard | | `always_full` | Jeder Lauf liest die gesamte Quelle | | `full_backup_weekday` | Zusätzlich an einem festen Wochentag voll | **Der Platzbedarf steigt bei „immer voll" nicht nennenswert.** Unveränderte Blöcke werden dedupliziert und liegen weiterhin nur einmal im Repository. Was steigt, ist die **Laufzeit**: Jeder Lauf liest, hasht, komprimiert und verschlüsselt alles neu. Gemessen (Phase 6): 190,7 MiB in 1 815 ms gegen 4,8 MiB in 133 ms bei einer geänderten von 41 Dateien. Wer das verwechselt, hält „immer voll" für teuer im Speicher und plant seinen Nachtbetrieb falsch. Wann eine Abweichung sinnvoll ist: - **Immer voll**, wenn das Repository außer Haus geht oder auf einen Datenträger geschrieben wird, der einzeln weggetragen wird. - **Wöchentlich voll** als üblicher Kompromiss: unter der Woche schnell, an einem festen Tag einmal vollständig. Der Wochentag wird in der **Zeitzone des Zeitplans** bestimmt. Ohne diese Umrechnung liefe derselbe Auftrag auf zwei Servern an verschiedenen Tagen voll — und ein Betreiber in Berlin bekäme seine Vollsicherung am Donnerstagabend. Ein Widerspruch — „immer voll" **und** ein Wochentag — wird von der Datenbank abgelehnt, nicht stillschweigend aufgelöst. Zur Einordnung: Syncova-Manifeste sind vollständig (Phase 6). Eine Zusatzsicherung trägt die Blockverweise des Elternbackups mit, ein Restore liest genau **ein** Manifest, und das Löschen eines alten Backups kann ein neueres nicht beschädigen. Der Unterschied liegt also in der Laufzeit, nicht in der Wiederherstellbarkeit. ## Grenzen des aktuellen Stands **Das Manifest wird vollständig im Speicher gehalten.** Das ist die wichtigste offene Baustelle. Jeder Blockverweis kostet rund 200 Byte: | Backupgröße | Blöcke | Nur für die Blockverweise | | --- | --- | --- | | 2 GB | 1 637 | 0,3 MiB | | 100 GB | 81 850 | 16 MiB | | 1 TB | 818 500 | 156 MiB | | 10 TB | 8 185 000 | **1,5 GiB** | Bei den in PROMPT.md §79 genannten Größenordnungen ist das nicht tragbar. Nötig wäre ein Manifest, das abschnittsweise auf die Platte geschrieben wird, statt am Stück im Speicher zu entstehen. **Weitere offene Punkte:** - **Kein Changed Block Tracking.** Jedes Backup liest die Quelle vollständig. Die Deduplizierung verhindert erneutes *Schreiben*, nicht erneutes *Lesen* (PROMPT.md §8). - **Keine Wiederaufnahme.** Ein abgebrochener Lauf beginnt von vorn; die bereits abgelegten Blöcke werden zwar wiederverwendet, aber die Quelle wird erneut vollständig gelesen (PROMPT.md §81/§82). - **Kein Bandbreitenlimit** (PROMPT.md §60). - **Kein Retry mit Backoff** innerhalb der Pipeline; ein Fehler bricht den gesamten Lauf ab. - **Inkrementelle Backups sind noch nicht umgesetzt.** Kettenfelder und Elternverweis existieren, aber jedes Backup ist derzeit eine Vollsicherung.