From de33eda846fb5d58447964b9efc97d45ccb30a13 Mon Sep 17 00:00:00 2001 From: k2safe Date: Tue, 22 Sep 2026 23:10:19 +0800 Subject: [PATCH] test: add configurable HTTP2 TLS soak --- CHANGELOG.md | 1 + transport/http2/soak_test.go | 152 +++++++++++++++++++++++++++++++++++ transport/native/HTTP2.md | 2 +- 3 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 transport/http2/soak_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index def2145..f92bd5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - 新增独立 `transport/http2` 实验 Runner:支持 h2c/TLS h2、连接上限、全局请求并发闸门、GOAWAY 排空、RST_STREAM 单流取消和生命周期统计;不改变 native HTTP/1.1 默认路径。 - HTTP/2 实验 Runner 增加标准 1 MiB 头部上限、默认 8 MiB 请求体上限、128 路同连接多路复用、2 MiB 慢读取流控验收和协议错误计数;已声明超限请求在进入业务 handler 前返回 413。 +- 新增可配置时长的 HTTP/2 TLS soak:默认 3 秒,Linux 验收可通过 `GO_OMNI_HTTP2_SOAK_DURATION=30m` 覆盖,混合验证 128 路 stream、慢 handler、请求体、Keep-Alive 和退出回收。 - native TLS 明确限制 ALPN 为 `http/1.1`:缺省自动宣告该协议,误配置 `h2` 等未实现协议时在启动阶段报错。 - native `Server.Stats()` 新增连接和请求过载拒绝计数;新增可信代理配置,访问日志只在 socket 对端属于配置网段时解析 `X-Forwarded-For`。 - 新增 transport-neutral `metrics` 包,支持请求数、错误数、in-flight、累计耗时和状态分布,并可直接注册 `/metrics` 接口。 diff --git a/transport/http2/soak_test.go b/transport/http2/soak_test.go new file mode 100644 index 0000000..a7d85ae --- /dev/null +++ b/transport/http2/soak_test.go @@ -0,0 +1,152 @@ +package http2 + +import ( + "context" + "crypto/tls" + "io" + "net" + "net/http" + "os" + "sync" + "sync/atomic" + "testing" + "time" + + xhttp2 "golang.org/x/net/http2" +) + +// TestTLSSoak is short by default so it remains part of normal package +// verification. Linux acceptance runs set GO_OMNI_HTTP2_SOAK_DURATION=30m. +func TestTLSSoak(t *testing.T) { + duration := 3 * time.Second + if value := os.Getenv("GO_OMNI_HTTP2_SOAK_DURATION"); value != "" { + parsed, err := time.ParseDuration(value) + if err != nil || parsed <= 0 { + t.Fatalf("invalid GO_OMNI_HTTP2_SOAK_DURATION %q: %v", value, err) + } + duration = parsed + } + + certificate, roots := testCertificate(t) + server := New(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + switch request.URL.Path { + case "/slow": + timer := time.NewTimer(10 * time.Millisecond) + defer timer.Stop() + select { + case <-timer.C: + case <-request.Context().Done(): + return + } + case "/body": + if _, err := io.Copy(io.Discard, request.Body); err != nil { + http.Error(writer, err.Error(), http.StatusBadRequest) + return + } + } + _, _ = io.WriteString(writer, request.URL.Path) + })) + server.MaxConnections = 8 + server.MaxConcurrentRequests = 128 + server.MaxConcurrentStreams = 128 + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + done := make(chan error, 1) + go func() { + done <- server.ServeTLS(context.Background(), listener, &tls.Config{ + Certificates: []tls.Certificate{certificate}, + MinVersion: tls.VersionTLS12, + }) + }() + + transport := &xhttp2.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: roots, + ServerName: "localhost", + MinVersion: tls.VersionTLS12, + NextProtos: []string{"h2"}, + }, + StrictMaxConcurrentStreams: true, + } + client := &http.Client{Transport: transport, Timeout: 3 * time.Second} + + var requests, failures atomic.Uint64 + deadline := time.Now().Add(duration) + var workers sync.WaitGroup + for worker := 0; worker < 128; worker++ { + workers.Add(1) + go func(worker int) { + defer workers.Done() + for time.Now().Before(deadline) { + path := "/health" + method := http.MethodGet + var body io.Reader + if worker%32 == 0 { + path = "/slow" + } + if worker%64 == 0 { + path = "/body" + method = http.MethodPost + body = &repeatingReader{remaining: 64 << 10} + } + request, err := http.NewRequestWithContext(context.Background(), method, "https://"+listener.Addr().String()+path, body) + if err != nil { + failures.Add(1) + continue + } + response, err := client.Do(request) + if err != nil { + failures.Add(1) + continue + } + _, readErr := io.Copy(io.Discard, response.Body) + response.Body.Close() + if readErr != nil || response.StatusCode != http.StatusOK { + failures.Add(1) + continue + } + requests.Add(1) + } + }(worker) + } + workers.Wait() + transport.CloseIdleConnections() + + shutdownContext, cancel := context.WithTimeout(context.Background(), 5*time.Second) + shutdownErr := server.Shutdown(shutdownContext) + cancel() + if shutdownErr != nil { + t.Fatalf("TLS soak shutdown: %v", shutdownErr) + } + if err := <-done; err != nil { + t.Fatalf("TLS soak serve: %v", err) + } + if failures.Load() != 0 || requests.Load() == 0 { + t.Fatalf("TLS soak requests=%d failures=%d stats=%+v", requests.Load(), failures.Load(), server.Stats()) + } + if stats := server.Stats(); stats.ActiveConnections != 0 || stats.ActiveRequests != 0 { + t.Fatalf("TLS soak did not reclaim resources: %+v", stats) + } +} + +type repeatingReader struct { + remaining int +} + +func (r *repeatingReader) Read(buffer []byte) (int, error) { + if r.remaining == 0 { + return 0, io.EOF + } + if len(buffer) > r.remaining { + buffer = buffer[:r.remaining] + } + for index := range buffer { + buffer[index] = 'x' + } + r.remaining -= len(buffer) + return len(buffer), nil +} + +func (r *repeatingReader) Close() error { return nil } diff --git a/transport/native/HTTP2.md b/transport/native/HTTP2.md index 15a30e6..2798bee 100644 --- a/transport/native/HTTP2.md +++ b/transport/native/HTTP2.md @@ -2,7 +2,7 @@ 当前 `transport/native` 是经过 Linux 长连接和反向代理验收的 HTTP/1.1 实现。HTTP/2 暂不直接打开,原因是它把一个 TCP 连接变成多个独立请求流,连接级的 Keep-Alive、超时和并发限制不能直接复用现有 HTTP/1.1 状态机。 -当前已提供独立的 `transport/http2` 实验 Runner。它已经验证 TLS/h2、h2c、本地连接和请求闸门、GOAWAY 排空以及 RST_STREAM 取消;这些结果用于验证生命周期设计,不代表 native 已经启用 HTTP/2。实验 Runner 还覆盖了默认 1 MiB 头部上限、8 MiB 请求体上限、128 路同连接 stream 多路复用,以及 2 MiB 慢读取 body 在窗口耗尽后的恢复。 +当前已提供独立的 `transport/http2` 实验 Runner。它已经验证 TLS/h2、h2c、本地连接和请求闸门、GOAWAY 排空以及 RST_STREAM 取消;这些结果用于验证生命周期设计,不代表 native 已经启用 HTTP/2。实验 Runner 还覆盖了默认 1 MiB 头部上限、8 MiB 请求体上限、128 路同连接 stream 多路复用,以及 2 MiB 慢读取 body 在窗口耗尽后的恢复。`TestTLSSoak` 默认运行 3 秒,Linux 长测通过 `GO_OMNI_HTTP2_SOAK_DURATION=30m go test -race ./transport/http2 -run '^TestTLSSoak$'` 启动。 ## 目标边界