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>
135 lines
4.5 KiB
Go
135 lines
4.5 KiB
Go
package benchmark
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Report fasst mehrere Messläufe zusammen.
|
|
type Report struct {
|
|
// Title benennt die Messreihe.
|
|
Title string `json:"title"`
|
|
// Machine ist die Maschine aller Läufe.
|
|
Machine MachineProfile `json:"machine"`
|
|
// Results sind die einzelnen Läufe.
|
|
Results []*Result `json:"results"`
|
|
// SkippedScenarios benennen nicht durchgeführte Szenarien samt Grund.
|
|
//
|
|
// Sie stehen gleichberechtigt neben den Ergebnissen: Ein Messbericht, der
|
|
// verschweigt, was er nicht gemessen hat, liest sich vollständig und ist es
|
|
// nicht. Dieselbe Regel wie bei den ungeprüften Bereichen im Security
|
|
// Center (Phase 15).
|
|
SkippedScenarios []SkippedScenario `json:"skipped_scenarios,omitempty"`
|
|
// GeneratedAt ist der Zeitpunkt des Berichts in UTC.
|
|
GeneratedAt time.Time `json:"generated_at"`
|
|
}
|
|
|
|
// SkippedScenario ist ein Szenario, das nicht gemessen werden konnte.
|
|
type SkippedScenario struct {
|
|
// Name benennt das Szenario.
|
|
Name string `json:"name"`
|
|
// Reason erklärt, warum es nicht gemessen wurde.
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// AddSkipped vermerkt ein nicht gemessenes Szenario.
|
|
func (report *Report) AddSkipped(scenarioName string, reason string) {
|
|
report.SkippedScenarios = append(report.SkippedScenarios,
|
|
SkippedScenario{Name: scenarioName, Reason: reason})
|
|
}
|
|
|
|
// WriteText schreibt den Bericht in lesbarer Form.
|
|
func (report *Report) WriteText(outputWriter io.Writer) {
|
|
fmt.Fprintf(outputWriter, "%s\n%s\n\n", report.Title, strings.Repeat("=", len(report.Title)))
|
|
|
|
fmt.Fprintf(outputWriter, "Maschine: %s/%s, %d Kerne (genutzt: %d), %s\n",
|
|
report.Machine.OperatingSystem, report.Machine.Architecture,
|
|
report.Machine.LogicalCPUs, report.Machine.GoMaxProcs, report.Machine.GoVersion)
|
|
|
|
if report.Machine.MemoryLimitBytes > 0 {
|
|
fmt.Fprintf(outputWriter, "Speichergrenze: %s\n",
|
|
FormatBytes(float64(report.Machine.MemoryLimitBytes)))
|
|
}
|
|
|
|
fmt.Fprintf(outputWriter, "Gemessen: %s\n\n", report.GeneratedAt.Format(time.RFC3339))
|
|
|
|
for _, result := range report.Results {
|
|
writeResultText(outputWriter, result)
|
|
}
|
|
|
|
if len(report.SkippedScenarios) > 0 {
|
|
fmt.Fprintf(outputWriter, "\nNicht gemessen\n--------------\n")
|
|
|
|
for _, skipped := range report.SkippedScenarios {
|
|
fmt.Fprintf(outputWriter, " %s\n %s\n", skipped.Name, skipped.Reason)
|
|
}
|
|
}
|
|
}
|
|
|
|
// writeResultText schreibt einen einzelnen Lauf.
|
|
func writeResultText(outputWriter io.Writer, result *Result) {
|
|
fmt.Fprintf(outputWriter, "%s\n", result.ScenarioName)
|
|
fmt.Fprintf(outputWriter, " %s\n", result.Description)
|
|
fmt.Fprintf(outputWriter, " Daten: %s (%s, %d Dateien)\n",
|
|
result.Data.Description, FormatBytes(float64(result.Data.TotalBytes)), result.Data.FileCount)
|
|
fmt.Fprintf(outputWriter, " Dauer: %.2f s\n", result.Usage.WallClockSeconds)
|
|
|
|
// Der Durchsatz erscheint nur bei inkompressiblen Daten. Bei
|
|
// komprimierbaren misst er die Kompression — und wer die Zahl aus dem
|
|
// Bericht zitiert, zitiert dann etwas anderes, als er meint.
|
|
if !result.Data.Compressible {
|
|
fmt.Fprintf(outputWriter, " Durchsatz: %s/s\n",
|
|
FormatBytes(result.ThroughputBytesPerSecond()))
|
|
} else {
|
|
fmt.Fprintf(outputWriter, " Durchsatz: nicht ausgewiesen (komprimierbare Daten — "+
|
|
"die Zahl beschriebe die Kompression)\n")
|
|
}
|
|
|
|
if result.FilesProcessed > 0 {
|
|
fmt.Fprintf(outputWriter, " Objekte: %d (%.0f je Sekunde)\n",
|
|
result.FilesProcessed, result.FilesPerSecond())
|
|
}
|
|
|
|
if result.BytesStored > 0 {
|
|
fmt.Fprintf(outputWriter, " Abgelegt: %s\n", FormatBytes(float64(result.BytesStored)))
|
|
}
|
|
|
|
fmt.Fprintf(outputWriter, " CPU: %.2f s Programm + %.2f s System "+
|
|
"(%.1f Kerne ausgelastet)\n",
|
|
result.Usage.UserCPUSeconds, result.Usage.SystemCPUSeconds, result.Usage.CPUEfficiency())
|
|
fmt.Fprintf(outputWriter, " Speicher: %s Höchststand, %s angefordert, %d Bereinigungen\n",
|
|
FormatBytes(float64(result.Usage.PeakMemoryBytes)),
|
|
FormatBytes(float64(result.Usage.AllocatedBytes)),
|
|
result.Usage.GarbageCollections)
|
|
|
|
for _, note := range result.Notes {
|
|
fmt.Fprintf(outputWriter, " Hinweis: %s\n", note)
|
|
}
|
|
|
|
fmt.Fprintln(outputWriter)
|
|
}
|
|
|
|
// FormatBytes stellt eine Datenmenge lesbar dar.
|
|
func FormatBytes(byteCount float64) string {
|
|
const unitStep = 1024.0
|
|
|
|
if byteCount < unitStep {
|
|
return fmt.Sprintf("%.0f B", byteCount)
|
|
}
|
|
|
|
unitNames := []string{"KiB", "MiB", "GiB", "TiB"}
|
|
scaledValue := byteCount / unitStep
|
|
|
|
for _, unitName := range unitNames {
|
|
if scaledValue < unitStep {
|
|
return fmt.Sprintf("%.1f %s", scaledValue, unitName)
|
|
}
|
|
|
|
scaledValue /= unitStep
|
|
}
|
|
|
|
return fmt.Sprintf("%.1f PiB", scaledValue)
|
|
}
|