Files
seaweedfs/weed/server/qa_block_perf_test.go
T
pingqiuandClaude Opus 4.6 643a5a1074 feat: Phase 12 P3+P4 — diagnosability surfaces, perf floor, rollout gates
P3: Add explicit bounded read-only diagnosis surfaces for all symptom classes:
- FailoverDiagnostic: volume-oriented failover state with per-volume
  DeferredPromotion/PendingRebuild entries and proper timer lifecycle
- PublicationDiagnostic: two-read coherence check (LookupBlockVolume vs
  registry authority) with computed Coherent verdict
- RecoveryDiagnostic: minimal ActiveTasks surface (Path A)
- Blocker ledger: 3 diagnosed + 3 unresolved, finite, from actual file
- Runbook references only exposed surfaces, no internal state

P4: Add bounded performance floor + rollout-gate package:
- Engine-local floor measurement with explicit IOPS gates per workload
- Cost characterization: WAL 2x write amp, -56% replication tax
- Rollout gates with semantic cross-checks against cited evidence
  (baseline numbers, transport/network matrix, blocker counts)
- Launch envelope tightened to actually measured combinations only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:20:22 -07:00

583 lines
19 KiB
Go

package weed_server
import (
"crypto/rand"
"fmt"
"math"
mrand "math/rand"
"os"
"path/filepath"
"sort"
"strings"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
)
// ============================================================
// Phase 12 P4: Performance Floor — Bounded Measurement Package
//
// Workload envelope:
// Topology: RF=2 sync_all accepted chosen path
// Operations: 4K random write, 4K random read, 4K sequential write, 4K sequential read
// Runtime: no failover, no disturbance, steady-state
// Environment: unit test harness (single-process, local disk, engine-local I/O)
//
// What this measures:
// Engine I/O floor for the accepted chosen path. WriteLBA/ReadLBA through
// the full fencing path (epoch, role, lease, writeGate, WAL, dirtyMap).
// No transport layer (iSCSI/NVMe). No cross-machine replication.
//
// What this does NOT measure:
// Transport throughput, cross-machine replication tax, multi-client concurrency,
// failover-under-load, degraded mode. Production floor with replication is
// documented in baseline-roce-20260401.md.
//
// NOT performance tuning. NOT broad benchmark.
// ============================================================
const (
perfBlockSize = 4096
perfVolumeSize = 64 * 1024 * 1024 // 64MB
perfWALSize = 16 * 1024 * 1024 // 16MB
perfOps = 1000 // ops per measurement run
perfWarmupOps = 200 // warmup ops (discarded from measurement)
perfIterations = 3 // run N times, report worst as floor
)
// Minimum acceptable floor thresholds (engine-local, single-writer).
//
// These are regression gates, not performance targets. Set conservatively
// so any reasonable hardware passes, but catastrophic regressions
// (accidental serialization, O(n^2) scan, broken WAL path) are caught.
//
// Rationale for values:
// Measured on dev SSD: rand-write ~10K, rand-read ~80K, seq-write ~30K, seq-read ~180K.
// Thresholds set at ~10% of measured to tolerate slow CI machines and VMs.
// Write P99 ceiling at 100ms catches deadlocks/stalls without false-positiving
// on slow storage.
var perfFloorGates = map[string]struct {
MinIOPS float64
MaxWriteP99 time.Duration // 0 = no ceiling (reads)
}{
"rand-write": {MinIOPS: 1000, MaxWriteP99: 100 * time.Millisecond},
"rand-read": {MinIOPS: 5000},
"seq-write": {MinIOPS: 2000, MaxWriteP99: 100 * time.Millisecond},
"seq-read": {MinIOPS: 10000},
}
// perfResult holds measurements for one workload run.
type perfResult struct {
Workload string
Ops int
Elapsed time.Duration
IOPS float64
MBps float64
LatSamples []int64 // per-op latency in nanoseconds
}
func (r *perfResult) latPct(pct float64) time.Duration {
if len(r.LatSamples) == 0 {
return 0
}
sorted := make([]int64, len(r.LatSamples))
copy(sorted, r.LatSamples)
sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
idx := int(math.Ceil(pct/100.0*float64(len(sorted)))) - 1
if idx < 0 {
idx = 0
}
if idx >= len(sorted) {
idx = len(sorted) - 1
}
return time.Duration(sorted[idx])
}
func (r *perfResult) latAvg() time.Duration {
if len(r.LatSamples) == 0 {
return 0
}
var sum int64
for _, s := range r.LatSamples {
sum += s
}
return time.Duration(sum / int64(len(r.LatSamples)))
}
// setupPerfVolume creates a BlockVol configured as Primary for perf measurement.
func setupPerfVolume(t *testing.T) *blockvol.BlockVol {
t.Helper()
dir := t.TempDir()
volPath := filepath.Join(dir, "perf.blk")
vol, err := blockvol.CreateBlockVol(volPath, blockvol.CreateOptions{
VolumeSize: perfVolumeSize,
BlockSize: perfBlockSize,
WALSize: perfWALSize,
})
if err != nil {
t.Fatal(err)
}
// Set up as Primary with long lease so writes are allowed.
if err := vol.HandleAssignment(1, blockvol.RolePrimary, 10*time.Minute); err != nil {
vol.Close()
t.Fatal(err)
}
t.Cleanup(func() { vol.Close() })
return vol
}
// maxLBAs returns the number of addressable 4K blocks in the extent area.
func maxLBAs() uint64 {
// Volume size minus WAL, divided by block size, with safety margin.
return (perfVolumeSize - perfWALSize) / perfBlockSize / 2
}
// runPerfWorkload executes one workload measurement and returns the result.
func runPerfWorkload(t *testing.T, vol *blockvol.BlockVol, workload string, ops int) perfResult {
t.Helper()
data := make([]byte, perfBlockSize)
rand.Read(data)
max := maxLBAs()
samples := make([]int64, 0, ops)
start := time.Now()
for i := 0; i < ops; i++ {
var lba uint64
switch {
case strings.HasPrefix(workload, "rand"):
lba = uint64(mrand.Int63n(int64(max)))
default: // sequential
lba = uint64(i) % max
}
opStart := time.Now()
switch {
case strings.HasSuffix(workload, "write"):
if err := vol.WriteLBA(lba, data); err != nil {
t.Fatalf("%s op %d: WriteLBA(%d): %v", workload, i, lba, err)
}
case strings.HasSuffix(workload, "read"):
if _, err := vol.ReadLBA(lba, perfBlockSize); err != nil {
t.Fatalf("%s op %d: ReadLBA(%d): %v", workload, i, lba, err)
}
}
samples = append(samples, time.Since(opStart).Nanoseconds())
}
elapsed := time.Since(start)
iops := float64(ops) / elapsed.Seconds()
mbps := iops * float64(perfBlockSize) / (1024 * 1024)
return perfResult{
Workload: workload,
Ops: ops,
Elapsed: elapsed,
IOPS: iops,
MBps: mbps,
LatSamples: samples,
}
}
// floorOf returns the worst (lowest) IOPS and worst (highest) P99 across iterations.
type perfFloor struct {
Workload string
FloorIOPS float64
FloorMBps float64
WorstAvg time.Duration
WorstP50 time.Duration
WorstP99 time.Duration
WorstMax time.Duration
}
func computeFloor(results []perfResult) perfFloor {
f := perfFloor{
Workload: results[0].Workload,
FloorIOPS: math.MaxFloat64,
FloorMBps: math.MaxFloat64,
}
for _, r := range results {
if r.IOPS < f.FloorIOPS {
f.FloorIOPS = r.IOPS
}
if r.MBps < f.FloorMBps {
f.FloorMBps = r.MBps
}
avg := r.latAvg()
if avg > f.WorstAvg {
f.WorstAvg = avg
}
p50 := r.latPct(50)
if p50 > f.WorstP50 {
f.WorstP50 = p50
}
p99 := r.latPct(99)
if p99 > f.WorstP99 {
f.WorstP99 = p99
}
pmax := r.latPct(100)
if pmax > f.WorstMax {
f.WorstMax = pmax
}
}
return f
}
// --- Test 1: PerformanceFloor_Bounded ---
func TestP12P4_PerformanceFloor_Bounded(t *testing.T) {
vol := setupPerfVolume(t)
workloads := []string{"rand-write", "rand-read", "seq-write", "seq-read"}
floors := make([]perfFloor, 0, len(workloads))
for _, wl := range workloads {
// Warmup: populate volume with data (needed for reads).
if strings.HasSuffix(wl, "read") {
warmupData := make([]byte, perfBlockSize)
rand.Read(warmupData)
for i := 0; i < int(maxLBAs()); i++ {
if err := vol.WriteLBA(uint64(i), warmupData); err != nil {
break // WAL full is acceptable during warmup
}
}
time.Sleep(200 * time.Millisecond) // let flusher drain
}
// Warmup ops (discarded).
runPerfWorkload(t, vol, wl, perfWarmupOps)
// Measurement: N iterations, take floor.
var results []perfResult
for iter := 0; iter < perfIterations; iter++ {
r := runPerfWorkload(t, vol, wl, perfOps)
results = append(results, r)
}
floor := computeFloor(results)
floors = append(floors, floor)
}
// Report structured floor table.
t.Log("")
t.Log("=== P12P4 Performance Floor (engine-local, single-writer) ===")
t.Log("")
t.Logf("%-12s %10s %8s %10s %10s %10s %10s",
"Workload", "Floor IOPS", "MB/s", "Avg Lat", "P50 Lat", "P99 Lat", "Max Lat")
t.Logf("%-12s %10s %8s %10s %10s %10s %10s",
"--------", "----------", "------", "-------", "-------", "-------", "-------")
for _, f := range floors {
t.Logf("%-12s %10.0f %8.2f %10s %10s %10s %10s",
f.Workload, f.FloorIOPS, f.FloorMBps, f.WorstAvg, f.WorstP50, f.WorstP99, f.WorstMax)
}
t.Log("")
t.Logf("Config: volume=%dMB WAL=%dMB block=%dB ops=%d warmup=%d iterations=%d",
perfVolumeSize/(1024*1024), perfWALSize/(1024*1024), perfBlockSize, perfOps, perfWarmupOps, perfIterations)
t.Log("Method: worst of N iterations (floor, not peak)")
t.Log("Scope: engine-local only; production RF=2 floor in baseline-roce-20260401.md")
// Gate: floor values must meet minimum acceptable thresholds.
// These are regression gates — if any floor drops below the gate,
// the test fails, blocking rollout.
t.Log("")
t.Log("=== Floor Gate Validation ===")
allGatesPassed := true
for _, f := range floors {
gate, ok := perfFloorGates[f.Workload]
if !ok {
t.Fatalf("no floor gate defined for workload %s", f.Workload)
}
passed := true
if f.FloorIOPS < gate.MinIOPS {
t.Errorf("GATE FAIL: %s floor IOPS %.0f < minimum %.0f", f.Workload, f.FloorIOPS, gate.MinIOPS)
passed = false
}
if gate.MaxWriteP99 > 0 && f.WorstP99 > gate.MaxWriteP99 {
t.Errorf("GATE FAIL: %s worst P99 %s > ceiling %s", f.Workload, f.WorstP99, gate.MaxWriteP99)
passed = false
}
status := "PASS"
if !passed {
status = "FAIL"
allGatesPassed = false
}
t.Logf(" %-12s min=%6.0f IOPS → floor=%6.0f [%s]", f.Workload, gate.MinIOPS, f.FloorIOPS, status)
}
if !allGatesPassed {
t.Fatal("P12P4 PerformanceFloor: FAIL — one or more floor gates not met")
}
t.Log("P12P4 PerformanceFloor: PASS — all floor gates met")
}
// --- Test 2: CostCharacterization_Bounded ---
func TestP12P4_CostCharacterization_Bounded(t *testing.T) {
vol := setupPerfVolume(t)
// Measure write latency breakdown: WriteLBA includes WAL append + group commit.
data := make([]byte, perfBlockSize)
rand.Read(data)
max := maxLBAs()
const costOps = 500
var writeLatSum int64
for i := 0; i < costOps; i++ {
lba := uint64(mrand.Int63n(int64(max)))
start := time.Now()
if err := vol.WriteLBA(lba, data); err != nil {
t.Fatalf("write op %d: %v", i, err)
}
writeLatSum += time.Since(start).Nanoseconds()
}
avgWriteLat := time.Duration(writeLatSum / costOps)
// Measure read latency for comparison.
// Populate first.
for i := 0; i < int(max/2); i++ {
vol.WriteLBA(uint64(i), data)
}
time.Sleep(200 * time.Millisecond) // let flusher drain
var readLatSum int64
for i := 0; i < costOps; i++ {
lba := uint64(mrand.Int63n(int64(max / 2)))
start := time.Now()
if _, err := vol.ReadLBA(lba, perfBlockSize); err != nil {
t.Fatalf("read op %d: %v", i, err)
}
readLatSum += time.Since(start).Nanoseconds()
}
avgReadLat := time.Duration(readLatSum / costOps)
// Cost statement.
t.Log("")
t.Log("=== P12P4 Cost Characterization (engine-local) ===")
t.Log("")
t.Logf("Average write latency: %s (includes WAL append + group commit sync)", avgWriteLat)
t.Logf("Average read latency: %s (dirtyMap lookup + WAL/extent read)", avgReadLat)
t.Log("")
t.Log("Bounded cost statement:")
t.Log(" WAL write amplification: 2x minimum (WAL write + eventual extent flush)")
t.Log(" Group commit: amortizes fdatasync across batched writers (1 sync per batch)")
t.Log(" Replication tax (production RF=2 sync_all): -56% vs RF=1 (barrier round-trip)")
t.Log(" Replication tax source: baseline-roce-20260401.md, measured on 25Gbps RoCE")
t.Log("")
t.Logf("Write/read ratio: %.1fx (write is %.1fx slower than read)",
float64(avgWriteLat)/float64(avgReadLat),
float64(avgWriteLat)/float64(avgReadLat))
t.Log("")
t.Logf("Config: volume=%dMB WAL=%dMB block=%dB ops=%d",
perfVolumeSize/(1024*1024), perfWALSize/(1024*1024), perfBlockSize, costOps)
// Proof: cost values are finite and positive.
if avgWriteLat <= 0 || avgReadLat <= 0 {
t.Fatal("latency values must be positive")
}
if avgWriteLat < avgReadLat {
t.Log("Note: write faster than read in this run (possible due to WAL cache hits)")
}
t.Log("P12P4 CostCharacterization: PASS — bounded cost statement produced")
}
// --- Test 3: RolloutGate_Bounded ---
func TestP12P4_RolloutGate_Bounded(t *testing.T) {
floorPath := "../../sw-block/.private/phase/phase-12-p4-floor.md"
gatesPath := "../../sw-block/.private/phase/phase-12-p4-rollout-gates.md"
baselinePath := "../../learn/projects/sw-block/test/results/baseline-roce-20260401.md"
blockerPath := "../../sw-block/.private/phase/phase-12-p3-blockers.md"
// --- Read all cited evidence sources ---
floorData, err := os.ReadFile(floorPath)
if err != nil {
t.Fatalf("floor doc must exist at %s: %v", floorPath, err)
}
floorContent := string(floorData)
gatesData, err := os.ReadFile(gatesPath)
if err != nil {
t.Fatalf("rollout-gates doc must exist at %s: %v", gatesPath, err)
}
gatesContent := string(gatesData)
baselineData, err := os.ReadFile(baselinePath)
if err != nil {
t.Fatalf("cited baseline must exist at %s: %v", baselinePath, err)
}
baselineContent := string(baselineData)
blockerData, err := os.ReadFile(blockerPath)
if err != nil {
t.Fatalf("cited blocker ledger must exist at %s: %v", blockerPath, err)
}
blockerContent := string(blockerData)
// --- Structural validation (shape) ---
// Floor doc: workload envelope, floor table, non-claims.
for _, required := range []string{"RF=2", "sync_all", "4K random write", "4K random read", "sequential write", "sequential read"} {
if !strings.Contains(floorContent, required) {
t.Fatalf("floor doc missing required content: %q", required)
}
}
if !strings.Contains(floorContent, "Floor") || !strings.Contains(floorContent, "IOPS") {
t.Fatal("floor doc must contain floor table with IOPS")
}
if !strings.Contains(floorContent, "does NOT") {
t.Fatal("floor doc must contain explicit non-claims")
}
// Gates doc: gates table, launch envelope, exclusions, non-claims.
if !strings.Contains(gatesContent, "Gate") || !strings.Contains(gatesContent, "Status") {
t.Fatal("rollout-gates doc must contain gates table")
}
if !strings.Contains(gatesContent, "Launch Envelope") {
t.Fatal("rollout-gates doc must contain launch envelope")
}
if !strings.Contains(gatesContent, "Exclusion") {
t.Fatal("rollout-gates doc must contain exclusions")
}
if !strings.Contains(gatesContent, "does NOT") {
t.Fatal("rollout-gates doc must contain explicit non-claims")
}
// Count gates — must be finite.
gateLines := 0
for _, line := range strings.Split(gatesContent, "\n") {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "| G") || strings.HasPrefix(trimmed, "| E") {
gateLines++
}
}
if gateLines == 0 {
t.Fatal("rollout-gates doc has no gate items")
}
if gateLines > 20 {
t.Fatalf("rollout-gates doc should be finite, got %d items", gateLines)
}
// --- Semantic cross-checks (evidence alignment) ---
// 1. G6 cites "28.4K write IOPS" — baseline must contain this number.
if strings.Contains(gatesContent, "28.4K write IOPS") || strings.Contains(gatesContent, "28,4") {
// The gates doc cites write IOPS from baseline. Verify the baseline has it.
if !strings.Contains(baselineContent, "28,") {
t.Fatal("G6 cites write IOPS but baseline does not contain matching value")
}
}
// More precise: baseline must contain the specific numbers cited in G6.
if !strings.Contains(baselineContent, "28,347") && !strings.Contains(baselineContent, "28,429") &&
!strings.Contains(baselineContent, "28,453") {
t.Fatal("baseline must contain RF=2 sync_all write IOPS data (28,3xx-28,4xx range)")
}
if !strings.Contains(baselineContent, "136,648") {
t.Fatal("baseline must contain RF=2 read IOPS data (136,648)")
}
// 2. G5 cites "-56% replication tax" — baseline must contain this.
if strings.Contains(gatesContent, "-56%") {
if !strings.Contains(baselineContent, "-56%") {
t.Fatal("G5 cites -56% replication tax but baseline does not contain -56%")
}
}
// 3. Launch envelope claims specific transport/network combos — verify against baseline.
// Claimed: NVMe-TCP @ 25Gbps RoCE
if strings.Contains(gatesContent, "NVMe-TCP @ 25Gbps RoCE") {
if !strings.Contains(baselineContent, "NVMe-TCP") || !strings.Contains(baselineContent, "RoCE") {
t.Fatal("launch envelope claims NVMe-TCP @ RoCE but baseline has no such data")
}
}
// Claimed: iSCSI @ 25Gbps RoCE
if strings.Contains(gatesContent, "iSCSI @ 25Gbps RoCE") {
if !strings.Contains(baselineContent, "iSCSI") || !strings.Contains(baselineContent, "RoCE") {
t.Fatal("launch envelope claims iSCSI @ RoCE but baseline has no such data")
}
}
// Claimed: iSCSI @ 1Gbps
if strings.Contains(gatesContent, "iSCSI @ 1Gbps") {
if !strings.Contains(baselineContent, "iSCSI") || !strings.Contains(baselineContent, "1Gbps") {
t.Fatal("launch envelope claims iSCSI @ 1Gbps but baseline has no such data")
}
}
// Exclusion: NVMe-TCP @ 1Gbps must NOT be claimed as supported.
if strings.Contains(gatesContent, "NOT included") {
// Verify baseline indeed lacks NVMe-TCP @ 1Gbps.
hasNvme1g := strings.Contains(baselineContent, "NVMe-TCP") && strings.Contains(baselineContent, "| NVMe-TCP | 1Gbps")
if hasNvme1g {
t.Fatal("baseline contains NVMe-TCP @ 1Gbps data but gates doc excludes it — resolve mismatch")
}
}
// 4. G7 cites blocker ledger counts — verify against actual ledger.
if strings.Contains(gatesContent, "3 diagnosed") {
diagCount := 0
for _, line := range strings.Split(blockerContent, "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "| B") {
diagCount++
}
}
if diagCount != 3 {
t.Fatalf("G7 claims 3 diagnosed blockers but ledger has %d", diagCount)
}
}
if strings.Contains(gatesContent, "3 unresolved") {
unresCount := 0
for _, line := range strings.Split(blockerContent, "\n") {
if strings.HasPrefix(strings.TrimSpace(line), "| U") {
unresCount++
}
}
if unresCount != 3 {
t.Fatalf("G7 claims 3 unresolved blockers but ledger has %d", unresCount)
}
}
// 5. Floor doc gate thresholds must match code-defined gates.
for workload, gate := range perfFloorGates {
// The doc uses comma-formatted numbers (e.g., "1,000" or "5,000").
minInt := int(gate.MinIOPS)
// Check for both comma-formatted and plain forms.
found := false
for _, form := range []string{
fmt.Sprintf("%d", minInt), // "1000"
fmt.Sprintf("%d,%03d", minInt/1000, minInt%1000), // "1,000"
} {
if strings.Contains(floorContent, form) {
found = true
break
}
}
if !found {
t.Errorf("floor doc gate for %s should cite minimum %d IOPS but doesn't", workload, minInt)
}
}
t.Logf("P12P4 RolloutGate: floor doc %d bytes, gates doc %d bytes, %d gate items",
len(floorData), len(gatesData), gateLines)
t.Log("P12P4 RolloutGate: semantic cross-checks passed (baseline, blocker ledger, gate thresholds)")
t.Log("P12P4 RolloutGate: PASS — bounded launch envelope with verified evidence alignment")
}
// --- Helpers ---
func init() {
// Seed random for reproducible LBA patterns within a test run.
mrand.Seed(time.Now().UnixNano())
}
// formatDuration formats a duration for table display.
func formatDuration(d time.Duration) string {
if d < time.Microsecond {
return fmt.Sprintf("%dns", d.Nanoseconds())
}
if d < time.Millisecond {
return fmt.Sprintf("%.1fus", float64(d.Nanoseconds())/1000.0)
}
return fmt.Sprintf("%.2fms", float64(d.Nanoseconds())/1e6)
}