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`、
|
命令输出 JSON 报告,包含成功/失败分类、`TunnelsEstablished`、
|
||||||
`ExtractResponsesValidated`、`ExtractReturned`、`ExtractValidationFailures`、固定内存的
|
`ExtractResponsesValidated`、`ExtractReturned`、`ExtractValidationFailures`、`RateStartsGenerated`、
|
||||||
|
`RateStartsDropped`、固定内存的
|
||||||
连接握手延迟分位上界、吞吐和 Go 运行时内存/GC 快照。它不会输出提取响应中的地址或
|
连接握手延迟分位上界、吞吐和 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}`,
|
"-requests", "3", "-concurrency", "2", "-timeout", "2s", "-header", "X-Run: fixed", "-body", `{"count":1}`,
|
||||||
}, func(_ context.Context, options loadgen.Options) (loadgen.Report, error) {
|
}, func(_ context.Context, options loadgen.Options) (loadgen.Report, error) {
|
||||||
received = options
|
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)
|
}, &stdout, &stderr)
|
||||||
if code != 0 || received.TargetURL != "https://target.example/path" || received.ProxyURL != "http://gateway.example:8080" ||
|
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 ||
|
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())
|
t.Fatalf("execute() = %d; options=%+v stderr=%q", code, received, stderr.String())
|
||||||
}
|
}
|
||||||
var report loadgen.Report
|
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)
|
t.Fatalf("JSON report = (%+v, %v)", report, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -182,6 +182,7 @@ Admin 应用层测试覆盖 typed-nil 依赖、Actor/SourceIP 映射、Routing C
|
|||||||
配置:Snapshot 规模、Proxy 容量、路由、重试、日志级别
|
配置:Snapshot 规模、Proxy 容量、路由、重试、日志级别
|
||||||
场景:协议、连接复用、响应体、持续时间、升压曲线、故障注入
|
场景:协议、连接复用、响应体、持续时间、升压曲线、故障注入
|
||||||
结果:QPS、建连速率、active、p50/p95/p99、错误、CPU、RSS、GC、FD、网络
|
结果:QPS、建连速率、active、p50/p95/p99、错误、CPU、RSS、GC、FD、网络
|
||||||
|
限速:配置 rate、实际 Requests、RateStartsGenerated、RateStartsDropped
|
||||||
不变量:capacity、ownership、extraction、idempotency、admin audit、reserve 检查结果
|
不变量:capacity、ownership、extraction、idempotency、admin audit、reserve 检查结果
|
||||||
结论:通过/失败,以及适用边界
|
结论:通过/失败,以及适用边界
|
||||||
```
|
```
|
||||||
|
|||||||
@ -75,6 +75,8 @@ type Report struct {
|
|||||||
ExtractReturned uint64
|
ExtractReturned uint64
|
||||||
ExtractValidationFailures uint64
|
ExtractValidationFailures uint64
|
||||||
Throughput float64
|
Throughput float64
|
||||||
|
RateStartsGenerated uint64
|
||||||
|
RateStartsDropped uint64
|
||||||
Latency LatencyReport
|
Latency LatencyReport
|
||||||
Runtime RuntimeReport
|
Runtime RuntimeReport
|
||||||
}
|
}
|
||||||
@ -110,6 +112,8 @@ type counters struct {
|
|||||||
extractResponsesValidated atomic.Uint64
|
extractResponsesValidated atomic.Uint64
|
||||||
extractReturned atomic.Uint64
|
extractReturned atomic.Uint64
|
||||||
extractValidationFailures atomic.Uint64
|
extractValidationFailures atomic.Uint64
|
||||||
|
rateStartsGenerated atomic.Uint64
|
||||||
|
rateStartsDropped atomic.Uint64
|
||||||
latency latencyHistogram
|
latency latencyHistogram
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +158,7 @@ func Run(ctx context.Context, options Options) (Report, error) {
|
|||||||
return report(started, normalized.Scenario, stats), err
|
return report(started, normalized.Scenario, stats), err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
runForDuration(ctx, normalized, execute)
|
runForDuration(ctx, normalized, execute, stats)
|
||||||
}
|
}
|
||||||
return report(started, normalized.Scenario, stats), nil
|
return report(started, normalized.Scenario, stats), nil
|
||||||
}
|
}
|
||||||
@ -285,7 +289,7 @@ func runFixed(ctx context.Context, options Options, execute func(context.Context
|
|||||||
workers.Wait()
|
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)
|
workloadContext, cancel := context.WithTimeout(ctx, options.Duration)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if options.Rate == 0 {
|
if options.Rate == 0 {
|
||||||
@ -334,14 +338,20 @@ func runForDuration(ctx context.Context, options Options, execute func(context.C
|
|||||||
last = now
|
last = now
|
||||||
tokens := int(credit)
|
tokens := int(credit)
|
||||||
credit -= float64(tokens)
|
credit -= float64(tokens)
|
||||||
|
if tokens <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
stats.rateStartsGenerated.Add(uint64(tokens))
|
||||||
if tokens > options.Concurrency {
|
if tokens > options.Concurrency {
|
||||||
|
stats.rateStartsDropped.Add(uint64(tokens - options.Concurrency))
|
||||||
tokens = options.Concurrency
|
tokens = options.Concurrency
|
||||||
credit = 0
|
credit = 0
|
||||||
}
|
}
|
||||||
for range tokens {
|
for index := 0; index < tokens; index++ {
|
||||||
select {
|
select {
|
||||||
case jobs <- struct{}{}:
|
case jobs <- struct{}{}:
|
||||||
case <-workloadContext.Done():
|
case <-workloadContext.Done():
|
||||||
|
stats.rateStartsDropped.Add(uint64(tokens - index))
|
||||||
workers.Wait()
|
workers.Wait()
|
||||||
return
|
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(),
|
Status5xx: stats.status5xx.Load(), TimeoutErrors: stats.timeoutErrors.Load(), RequestErrors: stats.requestErrors.Load(),
|
||||||
TunnelsEstablished: stats.tunnelsEstablished.Load(),
|
TunnelsEstablished: stats.tunnelsEstablished.Load(),
|
||||||
ExtractResponsesValidated: stats.extractResponsesValidated.Load(), ExtractReturned: stats.extractReturned.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},
|
Runtime: RuntimeReport{NumCPU: runtime.NumCPU(), Goroutines: runtime.NumGoroutine(), AllocBytes: memory.Alloc, SysBytes: memory.Sys, GCCount: memory.NumGC},
|
||||||
}
|
}
|
||||||
if duration > 0 {
|
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) {
|
func TestRunExecutesBoundedCONNECTTunnelWorkload(t *testing.T) {
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user