feat: report loadgen rate dispatch loss

This commit is contained in:
youfak 2026-08-07 15:42:28 +08:00
parent 26572da97e
commit 1367f1b11f
5 changed files with 54 additions and 8 deletions

View File

@ -258,9 +258,12 @@ go run ./cmd/proxy-loadgen `
```
命令输出 JSON 报告,包含成功/失败分类、`TunnelsEstablished`、
`ExtractResponsesValidated`、`ExtractReturned`、`ExtractValidationFailures`、固定内存的
`ExtractResponsesValidated`、`ExtractReturned`、`ExtractValidationFailures`、`RateStartsGenerated`、
`RateStartsDropped`、固定内存的
连接握手延迟分位上界、吞吐和 Go 运行时内存/GC 快照。它不会输出提取响应中的地址或
凭据,也不构成 100,000 QPS 证明。
凭据,也不构成 100,000 QPS 证明。限速场景以 `Requests` 表示实际发起数;当
`RateStartsDropped` 非零时,目标速率受压测端并发容量或时间窗限制,报告吞吐不得
标注为已达到配置的 `-rate`
## 关键配置与入口

View File

@ -18,7 +18,10 @@ func TestExecutePassesBoundedWorkloadAndWritesJSON(t *testing.T) {
"-requests", "3", "-concurrency", "2", "-timeout", "2s", "-header", "X-Run: fixed", "-body", `{"count":1}`,
}, func(_ context.Context, options loadgen.Options) (loadgen.Report, error) {
received = options
return loadgen.Report{Requests: 3, Completed: 3, Succeeded: 3, Duration: time.Second}, nil
return loadgen.Report{
Requests: 3, Completed: 3, Succeeded: 3, Duration: time.Second,
RateStartsGenerated: 3, RateStartsDropped: 1,
}, nil
}, &stdout, &stderr)
if code != 0 || received.TargetURL != "https://target.example/path" || received.ProxyURL != "http://gateway.example:8080" ||
received.Method != "post" || received.Requests != 3 || received.Duration != 0 || received.Concurrency != 2 ||
@ -26,7 +29,8 @@ func TestExecutePassesBoundedWorkloadAndWritesJSON(t *testing.T) {
t.Fatalf("execute() = %d; options=%+v stderr=%q", code, received, stderr.String())
}
var report loadgen.Report
if err := json.Unmarshal(stdout.Bytes(), &report); err != nil || report.Succeeded != 3 {
if err := json.Unmarshal(stdout.Bytes(), &report); err != nil || report.Succeeded != 3 ||
report.RateStartsGenerated != 3 || report.RateStartsDropped != 1 {
t.Fatalf("JSON report = (%+v, %v)", report, err)
}
}

View File

@ -182,6 +182,7 @@ Admin 应用层测试覆盖 typed-nil 依赖、Actor/SourceIP 映射、Routing C
配置Snapshot 规模、Proxy 容量、路由、重试、日志级别
场景:协议、连接复用、响应体、持续时间、升压曲线、故障注入
结果QPS、建连速率、active、p50/p95/p99、错误、CPU、RSS、GC、FD、网络
限速:配置 rate、实际 Requests、RateStartsGenerated、RateStartsDropped
不变量capacity、ownership、extraction、idempotency、admin audit、reserve 检查结果
结论:通过/失败,以及适用边界
```

View File

@ -75,6 +75,8 @@ type Report struct {
ExtractReturned uint64
ExtractValidationFailures uint64
Throughput float64
RateStartsGenerated uint64
RateStartsDropped uint64
Latency LatencyReport
Runtime RuntimeReport
}
@ -110,6 +112,8 @@ type counters struct {
extractResponsesValidated atomic.Uint64
extractReturned atomic.Uint64
extractValidationFailures atomic.Uint64
rateStartsGenerated atomic.Uint64
rateStartsDropped atomic.Uint64
latency latencyHistogram
}
@ -154,7 +158,7 @@ func Run(ctx context.Context, options Options) (Report, error) {
return report(started, normalized.Scenario, stats), err
}
} else {
runForDuration(ctx, normalized, execute)
runForDuration(ctx, normalized, execute, stats)
}
return report(started, normalized.Scenario, stats), nil
}
@ -285,7 +289,7 @@ func runFixed(ctx context.Context, options Options, execute func(context.Context
workers.Wait()
}
func runForDuration(ctx context.Context, options Options, execute func(context.Context)) {
func runForDuration(ctx context.Context, options Options, execute func(context.Context), stats *counters) {
workloadContext, cancel := context.WithTimeout(ctx, options.Duration)
defer cancel()
if options.Rate == 0 {
@ -334,14 +338,20 @@ func runForDuration(ctx context.Context, options Options, execute func(context.C
last = now
tokens := int(credit)
credit -= float64(tokens)
if tokens <= 0 {
continue
}
stats.rateStartsGenerated.Add(uint64(tokens))
if tokens > options.Concurrency {
stats.rateStartsDropped.Add(uint64(tokens - options.Concurrency))
tokens = options.Concurrency
credit = 0
}
for range tokens {
for index := 0; index < tokens; index++ {
select {
case jobs <- struct{}{}:
case <-workloadContext.Done():
stats.rateStartsDropped.Add(uint64(tokens - index))
workers.Wait()
return
}
@ -655,7 +665,9 @@ func report(started time.Time, scenario Scenario, stats *counters) Report {
Status5xx: stats.status5xx.Load(), TimeoutErrors: stats.timeoutErrors.Load(), RequestErrors: stats.requestErrors.Load(),
TunnelsEstablished: stats.tunnelsEstablished.Load(),
ExtractResponsesValidated: stats.extractResponsesValidated.Load(), ExtractReturned: stats.extractReturned.Load(),
ExtractValidationFailures: stats.extractValidationFailures.Load(), Latency: stats.latency.Report(completed),
ExtractValidationFailures: stats.extractValidationFailures.Load(),
RateStartsGenerated: stats.rateStartsGenerated.Load(), RateStartsDropped: stats.rateStartsDropped.Load(),
Latency: stats.latency.Report(completed),
Runtime: RuntimeReport{NumCPU: runtime.NumCPU(), Goroutines: runtime.NumGoroutine(), AllocBytes: memory.Alloc, SysBytes: memory.Sys, GCCount: memory.NumGC},
}
if duration > 0 {

View File

@ -71,6 +71,32 @@ func TestRunTimeBoxedRateIsBounded(t *testing.T) {
}
}
func TestRunReportsRateStartsDroppedByConcurrency(t *testing.T) {
started := make(chan struct{}, 1)
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
select {
case started <- struct{}{}:
default:
}
<-request.Context().Done()
response.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
report, err := Run(context.Background(), Options{
TargetURL: server.URL, Duration: 80 * time.Millisecond, Rate: 100_000, Concurrency: 1, RequestTimeout: time.Second,
})
select {
case <-started:
default:
t.Fatal("rate-limited workload did not start a request")
}
if err != nil || report.RateStartsGenerated <= report.Requests || report.RateStartsDropped == 0 ||
report.RateStartsDropped > report.RateStartsGenerated {
t.Fatalf("Run() = (%+v, %v)", report, err)
}
}
func TestRunExecutesBoundedCONNECTTunnelWorkload(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {