Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
112 lines
6.5 KiB
Markdown
112 lines
6.5 KiB
Markdown
# 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.
|
|
|
|
## 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.
|