diff --git a/README.md b/README.md index 12da9cd..f59fadc 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ flowchart LR ## 当前完成度 -截至 **2026-08-07**,实施计划中可直接勾选的检查项为 **70 / 75(93.3%)**。详情见 +截至 **2026-08-07**,实施计划中可直接勾选的检查项为 **71 / 75(94.7%)**。详情见 [实施计划](docs/development/implementation-plan.md)和 [交付完成度审计](docs/requirements/completion-audit.md)。 @@ -160,11 +160,15 @@ Secret 分离管理。 go run ./deploy/tools/configcheck deploy/config/local.yaml ./scripts/verify.ps1 ./scripts/package-docs.ps1 +./scripts/benchmark-gateway.ps1 ``` `package-docs.ps1` 默认生成被忽略的 `dist/proxy-pool-docs-v1.0.zip`;包内包含 README、 `docs/`、图表、OpenAPI 和 Proto 契约,并以 `manifest.json` 记录 Git revision、文件大小和 SHA-256。可使用 `-Version vMAJOR.MINOR[.PATCH]` 与 `-OutputPath OUTPUT.zip` 生成指定交付物。 +`benchmark-gateway.ps1` 固定执行一次 100k 索引调度、Routing Round Robin 和 Snapshot Apply +微基准,并把版本与原始输出写入 `dist/gateway-benchmarks.txt`;它用于回归比较,不构成 +网络转发或 100k QPS 集群容量证明。 Redis、PostgreSQL 和 Controller fixture 脚本会使用 Docker 启动隔离依赖: diff --git a/docs/development/implementation-plan.md b/docs/development/implementation-plan.md index 1e6ee2e..98eeea8 100644 --- a/docs/development/implementation-plan.md +++ b/docs/development/implementation-plan.md @@ -395,7 +395,11 @@ CI 另有独立 Deployment job,在占位凭据下渲染 Compose,并使用 `k - [x] Map every requirement ID to code, test, contract, document, or verified runtime evidence. - [ ] Run `gofmt`, `go vet`, unit tests, race tests, builds, contract validation, and documentation link/example validation. -- [ ] Run bounded local performance benchmarks; label 100k QPS as unverified until a +- [x] Run bounded local performance benchmarks; label 100k QPS as unverified until a representative cluster load run exists. + +`scripts/benchmark-gateway.ps1` 固定使用 `-count=1 -benchtime=1x` 分别执行 100k 索引调度、 +Round Robin 和 Snapshot Apply 基准,并将 Git revision 与原始输出保存到 `dist/`。该基线不包含 +网络、TLS、上游 RTT、真实连接或多 Worker 协调,不能替代代表性集群报告。 - [ ] Confirm no TODO/TBD/placeholders, secrets, unbounded queues, high-cardinality metric labels, extraction Lease APIs, or conflicting maxSize semantics remain. diff --git a/docs/package_test.go b/docs/package_test.go index d239784..f08610d 100644 --- a/docs/package_test.go +++ b/docs/package_test.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "testing" ) @@ -18,6 +19,36 @@ type docsPackageManifest struct { } `json:"files"` } +func TestGatewayBenchmarkScriptIsBoundedAndWritesEvidence(t *testing.T) { + pwsh, err := exec.LookPath("pwsh") + if err != nil { + t.Skip("pwsh is required to exercise the gateway benchmark script") + } + repositoryRoot := repositoryRoot(t) + outputPath := filepath.Join(t.TempDir(), "gateway-benchmarks.txt") + command := exec.Command(pwsh, "-NoProfile", "-NonInteractive", "-File", filepath.Join(repositoryRoot, "scripts", "benchmark-gateway.ps1"), "-OutputPath", outputPath) + command.Dir = repositoryRoot + if output, err := command.CombinedOutput(); err != nil { + t.Fatalf("run gateway benchmarks: %v\n%s", err, output) + } + payload, err := os.ReadFile(outputPath) + if err != nil { + t.Fatalf("read benchmark evidence: %v", err) + } + evidence := string(payload) + for _, required := range []string{ + "BenchmarkAcquire100k(Indexed|RoutingRoundRobin)", + "BenchmarkStoreApply100k", + "-benchtime=1x", + "-count=1", + "revision:", + } { + if !strings.Contains(evidence, required) { + t.Errorf("benchmark evidence does not contain %q", required) + } + } +} + func TestPackageDocsCreatesTraceableArchive(t *testing.T) { pwsh, err := exec.LookPath("pwsh") if err != nil { diff --git a/docs/requirements/completion-audit.md b/docs/requirements/completion-audit.md index ae1340d..0bb87fb 100644 --- a/docs/requirements/completion-audit.md +++ b/docs/requirements/completion-audit.md @@ -96,6 +96,7 @@ docker compose config PASS kubectl kustomize PASS configuration examples 21/21 PASS Mermaid blocks 35 +bounded gateway microbenchmarks PASS (local baseline only) ``` Windows 环境为 `CGO_ENABLED=0` 且没有 C 编译器,`go test -race` 在本机未执行; diff --git a/docs/testing/strategy.md b/docs/testing/strategy.md index 75d730d..721f748 100644 --- a/docs/testing/strategy.md +++ b/docs/testing/strategy.md @@ -83,13 +83,19 @@ soak 测试单独标记,不混入快速单测。 ## 5. 当前本地微基准 -2026-07-30,Windows/amd64、Intel Core Ultra 7 155H: +2026-08-07,Windows/amd64、Go 1.26.4、Intel Core Ultra 7 155H,以固定 +`-count=1 -benchtime=1x` 运行: ```text -BenchmarkAcquire100kIndexed-22 1000000-1867125 893.9-1047 ns/op 256 B/op 2 allocs/op -BenchmarkStoreApply100k-22 1 518.7 ms/op 540 MB/op 3000887 allocs/op +BenchmarkAcquire100kIndexed-22 1 13100 ns/op 256 B/op 2 allocs/op +BenchmarkAcquire100kRoutingRoundRobin-22 1 45200 ns/op 13680 B/op 81 allocs/op +BenchmarkStoreApply100k-22 1 539017100 ns/op 684677920 B/op 3001735 allocs/op ``` +使用 `./scripts/benchmark-gateway.ps1` 可复现该固定单次基线,输出写入被 Git 忽略的 +`dist/gateway-benchmarks.txt` 并附带 revision。单次基准存在运行噪声,应只用于发现明显回归; +性能比较需要在同规格环境增加多次迭代后另行保存原始证据。 + `Acquire` 已使用 scheme/upstream/tag 索引,结果只代表本地选择和容量预留。 `Store.Apply` 属于冷路径且当前内存开销较高;运行态为防止旧快照在途连接超配, 暂不自动回收曾出现过的 Proxy ID,但注册表有 1,000,000 项硬上限,达到上限时 diff --git a/scripts/benchmark-gateway.ps1 b/scripts/benchmark-gateway.ps1 new file mode 100644 index 0000000..9e771e4 --- /dev/null +++ b/scripts/benchmark-gateway.ps1 @@ -0,0 +1,53 @@ +param( + [string]$OutputPath = "" +) + +$ErrorActionPreference = "Stop" + +$repositoryRoot = Split-Path -Parent $PSScriptRoot +if ([string]::IsNullOrWhiteSpace($OutputPath)) { + $OutputPath = Join-Path $repositoryRoot "dist/gateway-benchmarks.txt" +} +elseif (-not [System.IO.Path]::IsPathRooted($OutputPath)) { + $OutputPath = Join-Path $repositoryRoot $OutputPath +} +$OutputPath = [System.IO.Path]::GetFullPath($OutputPath) +if ([System.IO.Path]::GetExtension($OutputPath) -ne ".txt") { + throw "OutputPath must end in .txt" +} + +$revision = (& git -C $repositoryRoot rev-parse --verify HEAD 2>$null).Trim() +if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($revision)) { + throw "could not resolve the current Git revision" +} + +$outputDirectory = Split-Path -Parent $OutputPath +New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null +@( + "proxy-pool gateway microbenchmarks", + "revision: $revision", + "generated_at_utc: $([DateTime]::UtcNow.ToString('O'))", + "go_version: $((& go version).Trim())", + "" +) | Set-Content -LiteralPath $OutputPath -Encoding utf8 + +function Invoke-Benchmark { + param( + [string]$Package, + [string]$Pattern + ) + + $arguments = @("test", "-count=1", "-run", "^$", "-bench", $Pattern, "-benchtime=1x", "-benchmem", $Package) + "==> go $($arguments -join ' ')" | Tee-Object -FilePath $OutputPath -Append + $lines = & go @arguments 2>&1 + $exitCode = $LASTEXITCODE + $lines | Tee-Object -FilePath $OutputPath -Append + if ($exitCode -ne 0) { + throw "benchmark failed with exit code $exitCode" + } + "" | Tee-Object -FilePath $OutputPath -Append +} + +Invoke-Benchmark -Package "./internal/gateway/dispatch" -Pattern "BenchmarkAcquire100k(Indexed|RoutingRoundRobin)" +Invoke-Benchmark -Package "./internal/gateway/snapshot" -Pattern "^BenchmarkStoreApply100k$" +Write-Host "benchmark evidence: $OutputPath"