Files
seaweedfs/weed/storage/blockvol/iscsi/cmd/blockbench/main.go
T
Ping QiuandClaude Opus 4.6 7940e6b7c9 feat: Phase 4A CP4b-4 Windows iSCSI + instrumentation + QA tests
Windows iSCSI Initiator compatibility:
- Add TargetPortalGroupTag to login response (RFC 7143 S13.9)
- Add REQUEST_SENSE, START_STOP_UNIT, MODE_SELECT(6/10) handlers
- Add PERSISTENT_RESERVE_IN/OUT, MAINTENANCE_IN (REPORT SUPPORTED OPCODES)
- Implement MODE SENSE caching (page 0x08) and control (page 0x0A) pages
- Fix Data-In residual underflow/overflow flags (U/O bits on final PDU)
- Rename ScsiReadCapacity16 -> ScsiServiceActionIn16 for correctness

Instrumentation and tooling:
- Add instrumentedAdapter with periodic PERF stats logging
- Add pprof endpoints on admin HTTP server (/debug/pprof/*)
- Add blockbench CLI tool for standalone block device benchmarking
- Add SCSI CDB debug logging in session dispatch

HA integration fixes:
- Move HA test replica ports to 9011-9014 to avoid conflicts
- Add QA adversarial tests for Phase 4A CP4b-4 (755 lines)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 21:17:57 -08:00

190 lines
4.6 KiB
Go

// blockbench is a simple block device benchmark tool.
// Usage: blockbench [-f path] [-size 256M] [-bs 4K] [-duration 10s] [-pattern seq|rand] [-rw read|write|mixed]
package main
import (
"crypto/rand"
"flag"
"fmt"
"math/big"
"os"
"strings"
"sync/atomic"
"time"
)
func main() {
filePath := flag.String("f", "", "test file path (e.g. F:\\testfile.dat)")
fileSize := flag.String("size", "256M", "test file size")
blockSize := flag.String("bs", "4K", "block size")
duration := flag.Duration("duration", 10*time.Second, "test duration")
pattern := flag.String("pattern", "seq", "access pattern: seq or rand")
mode := flag.String("rw", "write", "read, write, or mixed")
workers := flag.Int("workers", 1, "number of concurrent workers")
direct := flag.Bool("direct", false, "use O_DIRECT (not supported on all OS)")
flag.Parse()
if *filePath == "" {
fmt.Fprintln(os.Stderr, "error: -f is required")
flag.Usage()
os.Exit(1)
}
fSize, err := parseSize(*fileSize)
if err != nil {
fmt.Fprintf(os.Stderr, "bad -size: %v\n", err)
os.Exit(1)
}
bSize, err := parseSize(*blockSize)
if err != nil {
fmt.Fprintf(os.Stderr, "bad -bs: %v\n", err)
os.Exit(1)
}
_ = *direct // placeholder for future O_DIRECT support
fmt.Printf("=== BlockBench ===\n")
fmt.Printf("file=%s size=%s bs=%s duration=%s pattern=%s mode=%s workers=%d\n",
*filePath, *fileSize, *blockSize, *duration, *pattern, *mode, *workers)
// Create/open file
f, err := os.OpenFile(*filePath, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "open: %v\n", err)
os.Exit(1)
}
defer f.Close()
// Pre-allocate
if err := f.Truncate(int64(fSize)); err != nil {
fmt.Fprintf(os.Stderr, "truncate: %v\n", err)
os.Exit(1)
}
totalBlocks := fSize / bSize
// Prepare write buffer
writeBuf := make([]byte, bSize)
rand.Read(writeBuf)
var ops atomic.Int64
var bytes atomic.Int64
var maxLatUs atomic.Int64
isRand := *pattern == "rand"
doWrite := *mode == "write" || *mode == "mixed"
doRead := *mode == "read" || *mode == "mixed"
stop := make(chan struct{})
fmt.Printf("running %s %s for %s...\n", *pattern, *mode, *duration)
start := time.Now()
for w := 0; w < *workers; w++ {
wf, err := os.OpenFile(*filePath, os.O_RDWR, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "worker open: %v\n", err)
os.Exit(1)
}
defer wf.Close()
go func(wf *os.File, workerID int) {
buf := make([]byte, bSize)
copy(buf, writeBuf)
var seqBlock uint64
readBuf := make([]byte, bSize)
for {
select {
case <-stop:
return
default:
}
var blockNum uint64
if isRand {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(totalBlocks)))
blockNum = n.Uint64()
} else {
blockNum = seqBlock % totalBlocks
seqBlock++
}
off := int64(blockNum * bSize)
opStart := time.Now()
if doWrite && (!doRead || seqBlock%2 == 0) {
_, err = wf.WriteAt(buf, off)
} else if doRead {
_, err = wf.ReadAt(readBuf, off)
}
us := time.Since(opStart).Microseconds()
if err == nil {
ops.Add(1)
bytes.Add(int64(bSize))
for {
old := maxLatUs.Load()
if us <= old || maxLatUs.CompareAndSwap(old, us) {
break
}
}
}
}
}(wf, w)
}
time.Sleep(*duration)
close(stop)
elapsed := time.Since(start)
// Small sleep to let workers finish
time.Sleep(10 * time.Millisecond)
totalOps := ops.Load()
totalBytes := bytes.Load()
maxLat := maxLatUs.Load()
iops := float64(totalOps) / elapsed.Seconds()
mbps := float64(totalBytes) / (1024 * 1024) / elapsed.Seconds()
avgUs := int64(0)
if totalOps > 0 {
avgUs = int64(elapsed.Microseconds()) / totalOps * int64(*workers)
// Rough approximation — actual avg needs per-op tracking
avgUs = int64(float64(elapsed.Microseconds()) / float64(totalOps) * float64(*workers))
}
fmt.Printf("\n=== Results ===\n")
fmt.Printf("ops: %d\n", totalOps)
fmt.Printf("IOPS: %.0f\n", iops)
fmt.Printf("throughput: %.1f MB/s\n", mbps)
fmt.Printf("max_lat: %d us (%.1f ms)\n", maxLat, float64(maxLat)/1000)
fmt.Printf("elapsed: %.1fs\n", elapsed.Seconds())
if totalOps > 0 {
fmt.Printf("avg_lat: ~%d us (%.2f ms) [estimated]\n", avgUs, float64(avgUs)/1000)
}
}
func parseSize(s string) (uint64, error) {
s = strings.TrimSpace(s)
if len(s) == 0 {
return 0, fmt.Errorf("empty size")
}
mul := uint64(1)
switch s[len(s)-1] {
case 'K', 'k':
mul = 1024
s = s[:len(s)-1]
case 'M', 'm':
mul = 1024 * 1024
s = s[:len(s)-1]
case 'G', 'g':
mul = 1024 * 1024 * 1024
s = s[:len(s)-1]
}
var n uint64
_, err := fmt.Sscanf(s, "%d", &n)
return n * mul, err
}