From e6c82a01feab281e5b1dc181cd4d8afab821a19c Mon Sep 17 00:00:00 2001 From: k2safe Date: Mon, 21 Sep 2026 05:13:51 +0800 Subject: [PATCH] test: add TLS performance matrix --- PERFORMANCE.md | 1 + internal/perfcheck/main.go | 102 +++++++++++++++++++++++++++----- internal/perfcheck/main_test.go | 12 ++++ scripts/verifyPerformance.py | 3 + transport/native/server_test.go | 30 ++++++++++ 5 files changed, 133 insertions(+), 15 deletions(-) diff --git a/PERFORMANCE.md b/PERFORMANCE.md index 89beb47..6dbf7aa 100644 --- a/PERFORMANCE.md +++ b/PERFORMANCE.md @@ -50,6 +50,7 @@ go run ./internal/perfcheck --duration 2s --count 3 --output /tmp/goOmniPerf.jso - 服务端 `runtime.MemStats` 通过独立 stdin/stdout 控制通道读取,客户端内存不计入。累计分配差值包含少量采样开销。堆峰值每 200 ms 采样,可能错过瞬时峰值;这些指标不是进程 RSS。 - 空闲场景在所有连接完成健康请求后做 GC 再采样。新增堆内存包括连接、缓冲区及分摊的运行时开销。`after` 为客户端断开并排空后一次 GC,`afterReclaim` 为第二次 GC,用于观察对象池缓存释放。 - native 配置上限为 8,192 连接 / 4,096 活跃请求;本轮并发低于上限,未验证过载保护容量。标准库没有同样的并发闸门;比较包括各自真实实现的开销。 +- 可用 `python3 scripts/verifyPerformance.py --load --tls --duration 2s` 运行 TLS 版本的独立进程矩阵。该模式在每个子进程生成短期自签名证书,客户端仅用于本机基准验证;本报告的历史表格仍是无 TLS 数据。 ## benchmark 口径修正 diff --git a/internal/perfcheck/main.go b/internal/perfcheck/main.go index 2cac2f0..f382c28 100644 --- a/internal/perfcheck/main.go +++ b/internal/perfcheck/main.go @@ -5,11 +5,18 @@ package main import ( "bufio" "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "encoding/json" "errors" "flag" "fmt" "io" + "math/big" "math/bits" "net" "net/http" @@ -71,6 +78,7 @@ type report struct { Platform string `json:"platform"` GOMAXPROCS int `json:"gomaxprocs"` Duration string `json:"duration"` + TLS bool `json:"tls"` Notes []string `json:"notes"` Results []result `json:"results"` } @@ -102,8 +110,38 @@ func (c *trackedConn) Close() error { return err } +func testTLSConfig() (*tls.Config, error) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 120)) + if err != nil { + return nil, err + } + now := time.Now() + template := &x509.Certificate{ + SerialNumber: serial, + Subject: pkix.Name{CommonName: "localhost"}, + NotBefore: now.Add(-time.Minute), + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + DNSNames: []string{"localhost"}, + } + der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) + if err != nil { + return nil, err + } + return &tls.Config{ + Certificates: []tls.Certificate{{Certificate: [][]byte{der}, PrivateKey: key}}, + MinVersion: tls.VersionTLS12, + }, nil +} + func main() { serverMode := flag.String("server", "", "internal server subprocess: nethttp or native") + tlsEnabled := flag.Bool("tls", false, "run the comparison over TLS") duration := flag.Duration("duration", 2*time.Second, "measured duration per traffic case") concurrencyText := flag.String("concurrency", "1,32,128", "traffic concurrency levels") idleText := flag.String("idle", "128,1024", "established idle Keep-Alive connection levels") @@ -112,9 +150,9 @@ func main() { flag.Parse() var err error if *serverMode != "" { - err = serve(*serverMode) + err = serve(*serverMode, *tlsEnabled) } else { - err = compare(*duration, *concurrencyText, *idleText, *runs, *output) + err = compare(*duration, *concurrencyText, *idleText, *runs, *output, *tlsEnabled) } if err != nil { fmt.Fprintln(os.Stderr, err) @@ -122,12 +160,21 @@ func main() { } } -func serve(adapter string) error { +func serve(adapter string, tlsEnabled bool) error { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { return err } tracked := &trackedListener{Listener: listener} + var serveListener net.Listener = tracked + if tlsEnabled { + config, configErr := testTLSConfig() + if configErr != nil { + listener.Close() + return configErr + } + serveListener = tls.NewListener(tracked, config) + } application := omni.New() application.GET("/health", func(c *omni.Context) error { return c.OK(map[string]string{"status": "ok"}) @@ -140,7 +187,7 @@ func serve(adapter string) error { server.MaxConnections = 8192 server.MaxConcurrentRequests = 4096 shutdown = server.Shutdown - go func() { done <- server.Serve(context.Background(), tracked) }() + go func() { done <- server.Serve(context.Background(), serveListener) }() case "nethttp": server := &http.Server{ Handler: application, ReadHeaderTimeout: 5 * time.Second, @@ -149,7 +196,7 @@ func serve(adapter string) error { } shutdown = server.Shutdown go func() { - err := server.Serve(tracked) + err := server.Serve(serveListener) if errors.Is(err, http.ErrServerClosed) { err = nil } @@ -204,18 +251,27 @@ type child struct { encoder *json.Encoder decoder *json.Decoder address string + tls bool cancel context.CancelFunc closed bool } func startChild(adapter string) (*child, error) { + return startChildMode(adapter, false) +} + +func startChildMode(adapter string, tlsEnabled bool) (*child, error) { executable, err := os.Executable() if err != nil { return nil, err } // This bounds even a broken server or protocol read; it is not a load duration. ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) - c := &child{cmd: exec.CommandContext(ctx, executable, "--server", adapter), cancel: cancel} + args := []string{"--server", adapter} + if tlsEnabled { + args = append(args, "--tls") + } + c := &child{cmd: exec.CommandContext(ctx, executable, args...), cancel: cancel, tls: tlsEnabled} c.cmd.Stderr = os.Stderr if c.input, err = c.cmd.StdinPipe(); err != nil { cancel() @@ -297,7 +353,15 @@ func (h *histogram) percentile(count uint64, percent uint64) float64 { } func exchange(client *http.Client, address string) error { - req, err := http.NewRequest(http.MethodGet, "http://"+address+"/health", nil) + return exchangeMode(client, address, false) +} + +func exchangeMode(client *http.Client, address string, tlsEnabled bool) error { + scheme := "http" + if tlsEnabled { + scheme = "https" + } + req, err := http.NewRequest(http.MethodGet, scheme+"://"+address+"/health", nil) if err != nil { return err } @@ -316,15 +380,19 @@ func exchange(client *http.Client, address string) error { func traffic(c *child, concurrency int, duration time.Duration) (result, error) { r := result{Scenario: "traffic", Concurrency: concurrency} - client := &http.Client{Transport: &http.Transport{ + transport := &http.Transport{ MaxIdleConns: concurrency, MaxIdleConnsPerHost: concurrency, MaxConnsPerHost: concurrency, IdleConnTimeout: time.Minute, - }, Timeout: 5 * time.Second} + } + if c.tls { + transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // test certificate is self-signed + } + client := &http.Client{Transport: transport, Timeout: 5 * time.Second} defer client.CloseIdleConnections() // Warm every worker before forcing a baseline GC in the server. warmErrors := make(chan error, concurrency) for i := 0; i < concurrency; i++ { - go func() { warmErrors <- exchange(client, c.address) }() + go func() { warmErrors <- exchangeMode(client, c.address, c.tls) }() } for i := 0; i < concurrency; i++ { if err := <-warmErrors; err != nil { @@ -351,7 +419,7 @@ func traffic(c *child, concurrency int, duration time.Duration) (result, error) <-start for time.Now().Before(deadline) { began := time.Now() - err := exchange(client, c.address) + err := exchangeMode(client, c.address, c.tls) w.latencies.add(time.Since(began)) w.count++ if err != nil { @@ -438,6 +506,10 @@ func idle(c *child, count int) (result, error) { if err != nil { return r, err } + if c.tls { + secure := tls.Client(conn, &tls.Config{InsecureSkipVerify: true, ServerName: "localhost"}) // test certificate is self-signed + conn = secure + } connections = append(connections, conn) conn.SetDeadline(time.Now().Add(5 * time.Second)) if _, err := io.WriteString(conn, "GET /health HTTP/1.1\r\nHost: localhost\r\nX-Request-ID: perfCheck\r\n\r\n"); err != nil { @@ -505,7 +577,7 @@ func levels(text string) ([]int, error) { return values, nil } -func compare(duration time.Duration, concurrencyText, idleText string, count int, output string) error { +func compare(duration time.Duration, concurrencyText, idleText string, count int, output string, tlsEnabled bool) error { concurrency, err := levels(concurrencyText) if err != nil { return err @@ -519,9 +591,9 @@ func compare(duration time.Duration, concurrencyText, idleText string, count int } report := report{ CreatedAt: time.Now().UTC().Format(time.RFC3339), GoVersion: runtime.Version(), - Platform: runtime.GOOS + "/" + runtime.GOARCH, GOMAXPROCS: runtime.GOMAXPROCS(0), Duration: duration.String(), + Platform: runtime.GOOS + "/" + runtime.GOARCH, GOMAXPROCS: runtime.GOMAXPROCS(0), Duration: duration.String(), TLS: tlsEnabled, Notes: []string{ - "Loopback HTTP/1.1 health response, fixed requestId, no TLS, access logs or database.", + "Loopback HTTP/1.1 health response, fixed requestId, optional TLS, no access logs or database.", "Each case starts a fresh server process; load client runs in the parent process on the same machine.", "Closed-loop concurrency; throughput is successful responses / actual elapsed time, including drain.", "Latency percentiles include failures and are histogram upper bounds with <=6.25% rounding.", @@ -543,7 +615,7 @@ func compare(duration time.Duration, concurrencyText, idleText string, count int }{{"traffic", concurrency}, {"idle", idleLevels}} { for _, level := range scenario.levels { for _, adapter := range adapters { - c, err := startChild(adapter) + c, err := startChildMode(adapter, tlsEnabled) if err != nil { return err } diff --git a/internal/perfcheck/main_test.go b/internal/perfcheck/main_test.go index a4c87e8..6f29eb9 100644 --- a/internal/perfcheck/main_test.go +++ b/internal/perfcheck/main_test.go @@ -45,6 +45,18 @@ func TestExchangeRejectsInvalidResponses(t *testing.T) { } } +func TestExchangeModeTLS(t *testing.T) { + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("X-Request-ID", "perfCheck") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(expectedBody)) + })) + defer server.Close() + if err := exchangeMode(server.Client(), strings.TrimPrefix(server.URL, "https://"), true); err != nil { + t.Fatal(err) + } +} + func TestPercentileBounds(t *testing.T) { var h histogram // Known ranks, including a tail much slower than the median. diff --git a/scripts/verifyPerformance.py b/scripts/verifyPerformance.py index 4fcc6dc..56a7dd1 100755 --- a/scripts/verifyPerformance.py +++ b/scripts/verifyPerformance.py @@ -12,6 +12,7 @@ def main(): parser.add_argument("--go", default="go") parser.add_argument("--count", type=int, default=3) parser.add_argument("--load", action="store_true", help="Also compare isolated servers and idle connection memory") + parser.add_argument("--tls", action="store_true", help="Run the isolated load matrix over TLS") parser.add_argument("--duration", default="2s", help="Duration per load scenario") parser.add_argument("--output", help="JSON output path for --load") args = parser.parse_args() @@ -32,6 +33,8 @@ def main(): if args.load: command = [args.go, "run", "./internal/perfcheck", "--duration", args.duration, "--count", str(args.count)] + if args.tls: + command.append("--tls") if args.output: command += ["--output", str(Path(args.output).resolve())] checks.append((command, "isolated server concurrency and idle-memory matrix")) diff --git a/transport/native/server_test.go b/transport/native/server_test.go index f41b398..7310b4e 100644 --- a/transport/native/server_test.go +++ b/transport/native/server_test.go @@ -903,6 +903,36 @@ func TestAppRunWithNativeTLS(t *testing.T) { } } +func TestNativeTLSSlowHandshakeTimesOut(t *testing.T) { + application := omni.NewWithRouter(netrouter.New()) + application.GET("/health", func(ctx *omni.Context) error { return ctx.OK("ok") }) + server := native.New(application) + server.ReadHeaderTimeout = 30 * time.Millisecond + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + certificate := testCertificate(t) + done := make(chan error, 1) + go func() { + done <- server.ServeTLS(context.Background(), listener, &tls.Config{Certificates: []tls.Certificate{certificate}}) + }() + waitForServer(t, listener.Addr().String()) + defer stopServer(t, server, listener, done) + + conn, err := net.Dial("tcp", listener.Addr().String()) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + if err := conn.SetReadDeadline(time.Now().Add(time.Second)); err != nil { + t.Fatal(err) + } + if _, err := conn.Read(make([]byte, 1)); err == nil { + t.Fatal("slow TLS handshake was not closed") + } +} + func testCertificate(t *testing.T) tls.Certificate { t.Helper() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)