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>
193 lines
7.6 KiB
Go
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
|