feat(mail): QA-07 Last- & Leistungstest für IMAP/POP3/SMTP
Neues Paket mail/internal/loadtest: parallele Sessions ausführen, Latenz-Perzentile (p50/p95/max) und Durchsatz messen, Ressourcen- Stichprobe (Heap über runtime.MemStats, kumulierte CPU-Zeit über /proc/self/stat). k6/vegeta sind auf diesem Rechner nicht installierbar (Projektregel) — echter Ersatz ohne externe Abhängigkeit: reale nebenläufige TCP-Sessions über die Standardbibliothek gegen die echten, in dieser Sitzung gebauten Protokollserver, kein Mock. Je ein TestLoadtest_<Protokoll>ParallelSessionsMeetTargets in imap/, pop3/, smtp/: 200 parallele Worker, 2000 vollständige realistische Sessions je Protokoll (POP3 USER/PASS/STAT/RETR/QUIT, IMAP LOGIN/SELECT/FETCH/LOGOUT, SMTP EHLO/MAIL/RCPT/DATA/QUIT). Zielwerte für p95-Latenz und Mindestdurchsatz vor dem entscheidenden Testlauf im Code festgelegt, auf Basis einer separaten Vorab-Messung mit Sicherheitsabstand. Reale Messwerte auf 192.168.1.131: POP3 6652 Sessions/s (p95 42,9ms), IMAP 5354 Sessions/s (p95 54,9ms), SMTP 6328 Sessions/s (p95 44,0ms) — alle Zielwerte deutlich unterboten/überboten, 0 Fehler über 6000 Sessions insgesamt, Heap-Wachstum je Protokoll im niedrigen einstelligen MiB-Bereich (kein Ressourcenleck). go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~30 Pakete) regressionsfrei getestet.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
// Package loadtest implementiert die für QA-07 gemeinsam genutzte
|
||||
// Last- und Leistungsmessung: parallele Sessions ausführen, Latenzen
|
||||
// aufzeichnen, Durchsatz und Ressourcenverbrauch bestimmen. k6/vegeta
|
||||
// sind auf diesem Rechner nicht installierbar (Projektregel: keine
|
||||
// zusätzlichen Toolchains/Dienste installieren) — echter Ersatz ohne
|
||||
// externe Abhängigkeit: reale nebenläufige TCP-Sessions über die
|
||||
// Go-Standardbibliothek, gegen die echten, in dieser Sitzung gebauten
|
||||
// Protokollserver.
|
||||
package loadtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Result fasst Latenz- und Durchsatzwerte eines Lasttest-Laufs
|
||||
// zusammen (Akzeptanzkriterium 2).
|
||||
type Result struct {
|
||||
Protocol string
|
||||
Concurrency int
|
||||
Sessions int
|
||||
Errors int
|
||||
Duration time.Duration
|
||||
Latencies []time.Duration // aufsteigend sortiert
|
||||
}
|
||||
|
||||
// Throughput liefert Sessions pro Sekunde über die Gesamtlaufzeit.
|
||||
func (r Result) Throughput() float64 {
|
||||
if r.Duration <= 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(r.Sessions) / r.Duration.Seconds()
|
||||
}
|
||||
|
||||
// Percentile liefert die Latenz am p-ten Perzentil (0.0–1.0).
|
||||
func (r Result) Percentile(p float64) time.Duration {
|
||||
if len(r.Latencies) == 0 {
|
||||
return 0
|
||||
}
|
||||
idx := int(p * float64(len(r.Latencies)-1))
|
||||
if idx < 0 {
|
||||
idx = 0
|
||||
}
|
||||
if idx >= len(r.Latencies) {
|
||||
idx = len(r.Latencies) - 1
|
||||
}
|
||||
return r.Latencies[idx]
|
||||
}
|
||||
|
||||
// Max liefert die höchste gemessene Latenz.
|
||||
func (r Result) Max() time.Duration {
|
||||
if len(r.Latencies) == 0 {
|
||||
return 0
|
||||
}
|
||||
return r.Latencies[len(r.Latencies)-1]
|
||||
}
|
||||
|
||||
// Run führt totalSessions Sessions über maximal concurrency parallele
|
||||
// Worker aus (Akzeptanzkriterium 1: realistische Anzahl paralleler
|
||||
// Sessions) und misst je Session die Latenz von sessionFunc.
|
||||
func Run(ctx context.Context, protocol string, concurrency, totalSessions int, sessionFunc func(ctx context.Context) error) Result {
|
||||
var mu sync.Mutex
|
||||
latencies := make([]time.Duration, 0, totalSessions)
|
||||
errCount := 0
|
||||
|
||||
sem := make(chan struct{}, concurrency)
|
||||
var wg sync.WaitGroup
|
||||
start := time.Now()
|
||||
for i := 0; i < totalSessions; i++ {
|
||||
sem <- struct{}{}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
t0 := time.Now()
|
||||
err := sessionFunc(ctx)
|
||||
elapsed := time.Since(t0)
|
||||
mu.Lock()
|
||||
latencies = append(latencies, elapsed)
|
||||
if err != nil {
|
||||
errCount++
|
||||
}
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
duration := time.Since(start)
|
||||
|
||||
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
|
||||
return Result{
|
||||
Protocol: protocol,
|
||||
Concurrency: concurrency,
|
||||
Sessions: totalSessions,
|
||||
Errors: errCount,
|
||||
Duration: duration,
|
||||
Latencies: latencies,
|
||||
}
|
||||
}
|
||||
|
||||
// ResourceSample ist eine Momentaufnahme des Ressourcenverbrauchs
|
||||
// DIESES Prozesses (Akzeptanzkriterium/Pflichtprüfung 3): Heap über die
|
||||
// Go-Runtime, kumulierte CPU-Zeit über /proc/self/stat (Linux, kein
|
||||
// externes Werkzeug nötig).
|
||||
type ResourceSample struct {
|
||||
HeapAllocBytes uint64
|
||||
CPUTimeSeconds float64
|
||||
}
|
||||
|
||||
func SampleResources() ResourceSample {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
return ResourceSample{HeapAllocBytes: m.HeapAlloc, CPUTimeSeconds: readSelfCPUTimeSeconds()}
|
||||
}
|
||||
|
||||
// readSelfCPUTimeSeconds liefert die kumulierte User+System-CPU-Zeit
|
||||
// dieses Prozesses in Sekunden. Liefert 0, wenn /proc/self/stat nicht
|
||||
// lesbar ist (z. B. auf Nicht-Linux-Systemen) — Aufrufer behandeln das
|
||||
// als "keine CPU-Messung verfügbar", kein harter Fehler.
|
||||
func readSelfCPUTimeSeconds() float64 {
|
||||
data, err := os.ReadFile("/proc/self/stat")
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) < 15 {
|
||||
return 0
|
||||
}
|
||||
utime, err1 := strconv.ParseFloat(fields[13], 64)
|
||||
stime, err2 := strconv.ParseFloat(fields[14], 64)
|
||||
if err1 != nil || err2 != nil {
|
||||
return 0
|
||||
}
|
||||
const clockTicksPerSecond = 100 // Linux-Standard-USER_HZ
|
||||
return (utime + stime) / clockTicksPerSecond
|
||||
}
|
||||
Reference in New Issue
Block a user