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>
229 lines
8.6 KiB
Go
229 lines
8.6 KiB
Go
package backupengine
|
|
|
|
import (
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// Progress beschreibt den Stand eines laufenden Backups (PROMPT.md §34).
|
|
type Progress struct {
|
|
// BytesProcessed ist die Menge der gelesenen Ursprungsdaten.
|
|
BytesProcessed int64 `json:"bytes_processed"`
|
|
// BytesWritten ist die Menge der tatsächlich abgelegten Daten.
|
|
BytesWritten int64 `json:"bytes_written"`
|
|
// BytesDeduplicated ist die durch Deduplizierung eingesparte Menge.
|
|
BytesDeduplicated int64 `json:"bytes_deduplicated"`
|
|
// ChunksWritten ist die Zahl neu geschriebener Blöcke.
|
|
ChunksWritten int64 `json:"chunks_written"`
|
|
// ChunksDeduplicated ist die Zahl bereits vorhandener Blöcke.
|
|
ChunksDeduplicated int64 `json:"chunks_deduplicated"`
|
|
// BytesReused ist die Datenmenge, die dank Zusatzsicherung gar nicht
|
|
// gelesen werden musste.
|
|
//
|
|
// Sie steht bewusst neben BytesDeduplicated und nicht darin: ein
|
|
// deduplizierter Block wurde gelesen und gehasht, ein übernommener nicht
|
|
// einmal geöffnet. Die beiden Zahlen zu vermengen verschleierte genau den
|
|
// Gewinn, um dessentwillen es Zusatzsicherungen gibt.
|
|
BytesReused int64 `json:"bytes_reused"`
|
|
// ChunksReused ist die Zahl übernommener Blöcke.
|
|
ChunksReused int64 `json:"chunks_reused"`
|
|
// ChunksIncompressible ist die Zahl neu geschriebener, nicht verkleinerbarer Blöcke.
|
|
//
|
|
// Der Entropie-Indikator (Phase 16). Gezählt werden nur **neue** Blöcke:
|
|
// Ein deduplizierter stammt aus einem früheren Lauf und sagt nichts über
|
|
// die Daten von heute.
|
|
ChunksIncompressible int64 `json:"chunks_incompressible"`
|
|
// ElapsedSeconds ist die bisher verstrichene Zeit.
|
|
ElapsedSeconds float64 `json:"elapsed_seconds"`
|
|
// ThroughputBytesPerSecond ist der aktuelle Durchsatz auf die Ursprungsdaten bezogen.
|
|
ThroughputBytesPerSecond float64 `json:"throughput_bytes_per_second"`
|
|
}
|
|
|
|
// CompressionRatio liefert das Verhältnis von Ursprungs- zu abgelegten Daten.
|
|
//
|
|
// Der zweite Rückgabewert meldet, ob ein endliches Verhältnis existiert. Wurde
|
|
// nichts abgelegt, weil jeder Block bereits vorlag, gibt es keines — ein
|
|
// stillschweigendes 0 stellte den besten Fall als den schlechtesten dar.
|
|
func (progress Progress) CompressionRatio() (ratio float64, isDefined bool) {
|
|
if progress.BytesProcessed == 0 || progress.BytesWritten == 0 {
|
|
return 0, false
|
|
}
|
|
|
|
return float64(progress.BytesProcessed) / float64(progress.BytesWritten), true
|
|
}
|
|
|
|
// SavingsPercentage liefert den Anteil eingesparter Daten in Prozent.
|
|
//
|
|
// Die Kennzahl ist immer bestimmbar und deshalb für Anzeigen die verlässlichere.
|
|
func (progress Progress) SavingsPercentage() float64 {
|
|
if progress.BytesProcessed == 0 {
|
|
return 0
|
|
}
|
|
|
|
savedBytes := progress.BytesProcessed - progress.BytesWritten
|
|
if savedBytes < 0 {
|
|
// Bei sehr kleinen oder bereits komprimierten Daten kann die abgelegte
|
|
// Menge die Ursprungsmenge übersteigen. Eine negative Ersparnis
|
|
// auszuweisen wäre verwirrend; 0 ist die ehrliche Aussage.
|
|
return 0
|
|
}
|
|
|
|
return float64(savedBytes) / float64(progress.BytesProcessed) * 100
|
|
}
|
|
|
|
// ProgressCallback wird während eines Backups mit dem Fortschritt aufgerufen.
|
|
//
|
|
// Die Funktion muss schnell zurückkehren: sie läuft im Takt der Verarbeitung
|
|
// und würde die Pipeline sonst ausbremsen.
|
|
type ProgressCallback func(progress Progress)
|
|
|
|
// ProgressReporter sammelt die Kennzahlen eines laufenden Backups.
|
|
//
|
|
// Die Zähler sind atomar, weil mehrere Arbeiter gleichzeitig melden.
|
|
type ProgressReporter struct {
|
|
// bytesProcessed zählt die gelesenen Ursprungsdaten.
|
|
bytesProcessed atomic.Int64
|
|
// bytesWritten zählt die abgelegten Daten.
|
|
bytesWritten atomic.Int64
|
|
// bytesDeduplicated zählt die eingesparten Daten.
|
|
bytesDeduplicated atomic.Int64
|
|
// chunksWritten zählt die neu geschriebenen Blöcke.
|
|
chunksWritten atomic.Int64
|
|
// chunksDeduplicated zählt die bereits vorhandenen Blöcke.
|
|
chunksDeduplicated atomic.Int64
|
|
// bytesReused zählt die ungelesen übernommenen Bytes.
|
|
bytesReused atomic.Int64
|
|
// chunksReused zählt die ungelesen übernommenen Blöcke.
|
|
chunksReused atomic.Int64
|
|
// chunksIncompressible zählt die neu geschriebenen, nicht verkleinerbaren Blöcke.
|
|
//
|
|
// Der Entropie-Indikator der Anlage (Phase 16): Normale Nutzdaten lassen
|
|
// sich fast immer verkleinern, verschlüsselte nie. Ein Sprung in diesem
|
|
// Anteil ist das billigste verlässliche Signal für massenhafte
|
|
// Verschlüsselung.
|
|
chunksIncompressible atomic.Int64
|
|
// startTime ist der Beginn des Backups.
|
|
startTime time.Time
|
|
// callback meldet den Fortschritt nach außen.
|
|
callback ProgressCallback
|
|
// callbackInterval ist der Mindestabstand zweier Meldungen.
|
|
callbackInterval time.Duration
|
|
// callbackMutex schützt den Zeitpunkt der letzten Meldung.
|
|
callbackMutex sync.Mutex
|
|
// lastCallbackTime ist der Zeitpunkt der letzten Meldung.
|
|
lastCallbackTime time.Time
|
|
}
|
|
|
|
// defaultProgressInterval ist der Mindestabstand zweier Fortschrittsmeldungen.
|
|
//
|
|
// Ohne Drosselung entstünde bei kleinen Blöcken eine Flut von Aufrufen, die
|
|
// mehr Zeit kostete als die eigentliche Arbeit.
|
|
const defaultProgressInterval = 500 * time.Millisecond
|
|
|
|
// NewProgressReporter erzeugt einen Fortschrittszähler.
|
|
func NewProgressReporter(callback ProgressCallback, callbackInterval time.Duration) *ProgressReporter {
|
|
if callbackInterval <= 0 {
|
|
callbackInterval = defaultProgressInterval
|
|
}
|
|
|
|
return &ProgressReporter{
|
|
startTime: time.Now(),
|
|
callback: callback,
|
|
callbackInterval: callbackInterval,
|
|
lastCallbackTime: time.Now(),
|
|
}
|
|
}
|
|
|
|
// recordWrittenChunk vermerkt einen neu geschriebenen Block.
|
|
func (progressReporter *ProgressReporter) recordWrittenChunk(logicalBytes int64, storedBytes int64) {
|
|
progressReporter.bytesProcessed.Add(logicalBytes)
|
|
progressReporter.bytesWritten.Add(storedBytes)
|
|
progressReporter.chunksWritten.Add(1)
|
|
|
|
progressReporter.maybeReport()
|
|
}
|
|
|
|
// recordIncompressibleChunk vermerkt einen nicht verkleinerbaren Block.
|
|
func (progressReporter *ProgressReporter) recordIncompressibleChunk() {
|
|
progressReporter.chunksIncompressible.Add(1)
|
|
}
|
|
|
|
// recordDeduplicatedChunk vermerkt einen bereits vorhandenen Block.
|
|
func (progressReporter *ProgressReporter) recordDeduplicatedChunk(logicalBytes int64) {
|
|
progressReporter.bytesProcessed.Add(logicalBytes)
|
|
progressReporter.bytesDeduplicated.Add(logicalBytes)
|
|
progressReporter.chunksDeduplicated.Add(1)
|
|
|
|
progressReporter.maybeReport()
|
|
}
|
|
|
|
// recordReusedChunk vermerkt einen aus dem Elternbackup übernommenen Block.
|
|
//
|
|
// Er zählt nicht zu den verarbeiteten Bytes: gelesen wurde er nie. Stünde er
|
|
// dort, gäbe der Durchsatz eine Leseleistung aus, die es nicht gab.
|
|
func (progressReporter *ProgressReporter) recordReusedChunk(logicalBytes int64) {
|
|
progressReporter.bytesReused.Add(logicalBytes)
|
|
progressReporter.chunksReused.Add(1)
|
|
|
|
progressReporter.maybeReport()
|
|
}
|
|
|
|
// maybeReport meldet den Fortschritt, sofern der Mindestabstand verstrichen ist.
|
|
func (progressReporter *ProgressReporter) maybeReport() {
|
|
if progressReporter.callback == nil {
|
|
return
|
|
}
|
|
|
|
progressReporter.callbackMutex.Lock()
|
|
|
|
if time.Since(progressReporter.lastCallbackTime) < progressReporter.callbackInterval {
|
|
progressReporter.callbackMutex.Unlock()
|
|
return
|
|
}
|
|
|
|
progressReporter.lastCallbackTime = time.Now()
|
|
progressReporter.callbackMutex.Unlock()
|
|
|
|
// Die Meldung erfolgt außerhalb der Sperre: eine langsame Rückrufmethode
|
|
// soll die übrigen Arbeiter nicht blockieren.
|
|
progressReporter.callback(progressReporter.Snapshot())
|
|
}
|
|
|
|
// Snapshot liefert den aktuellen Stand.
|
|
func (progressReporter *ProgressReporter) Snapshot() Progress {
|
|
elapsedDuration := time.Since(progressReporter.startTime)
|
|
elapsedSeconds := elapsedDuration.Seconds()
|
|
|
|
bytesProcessed := progressReporter.bytesProcessed.Load()
|
|
|
|
var throughput float64
|
|
if elapsedSeconds > 0 {
|
|
throughput = float64(bytesProcessed) / elapsedSeconds
|
|
}
|
|
|
|
return Progress{
|
|
BytesProcessed: bytesProcessed,
|
|
BytesWritten: progressReporter.bytesWritten.Load(),
|
|
BytesDeduplicated: progressReporter.bytesDeduplicated.Load(),
|
|
ChunksWritten: progressReporter.chunksWritten.Load(),
|
|
ChunksDeduplicated: progressReporter.chunksDeduplicated.Load(),
|
|
BytesReused: progressReporter.bytesReused.Load(),
|
|
ChunksReused: progressReporter.chunksReused.Load(),
|
|
ChunksIncompressible: progressReporter.chunksIncompressible.Load(),
|
|
ElapsedSeconds: elapsedSeconds,
|
|
ThroughputBytesPerSecond: throughput,
|
|
}
|
|
}
|
|
|
|
// ReportFinal meldet den Endstand unabhängig vom Mindestabstand.
|
|
func (progressReporter *ProgressReporter) ReportFinal() Progress {
|
|
finalProgress := progressReporter.Snapshot()
|
|
|
|
if progressReporter.callback != nil {
|
|
progressReporter.callback(finalProgress)
|
|
}
|
|
|
|
return finalProgress
|
|
}
|