feat: support load generator request bodies
This commit is contained in:
parent
7ebd655936
commit
ec2ae8838a
15
README.md
15
README.md
@ -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 证明。
|
||||||
|
|
||||||
## 关键配置与入口
|
## 关键配置与入口
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -79,8 +79,8 @@ Provider、Pool、Routing、Distribution 在首版需要共享事务和一致性
|
|||||||
|
|
||||||
### 3.4 proxy-loadgen
|
### 3.4 proxy-loadgen
|
||||||
|
|
||||||
- 当前实现 HTTP 请求场景:固定请求数或固定时长,受限并发与可选目标 QPS;HTTPS
|
- 当前实现 HTTP 请求场景:固定请求数或固定时长,受限并发与可选目标 QPS;可配置方法、
|
||||||
目标经 HTTP Gateway 时由 Transport 走 CONNECT。
|
重复请求头和请求体。HTTPS 目标经 HTTP Gateway 时由 Transport 走 CONNECT。
|
||||||
- 输出场景、延迟分位上界、错误分类、吞吐和 Go 内存/GC 快照。CONNECT 长连接、
|
- 输出场景、延迟分位上界、错误分类、吞吐和 Go 内存/GC 快照。CONNECT 长连接、
|
||||||
Extract 并发、进程 CPU/RSS/句柄与网络采样仍需补齐。
|
Extract 并发、进程 CPU/RSS/句柄与网络采样仍需补齐。
|
||||||
|
|
||||||
|
|||||||
@ -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 结论的
|
||||||
证据工具,不是业务进程。
|
证据工具,不是业务进程。
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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 ||
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user