feat: report loadgen rate dispatch loss
This commit is contained in:
parent
26572da97e
commit
1367f1b11f
@ -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`。
|
||||
|
||||
## 关键配置与入口
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 检查结果
|
||||
结论:通过/失败,以及适用边界
|
||||
```
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user