feat: support load generator request bodies
Some checks are pending
ci / proto (push) Waiting to run
ci / test (ubuntu-latest) (push) Waiting to run
ci / test (windows-latest) (push) Waiting to run
ci / race (push) Waiting to run
ci / integration (push) Waiting to run

This commit is contained in:
youfak 2026-08-02 08:21:15 +08:00
parent 7ebd655936
commit ec2ae8838a
8 changed files with 38 additions and 11 deletions

View File

@ -184,8 +184,19 @@ go run ./cmd/proxy-loadgen `
``` ```
使用 `-duration 30s -rate 5000` 可运行限速场景;省略 `-rate` 时固定数量 worker 使用 `-duration 30s -rate 5000` 可运行限速场景;省略 `-rate` 时固定数量 worker
会饱和发送。命令输出 JSON 报告,包含成功/失败分类、固定内存的延迟分位上界、吞吐和 会饱和发送。`-method`、重复的 `-header``-body` 可组合用于 Distribution 的提取接口:
Go 运行时内存/GC 快照。它不包含长连接保持或 Extract 场景,也不构成 100,000 QPS 证明。
```powershell
go run ./cmd/proxy-loadgen `
-target http://CONTROLLER_HOST:8081/api/v1/proxies/extract `
-method POST -header "Content-Type: application/json" `
-header "X-API-Key: DISTRIBUTION_API_KEY" -body '{"count":1}' `
-requests 1000 -concurrency 32 -timeout 10s
```
命令输出 JSON 报告,包含成功/失败分类、固定内存的延迟分位上界、吞吐和 Go
运行时内存/GC 快照。它尚不包含长连接保持、Extract 的专用数据准备/结果校验,
也不构成 100,000 QPS 证明。
## 关键配置与入口 ## 关键配置与入口

View File

@ -31,6 +31,7 @@ func execute(ctx context.Context, args []string, run loadRun, stdout, stderr io.
targetURL := flags.String("target", "", "HTTP or HTTPS target URL") targetURL := flags.String("target", "", "HTTP or HTTPS target URL")
proxyURL := flags.String("proxy", "", "optional HTTP or HTTPS forward proxy URL") proxyURL := flags.String("proxy", "", "optional HTTP or HTTPS forward proxy URL")
method := flags.String("method", http.MethodGet, "HTTP method") method := flags.String("method", http.MethodGet, "HTTP method")
body := flags.String("body", "", "UTF-8 request body")
requests := flags.Int("requests", 0, "fixed request count; mutually exclusive with -duration") requests := flags.Int("requests", 0, "fixed request count; mutually exclusive with -duration")
duration := flags.Duration("duration", 0, "time-boxed workload duration") duration := flags.Duration("duration", 0, "time-boxed workload duration")
rate := flags.Int("rate", 0, "maximum request starts per second for -duration; zero saturates workers") rate := flags.Int("rate", 0, "maximum request starts per second for -duration; zero saturates workers")
@ -54,7 +55,7 @@ func execute(ctx context.Context, args []string, run loadRun, stdout, stderr io.
return 2 return 2
} }
report, err := run(ctx, loadgen.Options{ report, err := run(ctx, loadgen.Options{
TargetURL: *targetURL, ProxyURL: *proxyURL, Method: *method, Headers: parsedHeaders, TargetURL: *targetURL, ProxyURL: *proxyURL, Method: *method, Headers: parsedHeaders, RequestBody: []byte(*body),
Requests: *requests, Duration: *duration, Rate: *rate, Concurrency: *concurrency, RequestTimeout: *timeout, Requests: *requests, Duration: *duration, Rate: *rate, Concurrency: *concurrency, RequestTimeout: *timeout,
}) })
if err != nil { if err != nil {

View File

@ -15,14 +15,14 @@ func TestExecutePassesBoundedWorkloadAndWritesJSON(t *testing.T) {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
code := execute(context.Background(), []string{ code := execute(context.Background(), []string{
"-target", "https://target.example/path", "-proxy", "http://gateway.example:8080", "-method", "post", "-target", "https://target.example/path", "-proxy", "http://gateway.example:8080", "-method", "post",
"-requests", "3", "-concurrency", "2", "-timeout", "2s", "-header", "X-Run: fixed", "-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}, 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 ||
received.Headers.Get("X-Run") != "fixed" || stderr.Len() != 0 { received.Headers.Get("X-Run") != "fixed" || string(received.RequestBody) != `{"count":1}` || stderr.Len() != 0 {
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

View File

@ -79,8 +79,8 @@ Provider、Pool、Routing、Distribution 在首版需要共享事务和一致性
### 3.4 proxy-loadgen ### 3.4 proxy-loadgen
- 当前实现 HTTP 请求场景:固定请求数或固定时长,受限并发与可选目标 QPSHTTPS - 当前实现 HTTP 请求场景:固定请求数或固定时长,受限并发与可选目标 QPS可配置方法、
目标经 HTTP Gateway 时由 Transport 走 CONNECT。 重复请求头和请求体。HTTPS 目标经 HTTP Gateway 时由 Transport 走 CONNECT。
- 输出场景、延迟分位上界、错误分类、吞吐和 Go 内存/GC 快照。CONNECT 长连接、 - 输出场景、延迟分位上界、错误分类、吞吐和 Go 内存/GC 快照。CONNECT 长连接、
Extract 并发、进程 CPU/RSS/句柄与网络采样仍需补齐。 Extract 并发、进程 CPU/RSS/句柄与网络采样仍需补齐。

View File

@ -54,7 +54,7 @@ deadline 内执行 HTTP/HTTPS/SOCKS5 BASIC、EGRESS 和 TARGET 探测并微批
### proxy-loadgen ### proxy-loadgen
负载工具当前生成有界 HTTP 请求,支持经 Gateway 请求 HTTPS 目标、固定请求数或时长、 负载工具当前生成有界 HTTP 请求,支持经 Gateway 请求 HTTPS 目标、固定请求数或时长、
目标 QPS、连接复用和 JSON 指标输出。延迟统计使用固定大小直方图,不会因长时间高 QPS 目标 QPS、可重复请求头和请求体、连接复用和 JSON 指标输出。延迟统计使用固定大小直方图,不会因长时间高 QPS
运行积压样本。CONNECT 长连接、Extract 和故障注入场景仍待补齐;它是 100k QPS 结论的 运行积压样本。CONNECT 长连接、Extract 和故障注入场景仍待补齐;它是 100k QPS 结论的
证据工具,不是业务进程。 证据工具,不是业务进程。

View File

@ -295,7 +295,8 @@ Controller 即可生效Redis 任务存储仍仅承载 BASIC未扩展 EGRES
补充进度2026-08-02已新增 `proxy-loadgen` HTTP 场景。固定请求数和固定时长两种 补充进度2026-08-02已新增 `proxy-loadgen` HTTP 场景。固定请求数和固定时长两种
模式均通过固定 worker 数与有界派发通道执行,可选 QPS 限速;报告使用固定大小延迟直方图, 模式均通过固定 worker 数与有界派发通道执行,可选 QPS 限速;报告使用固定大小延迟直方图,
输出状态分类、吞吐和 Go 内存/GC 快照。CONNECT 长连接、Extract、故障注入以及代表性集群 输出状态分类、吞吐和 Go 内存/GC 快照。通用 `method/header/body` 参数可覆盖 Distribution
提取 HTTP 请求CONNECT 长连接、Extract 的专用数据准备与结果校验、故障注入以及代表性集群
报告仍未实现。 报告仍未实现。
## Task 12: Machine-readable Contracts ## Task 12: Machine-readable Contracts

View File

@ -3,6 +3,7 @@
package loadgen package loadgen
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"io" "io"
@ -27,6 +28,7 @@ type Options struct {
ProxyURL string ProxyURL string
Method string Method string
Headers http.Header Headers http.Header
RequestBody []byte
Requests int Requests int
Duration time.Duration Duration time.Duration
Rate int Rate int
@ -110,6 +112,8 @@ func Run(ctx context.Context, options Options) (Report, error) {
func newClient(options Options) (*http.Client, Options, error) { func newClient(options Options) (*http.Client, Options, error) {
normalized := options normalized := options
normalized.Method = strings.ToUpper(strings.TrimSpace(normalized.Method)) normalized.Method = strings.ToUpper(strings.TrimSpace(normalized.Method))
normalized.Headers = normalized.Headers.Clone()
normalized.RequestBody = bytes.Clone(normalized.RequestBody)
if normalized.Method == "" { if normalized.Method == "" {
normalized.Method = http.MethodGet normalized.Method = http.MethodGet
} }
@ -245,7 +249,12 @@ func executeOne(ctx context.Context, client *http.Client, options Options, stats
started := time.Now() started := time.Now()
requestContext, cancel := context.WithTimeout(ctx, options.RequestTimeout) requestContext, cancel := context.WithTimeout(ctx, options.RequestTimeout)
defer cancel() defer cancel()
request, err := http.NewRequestWithContext(requestContext, options.Method, options.TargetURL, nil) request, err := http.NewRequestWithContext(
requestContext,
options.Method,
options.TargetURL,
bytes.NewReader(options.RequestBody),
)
if err != nil { if err != nil {
stats.failed.Add(1) stats.failed.Add(1)
stats.requestErrors.Add(1) stats.requestErrors.Add(1)

View File

@ -3,6 +3,7 @@ package loadgen
import ( import (
"context" "context"
"errors" "errors"
"io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"sync/atomic" "sync/atomic"
@ -16,13 +17,17 @@ func TestRunExecutesFixedBoundedHTTPWorkload(t *testing.T) {
if request.Method != http.MethodPost || request.Header.Get("X-Scenario") != "fixed" { if request.Method != http.MethodPost || request.Header.Get("X-Scenario") != "fixed" {
t.Errorf("request = %s %q", request.Method, request.Header.Get("X-Scenario")) t.Errorf("request = %s %q", request.Method, request.Header.Get("X-Scenario"))
} }
body, err := io.ReadAll(request.Body)
if err != nil || string(body) != `{"count":1}` {
t.Errorf("request body = %q, error = %v", body, err)
}
requests.Add(1) requests.Add(1)
response.WriteHeader(http.StatusNoContent) response.WriteHeader(http.StatusNoContent)
})) }))
defer server.Close() defer server.Close()
report, err := Run(context.Background(), Options{ report, err := Run(context.Background(), Options{
TargetURL: server.URL, Method: http.MethodPost, Headers: http.Header{"X-Scenario": {"fixed"}}, TargetURL: server.URL, Method: http.MethodPost, Headers: http.Header{"X-Scenario": {"fixed"}}, RequestBody: []byte(`{"count":1}`),
Requests: 12, Concurrency: 3, RequestTimeout: time.Second, Requests: 12, Concurrency: 3, RequestTimeout: time.Second,
}) })
if err != nil || report.Requests != 12 || report.Completed != 12 || report.Succeeded != 12 || report.Failed != 0 || if err != nil || report.Requests != 12 || report.Completed != 12 || report.Succeeded != 12 || report.Failed != 0 ||