syncova-backup/packages/benchmark/measurement.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
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>
2026-08-17 09:10:54 +02:00

193 lines
7.6 KiB
Go

// Package benchmark misst Durchsatz und Ressourcenverbrauch der Anlage
// (SYNCOVA_IMPLEMENTATION_PLAN.md §22).
//
// Die Regel dieses Pakets steht in einem Satz:
//
// Eine Zahl ohne Messbedingungen ist wertlos — und wird trotzdem zitiert.
//
// Deshalb trägt jede Messung die Maschine, die Datenmenge, die Beschaffenheit
// der Daten und die Einstellungen bei sich. „420 MiB/s" ohne den Zusatz
// „inkompressible Daten, lokale SSD, acht Kerne" ist eine Zahl, die in einer
// Präsentation landet und dort etwas anderes behauptet, als gemessen wurde.
//
// Das Projekt hat diesen Fehler bereits einmal gemacht: In Phase 4 zeigte ein
// Lauf eine 1021-fache Kompression, weil die Testdaten periodisch waren. Seither
// gilt: Durchsatzmessungen ausschließlich mit inkompressiblen Daten.
package benchmark
import (
"fmt"
"runtime"
"time"
)
// MachineProfile beschreibt die Maschine, auf der gemessen wurde.
//
// Ohne diese Angaben lässt sich eine Messung weder einordnen noch wiederholen.
type MachineProfile struct {
// OperatingSystem ist das Betriebssystem.
OperatingSystem string `json:"operating_system"`
// Architecture ist die Prozessorarchitektur.
Architecture string `json:"architecture"`
// LogicalCPUs ist die Zahl nutzbarer Kerne.
LogicalCPUs int `json:"logical_cpus"`
// GoMaxProcs ist die tatsächlich genutzte Zahl paralleler Abläufe.
//
// Sie steht neben der Kernzahl, weil ein Messlauf sie absichtlich
// begrenzen kann — genau das ist das Szenario „wenig CPU".
GoMaxProcs int `json:"go_max_procs"`
// GoVersion ist die Fassung der Laufzeitumgebung.
GoVersion string `json:"go_version"`
// MemoryLimitBytes ist eine gesetzte Speichergrenze; 0 bedeutet keine.
MemoryLimitBytes int64 `json:"memory_limit_bytes,omitempty"`
}
// CurrentMachine erfasst die Maschine des laufenden Prozesses.
func CurrentMachine() MachineProfile {
return MachineProfile{
OperatingSystem: runtime.GOOS,
Architecture: runtime.GOARCH,
LogicalCPUs: runtime.NumCPU(),
GoMaxProcs: runtime.GOMAXPROCS(0),
GoVersion: runtime.Version(),
}
}
// DataShape beschreibt die Beschaffenheit der gemessenen Daten.
//
// Der wichtigste Wert ist Compressible: Bei komprimierbaren Daten misst man die
// Kompression, nicht den Durchsatz. Beide Zahlen sind gültig — sie beantworten
// nur verschiedene Fragen, und wer sie verwechselt, veröffentlicht eine
// Leistung, die es nie gab.
type DataShape struct {
// Description benennt die Datenart in Worten.
Description string `json:"description"`
// TotalBytes ist die Gesamtmenge der Ursprungsdaten.
TotalBytes int64 `json:"total_bytes"`
// FileCount ist die Zahl der Dateien.
FileCount int `json:"file_count"`
// Compressible meldet komprimierbare Daten.
Compressible bool `json:"compressible"`
}
// ResourceUsage sind die um einen Lauf herum erfassten Verbrauchswerte.
type ResourceUsage struct {
// WallClockSeconds ist die vergangene Zeit.
WallClockSeconds float64 `json:"wall_clock_seconds"`
// UserCPUSeconds ist die im Programm verbrachte Rechenzeit.
UserCPUSeconds float64 `json:"user_cpu_seconds"`
// SystemCPUSeconds ist die im Betriebssystem verbrachte Rechenzeit.
SystemCPUSeconds float64 `json:"system_cpu_seconds"`
// PeakMemoryBytes ist der höchste beobachtete Speicherbedarf.
//
// Gemessen als maximale Belegung des Go-Haufens, nicht als RSS des
// Prozesses: Der RSS enthält auch den Dateizwischenspeicher des
// Betriebssystems und stiege bei einem Sicherungslauf allein durch das
// Lesen der Quelle — eine Zahl, die mehr über den Kernel aussagt als über
// diese Anlage.
PeakMemoryBytes int64 `json:"peak_memory_bytes"`
// AllocatedBytes ist die insgesamt angeforderte Speichermenge.
//
// Sie liegt weit über dem Höchststand, weil kurzlebige Puffer mitzählen.
// Interessant ist sie als Maß für den Druck auf die Speicherbereinigung.
AllocatedBytes int64 `json:"allocated_bytes"`
// GarbageCollections ist die Zahl der Speicherbereinigungen.
GarbageCollections int `json:"garbage_collections"`
}
// CPUEfficiency liefert die verbrauchte Rechenzeit je Sekunde Laufzeit.
//
// Ein Wert um 1,0 bedeutet: ein Kern war ausgelastet. Bei 8,0 laufen acht Kerne
// voll. Deutlich unter 1,0 heißt, dass der Lauf auf etwas gewartet hat — meist
// auf die Platte.
func (usage ResourceUsage) CPUEfficiency() float64 {
if usage.WallClockSeconds <= 0 {
return 0
}
return (usage.UserCPUSeconds + usage.SystemCPUSeconds) / usage.WallClockSeconds
}
// Result ist das Ergebnis eines Messlaufs.
type Result struct {
// ScenarioName benennt das Szenario.
ScenarioName string `json:"scenario"`
// Description erklärt, welche Frage der Lauf beantwortet.
Description string `json:"description"`
// Machine ist die Maschine, auf der gemessen wurde.
Machine MachineProfile `json:"machine"`
// Data beschreibt die gemessenen Daten.
Data DataShape `json:"data"`
// Usage sind die Verbrauchswerte.
Usage ResourceUsage `json:"usage"`
// BytesProcessed ist die verarbeitete Datenmenge.
BytesProcessed int64 `json:"bytes_processed"`
// BytesStored ist die tatsächlich abgelegte Datenmenge.
BytesStored int64 `json:"bytes_stored"`
// FilesProcessed ist die Zahl verarbeiteter Objekte.
FilesProcessed int `json:"files_processed"`
// Notes benennen Einschränkungen dieser Messung.
//
// Sie stehen **im Ergebnis**, nicht in einer Fußnote: Wer die Zahl
// weiterreicht, reicht die Einschränkung mit.
Notes []string `json:"notes,omitempty"`
// MeasuredAt ist der Zeitpunkt der Messung in UTC.
MeasuredAt time.Time `json:"measured_at"`
}
// ThroughputBytesPerSecond liefert den Durchsatz.
//
// Er ist nur dann eine Aussage, wenn die Daten inkompressibel waren. Bei
// komprimierbaren Daten misst er, wie gut zstd arbeitet — eine andere Frage.
func (result *Result) ThroughputBytesPerSecond() float64 {
if result.Usage.WallClockSeconds <= 0 {
return 0
}
return float64(result.BytesProcessed) / result.Usage.WallClockSeconds
}
// FilesPerSecond liefert die Zahl verarbeiteter Objekte je Sekunde.
//
// Bei vielen kleinen Dateien ist das die aussagekräftigere Zahl: Dort begrenzt
// nicht der Durchsatz, sondern der Aufwand je Datei.
func (result *Result) FilesPerSecond() float64 {
if result.Usage.WallClockSeconds <= 0 {
return 0
}
return float64(result.FilesProcessed) / result.Usage.WallClockSeconds
}
// AddNote ergänzt eine Einschränkung.
func (result *Result) AddNote(noteFormat string, formatArguments ...any) {
result.Notes = append(result.Notes, fmt.Sprintf(noteFormat, formatArguments...))
}
// Validate prüft ein Messergebnis auf Aussagekraft.
//
// Sie verhindert die beiden Zahlen, die am häufigsten falsch zitiert werden:
// einen Durchsatz aus komprimierbaren Daten und einen aus einem Lauf, der zu
// kurz war, um mehr zu messen als die eigene Startzeit.
func (result *Result) Validate() error {
if result.Data.Compressible && result.BytesProcessed > 0 {
return fmt.Errorf("der lauf %q misst komprimierbare daten; ein durchsatz daraus "+
"beschreibt die kompression, nicht die leistung", result.ScenarioName)
}
if result.Usage.WallClockSeconds < minimumMeaningfulSeconds {
return fmt.Errorf("der lauf %q dauerte nur %.3f s; darunter bestimmt die "+
"messungenauigkeit das ergebnis", result.ScenarioName, result.Usage.WallClockSeconds)
}
return nil
}
// minimumMeaningfulSeconds ist die kürzeste noch aussagekräftige Laufzeit.
//
// Eine halbe Sekunde: Darunter schlagen der Start der Arbeiter, das Öffnen des
// Repositorys und die erste Speicherbereinigung stärker durch als die eigentliche
// Arbeit. Dieselbe Überlegung wie bei den Durchsatzdiagrammen (Phase 13), wo
// Läufe unter einer Sekunde herausfallen.
const minimumMeaningfulSeconds = 0.5