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>
28 KiB
Scheduler und Auftragsverwaltung
Status
Die Kette ist geschlossen: Auftrag → Scheduler → Backup Engine → Repository → Wiederherstellung. Der Scheduler sichert selbstständig.
Real nachgewiesen mit 38 MiB in 24 Objekten: Auftrag über die API angelegt, vom Scheduler ausgeführt, Integritätsprüfung sauber (44 Chunks), Quelle gelöscht, aus der Zusatzsicherung wiederhergestellt — bitgenau identisch inklusive Rechten, Symlink und leerem Verzeichnis. Der zweite Lauf las 0,0 MiB in 0,06 s statt 38,1 MiB in 0,57 s.
Es fehlen noch Quellen außer Dateisystemen (siehe „Offene Punkte").
Die Ausführungsschleife
Ein Durchgang (Standardtakt 10 s):
- Verwaiste Läufe freigeben — Selbstheilung nach dem Absturz eines Control-Servers.
- Fällige Aufträge übernehmen und ihre Läufe anlegen.
- Anstehende Läufe übernehmen und ausführen.
- Ergebnis festschreiben, gegebenenfalls Wiederholung einreihen.
Die Executor-Schnittstelle
type Executor interface {
Execute(ctx context.Context, req ExecutionRequest) (ExecutionResult, error)
}
Sie hält die Schleife frei von Backup Engine, Repository und Providern. Das ist nicht nur Ordnung: Ohne sie liesse sich das Zusammenspiel von Zeitplan, Wartungsfenster, Nebenläufigkeit und Wiederholung nur mit einem echten Repository prüfen — also praktisch gar nicht. Alle zwölf Schleifentests laufen gegen einen aufzeichnenden Ersatz.
Umgesetzt ist sie in packages/backupexecutor. Ohne eingerichteten Executor greift NotImplementedExecutor und meldet EXECUTOR_NOT_CONFIGURED — ein Executor, der stillschweigend Erfolg meldete, wäre das gefährlichste Fake-Feature der Anlage: grüne Läufe in der Oberfläche, leeres Repository.
Der Backup-Executor
Ablauf je Lauf: Repository auflösen und öffnen → jede Quelle sichern → Backup in der Control Plane vermerken → Kennzahlen zurückgeben.
Das Repository wird einmal für den ganzen Lauf geöffnet und hält dabei die Schreibsperre. Es je Quelle zu öffnen und zu schließen liefe auf ein Wechselspiel um dieselbe Sperre hinaus.
Verschlüsselung ist keine Option
Ein Executor ohne Schlüsselmaterial wird beim Einrichten abgelehnt, nicht erst beim ersten Lauf um zwei Uhr nachts. Wer unverschlüsselt sichern will, muss AllowUnencrypted setzen — und bekommt bei jedem Start eine Warnung ins Protokoll. Ein Executor, der ohne Schlüssel trotzdem sicherte, legte unverschlüsselte Backups an, ohne dass es jemandem auffiele.
Eine gescheiterte Quelle bricht den Lauf nicht ab
Die übrigen sollen gesichert werden. Verschwiegen wird sie trotzdem nicht: Sie zählt als übergangenes Objekt, und damit ist der Lauf ein Teilfehler — niemals ein Erfolg. Scheitern alle Quellen, ist der Lauf gescheitert (ALL_SOURCES_FAILED).
Real geprüft: Ein Auftrag mit einer guten und einer fehlenden Quelle endet mit partial_failure, 24 gesicherten Objekten und einer übergangenen — und die gute Quelle liegt vollständig im Repository.
Zusatzsicherung ist der Standard
Liegt ein Elternbackup in der Kette, wird inkrementell gesichert. Das ist die richtige Voreinstellung: Der Gewinn ist Lesezeit, und die ist bei jedem Lauf nach dem ersten der begrenzende Faktor.
Je Quelle und Repository gibt es genau eine offene Kette. Zwei Quellen in einer Kette zu führen machte die eigenständige Wiederherstellung einer einzelnen Quelle unmöglich.
Die Backup-Kennung enthält die Laufkennung
run-<lauf-uuid>-<bereinigte-quelle>
Damit lässt sich von einem Backup im Repository aus zurückverfolgen, welcher Lauf es erzeugt hat — auch dann, wenn die Datenbank verloren ging. Der Quellanteil ist nur eine Lesehilfe und wird gekürzt; das Repository begrenzt Kennungen auf 64 Zeichen, weil sie zu Dateinamen werden. Pfadtrenner, führende Punkte und .. werden ersetzt.
Der Datenbankeintrag ist ein Verweis, keine Kopie
RecordBackup hält fest, wo das Manifest liegt, nicht seinen Inhalt. Das Repository bleibt ohne die Datenbank rekonstruierbar (SYNCOVA_ARCHITECTURE.md §10); der Eintrag beschleunigt nur die Suche.
Scheitert er, wird das protokolliert und nicht geworfen: Das Backup liegt bereits vollständig im Repository und ist von dort wiederherstellbar. Den Lauf als gescheitert zu melden, obwohl die Daten sicher sind, führte zu einer sinnlosen Wiederholung.
Ausgetauschtes Repository
Beim ersten Öffnen wird die im Repository hinterlegte Kennung übernommen. Weicht sie später ab, wurde das Verzeichnis ausgetauscht. Der Lauf wird deswegen nicht abgebrochen — das Repository ist in sich stimmig —, aber die Angaben der Control Plane zu Backups und Belegung beziehen sich dann auf einen fremden Bestand. Das gehört ins Protokoll.
Übergangene Objekte sind ein Teilfehler
Die Auswertung eines Laufs folgt einer festen Reihenfolge, und die ist nicht beliebig:
- Abbruch wiegt am schwersten. Wer abbricht, will kein Ergebnis mehr; ein „gescheitert" löste eine sinnlose Wiederholung aus.
- Fehler des Executors — mit seiner Fehlerklasse, die über die Wiederholung entscheidet.
- Übergangene Objekte — auch ohne gemeldeten Fehler wird der Lauf zum Teilfehler.
Punkt 3 ist der Kern. Ein Executor kann fehlerfrei durchlaufen und trotzdem 17 Dateien nicht gelesen haben. Dieser Lauf ist kein Erfolg. Die Begründung kommt mit: „17 Objekte übergangen" ist keine Auskunft, „17 Objekte wegen fehlender Leseberechtigung" schon.
Was nicht wiederholt wird
Ein Teilfehler wird nicht wiederholt. Die übergangenen Objekte wären beim nächsten Versuch mit hoher Wahrscheinlichkeit dieselben, und der Lauf kostete die volle Zeit für dasselbe Ergebnis. Er verlangt einen Blick, keine Wiederholung.
Gescheiterte Läufe werden nach der Wiederholungsstrategie des Auftrags erneut eingereiht — als eigener Lauf mit trigger=retry und erhöhter Versuchsnummer, geplant für den Zeitpunkt nach dem Backoff. Der neue Lauf entsteht erst, nachdem der gescheiterte abgeschlossen wurde; sonst verhinderte ihn der Teilindex, der nur einen aktiven Lauf je Auftrag zulässt.
Versäumte Läufe werden übersprungen, nicht nachgeholt
War der Server drei Tage aus, liegt der geplante Zeitpunkt drei Tage zurück. Der Auftrag läuft einmal, nicht dreimal: Drei Sicherungen desselben Bestands hintereinander kosten Zeit und Platz, ohne einen einzigen zusätzlichen Wiederherstellungspunkt zu schaffen — die Daten von vorgestern gibt es nicht mehr. Die Zahl der übersprungenen Läufe wird protokolliert, nicht verschwiegen.
Kein Drift: Gerechnet wird vom geplanten Zeitpunkt aus, nicht vom tatsächlichen Beginn. Sonst schöbe sich ein Auftrag mit jeder Verspätung weiter nach hinten. Und der nächste Zeitpunkt wird beim Übernehmen fortgeschrieben, nicht nach dem Lauf: Sonst bliebe der Auftrag bei einem langen Backup fällig und würde im nächsten Durchgang erneut übernommen.
Selbstheilung nach einem Absturz
Das ist der wichtigste Mechanismus der ganzen Ausführung.
Stirbt ein Control-Server mitten im Lauf, bleibt dessen Zeile auf running stehen. Der Teilindex lässt dann keinen weiteren Lauf dieses Auftrags zu — die Sicherung fiele dauerhaft aus, ohne dass jemand einen Fehler sähe. Es gäbe nur einen Auftrag, der nie wieder läuft.
Deshalb meldet jeder laufende Vorgang sich alle 30 Sekunden lebendig. Bleibt die Meldung fünf Minuten aus, gilt der Lauf als verwaist und wird freigegeben — als gescheitert vermerkt, nicht gelöscht: Der Abbruch ist ein Ereignis, das ins Protokoll gehört. Die Fehlerklasse transient sorgt dafür, dass die Wiederholungsstrategie greift.
Die Frist muss deutlich über dem Meldeabstand liegen. Andernfalls gäbe ein kurzer Aussetzer — eine langsame Datenbank, ein überlasteter Server — einen noch laufenden Lauf frei, und derselbe Auftrag liefe zweimal. Die Schleife lehnt eine Frist unterhalb des Meldeabstands beim Start ab; der Fehler wäre im Betrieb kaum zu finden.
Geordnetes Beenden
Bei SIGTERM werden die laufenden Vorgänge abgebrochen, nicht zu Ende geführt. Sie laufen zu lassen hiesse, das Herunterfahren an das längste Backup zu hängen — bei einem mehrstündigen Lauf ein Neustart, der nie endet.
Das Ergebnis wird trotzdem geschrieben, mit eigenem Kontext: Sonst bliebe die Zeile auf running stehen und blockierte den Auftrag bis zum Ablauf der Frist. Verstreicht die Frist von 30 Sekunden, während noch Läufe aktiv sind, wird das als Warnung gemeldet — sie werden dann von der Freigabe verwaister Läufe eingesammelt. Das ist der ehrliche Ausgang: Wir wissen nicht, ob sie noch etwas schreiben.
Der Backup-Wizard
Zehn Schritte von Name bis Anlegen (apps/web/src/features/jobs). Der Aufbau folgt drei Entscheidungen:
Die Logik liegt getrennt von der Darstellung (wizardModel.ts). Welcher Schritt vollständig ist und was in die Anfrage wandert, ist reine Berechnung — und damit ohne gerenderte Maske prüfbar. 23 der 34 Frontend-Tests laufen gegen das Modell.
Der Entwurf lebt in einem Zustand, nicht in den Eingabefeldern. Sonst wäre jeder Blick zurück ein Datenverlust. Vorwärts geht es nur über einen vollständigen Schritt; ein noch nicht erreichter Schritt ist in der Leiste nicht anklickbar — er würde eine Prüfung überspringen, die der Assistent gerade führen soll.
Fehler erscheinen erst beim Versuch weiterzugehen. Sie von Anfang an zu zeigen hieße, ein leeres Formular als fehlerhaft zu markieren.
Drei Schritte fragen nichts ab
Aufbewahrung, Prüfung und Benachrichtigung haben keine Umsetzung im Backend. Sie erscheinen trotzdem — der Plan nennt zehn Schritte —, aber ohne Eingabefelder. Eine Maske, die Werte sammelt, die niemand auswertet, ist ein vorgetäuschtes Funktionsversprechen (PROMPT.md §138).
Stattdessen steht dort, was ohne diese Einstellung tatsächlich geschieht:
Noch nicht verfügbar — Aufbewahrungsregeln sind noch nicht umgesetzt. Backups bleiben bis auf Weiteres vollständig erhalten und werden nicht automatisch gelöscht — der Speicherbedarf wächst also mit jedem Lauf.
Die Übersicht in Schritt 9 wiederholt das: „Aufbewahrung: unbegrenzt (Regeln noch nicht umgesetzt)". Eine leere Zeile ließe offen, ob nichts eingestellt oder nichts möglich ist.
Ebenso im Quellschritt: Proxmox-, Windows- und Linux-Quellen stehen in der Auswahl, sind aber deaktiviert und als „noch nicht verfügbar" gekennzeichnet — statt sie wegzulassen und den Anwender rätseln zu lassen, ob sie fehlen oder nur versteckt sind.
Was der Wizard nicht entscheidet
Verschlüsselung ist keine Wahl. Der Executor verweigert den Dienst ohne Schlüsselmaterial; eine Schaltfläche zum Abschalten wäre eine Einstellung, die es nicht gibt. Der Schritt „Sicherheit" sagt das und fragt stattdessen nach Dringlichkeit und Bandbreitengrenze.
Ob ein Repository Sicherungen annimmt, entscheidet der Server. Die API liefert accepts_backups; die Oberfläche müsste sonst wissen, welche Zustände schreibend sind. Gesperrte Ziele erscheinen in der Liste, sind aber nicht wählbar und tragen ihren Grund — sie wegzulassen ließe den Anwender ein Repository suchen, das er nicht findet.
Die Zeitzone kommt aus dem Browser
Voreingestellt ist Intl.DateTimeFormat().resolvedOptions().timeZone. Ohne Angabe rechnet der Server in UTC — derselbe Auftrag liefe dann je nach Standort zu einer anderen Uhrzeit. Bei einem Intervallplan wird die Zeitzone weggelassen: Er zählt Abstände, keine Uhrzeiten.
Der Repository-Endpunkt
GET /api/v1/repositories (Recht repositories.read), rein lesend und ohne Pagination — ein Betrieb hat eine Handvoll Ziele, nicht tausende. Das Anlegen bleibt bei syncova-repo create: Ein Repository entsteht auf einem Datenträger, nicht in einer Datenbankzeile.
Nachgewiesen
Genau die Anfrage, die der Wizard baut, gegen den laufenden Dienst: Auftrag angelegt (täglich um 02:00 Uhr (Europe/Berlin), nächster Lauf berechnet), ausgeführt (4,8 MiB, 5 Objekte, 0,16 s), wiederhergestellt — und die im Wizard eingetragene Ausschlussregel *.tmp hat gewirkt: Die Datei fehlt im Backup.
Die Zeitumstellung
Das ist der Punkt, an dem die meisten Scheduler stillschweigend danebenliegen, und er kostet genau einen Backup-Lauf im Jahr — den niemand vermisst, bis er gebraucht wird.
Frühjahr, die Uhr springt von 02:00 auf 03:00. Ein Auftrag „täglich um 02:30" hat an diesem Tag keinen Zeitpunkt. Wer den Tag überspringt, lässt die Sicherung ausfallen. Syncova nimmt den nächsten gültigen Zeitpunkt (03:30) — der Lauf findet statt.
Herbst, 02:00 bis 03:00 wiederholt sich. „Täglich um 02:30" gäbe es zweimal. Es läuft nur einmal, und zwar beim ersten Vorkommen: Ein Backup lieber eine Stunde früher als zwei Backups, die zwei Ketten anlegen.
Beides ist mit Europe/Berlin gegen die echten Umstellungstermine 2026 geprüft (TestDailyScheduleSurvivesSpringForward, TestDailyScheduleRunsOnceOnFallBack).
Daraus folgt auch die Suchweise: Kalenderzeitpläne werden tageweise mit AddDate gesucht, nicht mit Add(24h). An Umstellungstagen hat ein Tag 23 oder 25 Stunden; mit einer festen Stundenzahl verschöbe sich die Suche zweimal im Jahr.
Ohne Zeitzone gilt UTC, nicht die Ortszeit des Servers. Andernfalls liefe dieselbe Konfiguration auf zwei Servern zu verschiedenen Zeiten — der häufigste Grund für ein Backup, das „mal um zwei und mal um vier" läuft.
Zeitplanarten
| Art | Beispiel |
|---|---|
manual |
nur auf Anforderung |
interval |
alle 6 Stunden (mindestens 1 Minute) |
hourly |
stündlich zur Minute 30 |
daily |
täglich um 02:00 |
weekly |
samstags und sonntags um 22:00 |
monthly |
am 1. und am letzten Tag des Monats |
cron |
*/15 9-17 * * mon-fri |
Der Monatsletzte (-1) ist kein Luxus. Wer „am 31." einträgt, bekommt in vier Monaten des Jahres keine Sicherung. Wer den Monatsabschluss sichern will, meint den letzten Tag.
Der eigene Cron-Parser
Cron ist klein; die Fallstricke liegen nicht im Zerlegen, sondern in Zeitzonen und einer Sonderregel, die ohnehin selbst gelöst werden muss:
Sind Tag-des-Monats und Wochentag eingeschränkt, gilt ODER statt UND. 0 0 13 * 5 bedeutet „am 13. oder freitags", nicht „an Freitagen, die der 13. sind". Wer das als UND umsetzt, baut einen Zeitplan, der fast nie läuft — und es fällt erst nach Monaten auf.
Sekunden gibt es bewusst nicht: Ein Sicherungsauftrag im Sekundentakt ist kein Zeitplan, sondern ein Tippfehler. Sonntag darf 0 oder 7 heißen, Namen (mon, jan) sind zugelassen, @daily und Verwandte auch.
Wartungsfenster
Zwei Wirkungen: blackout unterdrückt Läufe im Fenster, allowed lässt sie nur im Fenster zu.
Ein durch ein Fenster verhinderter Lauf wird verschoben, nicht übergangen. Ihn ausfallen zu lassen erzeugte eine Lücke in der Sicherungskette, von der niemand erführe: Dass ein Lauf verspätet ist, sieht man; dass er fehlt, nicht.
Zwei Feinheiten:
- Ein wiederkehrendes Fenster hat eine Länge, keine Endzeit. Sonst bräuchte „22:00 bis 02:00" eine Sonderregel, die gern vergessen wird. Die Auswertung schaut acht Tage zurück: Ein Fenster, das samstags um 22:00 beginnt und zwölf Stunden dauert, reicht bis Sonntag 10:00 — wer nur den Tag des Zeitpunkts prüft, hielte Sonntag 09:00 für frei.
- Ein Erlaubnisfenster für Auftrag A sperrt Auftrag B nicht. Ohne diese Einschränkung fielen sämtliche Sicherungen aus, sobald irgendwo ein Erlaubnisfenster existiert.
Findet sich binnen eines Monats kein freier Zeitpunkt, ist das ein Fehler und kein stiller Ausfall — fast immer eine Fehlkonfiguration.
Warteschlange
Verhungerungsschutz
Ein wartender Auftrag gewinnt 50 Punkte je Stunde, gedeckelt bei 100. Ohne Alterung verhungert ein Auftrag niedriger Priorität in einer Anlage mit genug dringenden Aufträgen für immer — und niemand bemerkt es, weil er technisch „wartet" statt zu scheitern. Der Deckel entspricht dem Abstand zweier Stufen: Ein wartender Auftrag steigt höchstens um eine Stufe auf und überholt damit nie eine kritische Sicherung.
Eine unbekannte Prioritätsstufe gilt als normal, nicht als höchste: Ein Tippfehler soll keinen Auftrag an die Spitze befördern.
Nebenläufigkeitsgrenzen
Global, je Repository und je Quelle. Die letzten beiden sind keine Feinheit:
- Je Repository: Das Repository hält beim Schreiben eine Sperre. Mehrere gleichzeitige Läufe warten ohnehin aufeinander und belegen dabei nur Arbeitsspeicher.
- Je Quelle: Zwei gleichzeitige Sicherungen derselben VM lasten den Wirt aus, ohne zusätzlichen Nutzen.
Jeder zurückgehaltene Auftrag trägt eine Begründung. Ein wartender Auftrag ohne Begründung ist für den Betrieb wertlos: Man sieht, dass nichts geschieht, aber nicht warum.
Abhängigkeiten
Eine Abhängigkeit ist erfüllt, wenn der Vorgänger zuletzt vollständig gelungen ist. Ein Teilfehler genügt nicht: Wer eine Datenbank sichert und danach das Anwendungsverzeichnis, will nicht das Verzeichnis zu einer halben Datenbank.
Ringschlüsse werden beim Anlegen erkannt, nicht beim Ausführen. Ein Ring bliebe sonst unbemerkt: Jeder Auftrag wartete auf einen anderen, keiner liefe je an, und die Oberfläche zeigte lauter „wartende" Aufträge ohne erkennbaren Grund.
Wiederholungen
Wiederholt wird nur bei transient, network, repository und source. Ein Anmeldefehler behebt sich nicht durch Warten; ein Integritätsfehler wird durch Wiederholen nicht besser, sondern nur später bemerkt; ein sicherheitsrelevanter Fehler gehört gemeldet, nicht verschluckt.
Voreinstellung: drei Versuche, 30 s → 60 s → 120 s, gedeckelt bei 15 Minuten. Der Deckel ist wesentlich: Ohne ihn wären es beim achten Versuch über eine Stunde und beim zehnten mehr als vier — ein nächtliches Sicherungsfenster wäre längst vorbei. Mehr als zehn Versuche sind nicht zugelassen; sie verschleiern ein dauerhaftes Problem, statt es zu melden.
Die Streuung wirkt nur nach unten. Nach oben verlängerte sie die Wartezeit über die vereinbarte Grenze — der Deckel wäre dann keiner. Ihr Zweck ist der Gleichlauf: Fallen zwanzig Aufträge derselben Störung zum Opfer, liefen sie ohne Streuung alle gleichzeitig wieder an und überlasteten die eben erholte Gegenstelle sofort erneut.
Gerechnet wird in Gleitkomma statt mit Schiebeoperationen: Ein Schieben um mehr als 62 Stellen liefe über und machte aus einer langen Wartezeit eine negative.
Eine unbekannte Fehlerart gilt als dauerhaft, also „nicht wiederholen". Der umgekehrte Standard wäre gefährlicher: Ein falsch als vorübergehend eingeordneter Fehler liesse einen aussichtslosen Auftrag immer wieder anlaufen und verdeckte dabei die Ursache.
Bandbreitengrenze
Token-Bucket, kein festes Zeitfenster: Ein Fenster erlaubt an seiner Grenze die doppelte Rate — 10 MB am Ende des einen und 10 MB am Anfang des nächsten ergeben 20 MB in kurzer Folge.
Was begrenzt wird
Das Lesen von der Quelle, nicht das Schreiben ins Repository. Weil die Pipeline mit Gegendruck arbeitet, bremst das den gesamten Ablauf: Was nicht gelesen wird, wird auch nicht gehasht, komprimiert, verschlüsselt und abgelegt. Ein Begrenzer an dieser einen Stelle bindet damit die gesamte Last.
Das ist auch die Größe, die ein Betreiber meint: „Lies höchstens 50 MB/s vom Dateiserver." Die Last auf dem produktiven System entsteht beim Lesen.
Nicht begrenzt wird der Schreibweg zum Repository. Bei einem lokalen Repository fällt das zusammen; bei einem entfernten (NFS, später Objektspeicher) wäre es eine eigene Grenze — und wegen der Deduplizierung eine ganz andere Zahl: Von 19 MiB gelesener Daten gehen bei einem zweiten Lauf 0 MiB über die Leitung.
Ein Begrenzer je Lauf
Er gilt für den gesamten Lauf und wird über alle Quellen und alle Arbeiter geteilt. Je Quelle oder je Arbeiter einen eigenen zu führen ergäbe ein Vielfaches der vereinbarten Rate — genau der Fehler, der eine Bandbreitengrenze wirkungslos macht. Ein Test prüft das mit drei Quellen: Sie brauchen zusammen die Zeit, die eine einzelne Quelle dreifacher Größe bräuchte.
Beim Lesen wird nach der Übertragung gewartet (am Dateiende zahlte man sonst für Bytes, die es nicht mehr gibt), beim Schreiben davor (die Menge steht fest, und die Leitung wäre sonst schon belegt).
Gemessen
19,1 MiB in einem Repository, derselbe Bestand zweimal über den Scheduler:
| Dauer | Durchsatz | |
|---|---|---|
| ohne Grenze | 0,37 s | 51,7 MiB/s |
bandwidth_limit_bps: 1048576 |
18,17 s | 1,05 MiB/s |
Der Eimer liefert eine Sekunde Vorrat, deshalb liegt der gemessene Durchschnitt bei kurzen Läufen etwas über der Grenze — bei 2 MiB gegen 1 MiB/s sind es 1,79 MiB/s. Über längere Läufe verschwindet der Effekt, wie die Messung oben zeigt.
Bit und Byte
100Mbit ist ein Achtel von 100MB. Netzwerkleute rechnen in Bit, Speicherleute in Byte; wer das verwechselt, vergibt das Achtfache. Beide Schreibweisen werden angenommen, ebenso 50MB/s, 1,5MB und 1.5MB.
syncova-agent backup --repository /backup/repo --path /daten --bandwidth 50MB
syncova-agent backup --repository /backup/repo --path /daten --bandwidth 400Mbit
Im Auftrag: "bandwidth_limit_bps": 52428800. Die Angabe wird vor dem Öffnen des Repositorys geprüft — ein Tippfehler soll nicht erst nach dem Sperren auffallen.
Datenbank
Migration 000004_jobs. Zehn Tabellen; drei Entscheidungen lohnen die Erwähnung:
Ein Lauf ist eine eigene Zeile, kein Zustandsfeld am Auftrag. Ohne Historie ist nicht feststellbar, ob eine Sicherung regelmäßig gelingt.
Ein Teilindex erzwingt einen aktiven Lauf je Auftrag:
CREATE UNIQUE INDEX backup_job_runs_single_active_idx
ON backup_job_runs (job_id) WHERE status IN ('queued','running');
Zwei Control-Server, die gleichzeitig denselben fälligen Auftrag sehen, prüfen beide erfolgreich und starten beide — die Datenbank lässt nur einen durch. Der Übernahmemechanismus nutzt zusätzlich FOR UPDATE SKIP LOCKED: Der erste Server sperrt die Zeilen, der zweite überspringt sie, statt zu warten. Geprüft mit vier gleichzeitigen Servern auf zehn Aufträge (TestClaimDueJobsNeverHandsOutSameJobTwice).
Regeln stehen als CHECK in der Datenbank, nicht nur im Code:
CONSTRAINT backup_job_runs_skipped_is_not_success CHECK (
files_skipped = 0 OR status <> 'succeeded'
)
Ein Lauf mit übergangenen Objekten kann nicht als Erfolg dastehen. Im Code müsste jede Stelle die Regel einhalten — und eine davon vergisst es. Ebenso abgesichert: Zusatzsicherung ohne Elternbackup, Selbstabhängigkeit, Bandbreitengrenze 0, Wartungsfenster mit einmaliger und wiederkehrender Angabe. Alle acht Regeln sind gegen die echte Datenbank geprüft.
Aufträge werden weich gelöscht. Ein hart gelöschter Auftrag risse seine Läufe mit — und damit den Nachweis, dass gesichert wurde.
API
GET /api/v1/jobs jobs.read Filter: status, repository, search; Pagination
POST /api/v1/jobs jobs.write
GET /api/v1/jobs/{id} jobs.read
DELETE /api/v1/jobs/{id} jobs.write weich, immer auditiert
POST /api/v1/jobs/{id}/pause jobs.run auditiert
POST /api/v1/jobs/{id}/resume jobs.run auditiert, berechnet next_run_at neu
POST /api/v1/jobs/{id}/run jobs.run 202: reiht einen Lauf ein, auditiert
GET /api/v1/jobs/{id}/runs jobs.read Historie mit Pagination
POST /api/v1/backup-runs/{id}/cancel jobs.run bricht einen Lauf ab, auditiert
Beispiel:
{
"name": "Nächtliche Sicherung",
"priority": "high",
"schedule": {"type": "daily", "time": "02:00", "time_zone": "Europe/Berlin"},
"sources": [
{"type": "filesystem", "id": "/daten", "exclude_patterns": ["*.tmp"]},
{"type": "proxmox_vm", "id": "qemu/100"}
],
"repository_id": "…",
"bandwidth_limit_bps": 52428800
}
Die Antwort enthält schedule_description („täglich um 02:00 Uhr (Europe/Berlin)"). Ein Cron-Ausdruck sagt einem Anwender wenig; die Erklärung entsteht auf dem Server, damit sie in Oberfläche und Benachrichtigung gleich lautet.
Der nächste Zeitpunkt wird beim Anlegen sofort berechnet. Ohne ihn stünde der Auftrag als aktiv da, ohne je zu laufen. Beim Fortsetzen wird er neu berechnet: Ein Auftrag, der eine Woche ausgesetzt war, liefe sonst sofort los und danach zur falschen Zeit weiter.
Das Aussetzen wird auditiert wie eine Löschung. Es lässt den Schutz still auslaufen, ohne dass etwas kaputtgeht — genau deshalb gehört es ins Protokoll.
Widersprüchliche Schutzziele werden abgelehnt
VALIDATION_FAILED: der zeitplan läuft nur alle 1 Tage und kann den geforderten
wiederherstellungspunkt von 4 Stunden nicht einhalten
Ein RPO, den der Zeitplan nicht einhalten kann, ist keine Kleinigkeit: Der Betreiber glaubt, höchstens vier Stunden zu verlieren, während der Auftrag nur täglich läuft. Das gehört beim Anlegen gesagt, nicht nach dem Ausfall.
Rettung nach abgebrochener Migration
Bricht eine Migration mitten in der Datei ab, merkt golang-migrate die Datenbank als „dirty" vor und verweigert jeden weiteren Schritt. Bisher gab es dafür keinen Weg zurück außer von Hand in der Datenbank — was ein Migrationswerkzeug gerade ersparen soll.
syncova-migrate status # zeigt den abgebrochenen Zustand
# Schema von Hand auf einen bekannten Stand bringen
SYNCOVA_MIGRATE_CONFIRM_FORCE=yes syncova-migrate force 3
force führt kein SQL aus. Es behauptet nur einen Stand. Wer es falsch anwendet, lässt die Anwendung gegen ein Schema arbeiten, das sie für ein anderes hält. Deshalb: ausdrückliche Bestätigung über die Umgebungsvariable, Ausgabe des Vorher/Nachher, und eine Ablehnung auf sauberer Datenbank — dort gibt es nichts zu retten, und die Folge wäre ein übersprungenes Schema.
Warum 202 und nicht 201
POST /jobs/{id}/run reiht den Lauf ein; die Schleife holt ihn im nächsten Durchgang. Die Sicherung hat also noch nicht begonnen — 201 („angelegt") behauptete mehr, als geschehen ist. Ebenso beim Abbruch: Er wird vermerkt, ein bereits laufender Vorgang endet erst beim nächsten Durchgang. Auch das wird gesagt, statt einen beendeten Lauf vorzutäuschen.
Ein ausgesetzter Auftrag wird nicht heimlich reaktiviert: Wer ihn ausführen will, setzt ihn zuerst fort. Sonst liefe er einmal und schwiege danach wieder, ohne dass es jemandem auffiele.
Ein zweiter Anstoß bei laufendem Auftrag ergibt 409, keinen Serverfehler: Der Aufrufer hat nichts falsch gemacht, der Auftrag läuft nur bereits.
Die Laufhistorie weist delay_seconds aus — die Abweichung zwischen geplantem und tatsächlichem Beginn. Ein Lauf, der regelmäßig eine Stunde zu spät beginnt, hat ein Problem, das man ohne diesen Vergleich nicht sieht.
Offene Punkte
- Aufbewahrung, Prüfung und Benachrichtigung — im Wizard als „noch nicht verfügbar" gekennzeichnet, weil dahinter nichts liegt.
- Nur Dateisystemquellen.
proxmox_vm,windows_systemundlinux_systemwerden abgelehnt, nicht stillschweigend übergangen — eine übergangene Quelle ergäbe ein Backup, das vollständig aussieht und es nicht ist. Für Proxmox fehlt derArchiveTransport(sieheproxmox.md), für die Systemquellen die Auftragsübermittlung an den Agent. - Der Schreibweg zum Repository ist nicht begrenzt. Bei einem lokalen Repository fällt das mit dem Lesen zusammen; für ein entferntes Ziel braucht es eine eigene Grenze.
- Die Wiederherstellung ist nicht begrenzt. Ein Restore kann die Leitung voll belegen.
logical_byteseiner Zusatzsicherung ist die gelesene, nicht die gesicherte Menge. Bei einem Lauf ohne Änderungen steht dort 0, obwohl das Backup den vollen Bestand beschreibt. Für die Belegungsrechnung ist das irreführend und gehört korrigiert.- Keine Wiederherstellung über die API. Sie geht nur über
syncova-agent restore. PATCH /jobs/{id}— Ändern ist noch nicht umgesetzt; ein Auftrag muss gelöscht und neu angelegt werden.- Wartungsfenster nur im Code, ohne API und ohne Anbindung an die Auftragsübernahme.
- Kein
Idempotency-KeybeiPOST /jobs(SYNCOVA_API.md verlangt ihn). - Retention-Regeln sind als Tabelle angelegt, aber ohne Auswertung.