test: add TLS performance matrix
This commit is contained in:
@@ -50,6 +50,7 @@ go run ./internal/perfcheck --duration 2s --count 3 --output /tmp/goOmniPerf.jso
|
|||||||
- 服务端 `runtime.MemStats` 通过独立 stdin/stdout 控制通道读取,客户端内存不计入。累计分配差值包含少量采样开销。堆峰值每 200 ms 采样,可能错过瞬时峰值;这些指标不是进程 RSS。
|
- 服务端 `runtime.MemStats` 通过独立 stdin/stdout 控制通道读取,客户端内存不计入。累计分配差值包含少量采样开销。堆峰值每 200 ms 采样,可能错过瞬时峰值;这些指标不是进程 RSS。
|
||||||
- 空闲场景在所有连接完成健康请求后做 GC 再采样。新增堆内存包括连接、缓冲区及分摊的运行时开销。`after` 为客户端断开并排空后一次 GC,`afterReclaim` 为第二次 GC,用于观察对象池缓存释放。
|
- 空闲场景在所有连接完成健康请求后做 GC 再采样。新增堆内存包括连接、缓冲区及分摊的运行时开销。`after` 为客户端断开并排空后一次 GC,`afterReclaim` 为第二次 GC,用于观察对象池缓存释放。
|
||||||
- native 配置上限为 8,192 连接 / 4,096 活跃请求;本轮并发低于上限,未验证过载保护容量。标准库没有同样的并发闸门;比较包括各自真实实现的开销。
|
- native 配置上限为 8,192 连接 / 4,096 活跃请求;本轮并发低于上限,未验证过载保护容量。标准库没有同样的并发闸门;比较包括各自真实实现的开销。
|
||||||
|
- 可用 `python3 scripts/verifyPerformance.py --load --tls --duration 2s` 运行 TLS 版本的独立进程矩阵。该模式在每个子进程生成短期自签名证书,客户端仅用于本机基准验证;本报告的历史表格仍是无 TLS 数据。
|
||||||
|
|
||||||
## benchmark 口径修正
|
## benchmark 口径修正
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,18 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/big"
|
||||||
"math/bits"
|
"math/bits"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -71,6 +78,7 @@ type report struct {
|
|||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
GOMAXPROCS int `json:"gomaxprocs"`
|
GOMAXPROCS int `json:"gomaxprocs"`
|
||||||
Duration string `json:"duration"`
|
Duration string `json:"duration"`
|
||||||
|
TLS bool `json:"tls"`
|
||||||
Notes []string `json:"notes"`
|
Notes []string `json:"notes"`
|
||||||
Results []result `json:"results"`
|
Results []result `json:"results"`
|
||||||
}
|
}
|
||||||
@@ -102,8 +110,38 @@ func (c *trackedConn) Close() error {
|
|||||||
return err
|
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() {
|
func main() {
|
||||||
serverMode := flag.String("server", "", "internal server subprocess: nethttp or native")
|
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")
|
duration := flag.Duration("duration", 2*time.Second, "measured duration per traffic case")
|
||||||
concurrencyText := flag.String("concurrency", "1,32,128", "traffic concurrency levels")
|
concurrencyText := flag.String("concurrency", "1,32,128", "traffic concurrency levels")
|
||||||
idleText := flag.String("idle", "128,1024", "established idle Keep-Alive connection levels")
|
idleText := flag.String("idle", "128,1024", "established idle Keep-Alive connection levels")
|
||||||
@@ -112,9 +150,9 @@ func main() {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
var err error
|
var err error
|
||||||
if *serverMode != "" {
|
if *serverMode != "" {
|
||||||
err = serve(*serverMode)
|
err = serve(*serverMode, *tlsEnabled)
|
||||||
} else {
|
} else {
|
||||||
err = compare(*duration, *concurrencyText, *idleText, *runs, *output)
|
err = compare(*duration, *concurrencyText, *idleText, *runs, *output, *tlsEnabled)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
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")
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
tracked := &trackedListener{Listener: listener}
|
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 := omni.New()
|
||||||
application.GET("/health", func(c *omni.Context) error {
|
application.GET("/health", func(c *omni.Context) error {
|
||||||
return c.OK(map[string]string{"status": "ok"})
|
return c.OK(map[string]string{"status": "ok"})
|
||||||
@@ -140,7 +187,7 @@ func serve(adapter string) error {
|
|||||||
server.MaxConnections = 8192
|
server.MaxConnections = 8192
|
||||||
server.MaxConcurrentRequests = 4096
|
server.MaxConcurrentRequests = 4096
|
||||||
shutdown = server.Shutdown
|
shutdown = server.Shutdown
|
||||||
go func() { done <- server.Serve(context.Background(), tracked) }()
|
go func() { done <- server.Serve(context.Background(), serveListener) }()
|
||||||
case "nethttp":
|
case "nethttp":
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
Handler: application, ReadHeaderTimeout: 5 * time.Second,
|
Handler: application, ReadHeaderTimeout: 5 * time.Second,
|
||||||
@@ -149,7 +196,7 @@ func serve(adapter string) error {
|
|||||||
}
|
}
|
||||||
shutdown = server.Shutdown
|
shutdown = server.Shutdown
|
||||||
go func() {
|
go func() {
|
||||||
err := server.Serve(tracked)
|
err := server.Serve(serveListener)
|
||||||
if errors.Is(err, http.ErrServerClosed) {
|
if errors.Is(err, http.ErrServerClosed) {
|
||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
@@ -204,18 +251,27 @@ type child struct {
|
|||||||
encoder *json.Encoder
|
encoder *json.Encoder
|
||||||
decoder *json.Decoder
|
decoder *json.Decoder
|
||||||
address string
|
address string
|
||||||
|
tls bool
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func startChild(adapter string) (*child, error) {
|
func startChild(adapter string) (*child, error) {
|
||||||
|
return startChildMode(adapter, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startChildMode(adapter string, tlsEnabled bool) (*child, error) {
|
||||||
executable, err := os.Executable()
|
executable, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// This bounds even a broken server or protocol read; it is not a load duration.
|
// This bounds even a broken server or protocol read; it is not a load duration.
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
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
|
c.cmd.Stderr = os.Stderr
|
||||||
if c.input, err = c.cmd.StdinPipe(); err != nil {
|
if c.input, err = c.cmd.StdinPipe(); err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
@@ -297,7 +353,15 @@ func (h *histogram) percentile(count uint64, percent uint64) float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func exchange(client *http.Client, address string) error {
|
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 {
|
if err != nil {
|
||||||
return err
|
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) {
|
func traffic(c *child, concurrency int, duration time.Duration) (result, error) {
|
||||||
r := result{Scenario: "traffic", Concurrency: concurrency}
|
r := result{Scenario: "traffic", Concurrency: concurrency}
|
||||||
client := &http.Client{Transport: &http.Transport{
|
transport := &http.Transport{
|
||||||
MaxIdleConns: concurrency, MaxIdleConnsPerHost: concurrency,
|
MaxIdleConns: concurrency, MaxIdleConnsPerHost: concurrency,
|
||||||
MaxConnsPerHost: concurrency, IdleConnTimeout: time.Minute,
|
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()
|
defer client.CloseIdleConnections()
|
||||||
// Warm every worker before forcing a baseline GC in the server.
|
// Warm every worker before forcing a baseline GC in the server.
|
||||||
warmErrors := make(chan error, concurrency)
|
warmErrors := make(chan error, concurrency)
|
||||||
for i := 0; i < concurrency; i++ {
|
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++ {
|
for i := 0; i < concurrency; i++ {
|
||||||
if err := <-warmErrors; err != nil {
|
if err := <-warmErrors; err != nil {
|
||||||
@@ -351,7 +419,7 @@ func traffic(c *child, concurrency int, duration time.Duration) (result, error)
|
|||||||
<-start
|
<-start
|
||||||
for time.Now().Before(deadline) {
|
for time.Now().Before(deadline) {
|
||||||
began := time.Now()
|
began := time.Now()
|
||||||
err := exchange(client, c.address)
|
err := exchangeMode(client, c.address, c.tls)
|
||||||
w.latencies.add(time.Since(began))
|
w.latencies.add(time.Since(began))
|
||||||
w.count++
|
w.count++
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -438,6 +506,10 @@ func idle(c *child, count int) (result, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return r, err
|
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)
|
connections = append(connections, conn)
|
||||||
conn.SetDeadline(time.Now().Add(5 * time.Second))
|
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 {
|
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
|
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)
|
concurrency, err := levels(concurrencyText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -519,9 +591,9 @@ func compare(duration time.Duration, concurrencyText, idleText string, count int
|
|||||||
}
|
}
|
||||||
report := report{
|
report := report{
|
||||||
CreatedAt: time.Now().UTC().Format(time.RFC3339), GoVersion: runtime.Version(),
|
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{
|
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.",
|
"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.",
|
"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.",
|
"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}} {
|
}{{"traffic", concurrency}, {"idle", idleLevels}} {
|
||||||
for _, level := range scenario.levels {
|
for _, level := range scenario.levels {
|
||||||
for _, adapter := range adapters {
|
for _, adapter := range adapters {
|
||||||
c, err := startChild(adapter)
|
c, err := startChildMode(adapter, tlsEnabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
func TestPercentileBounds(t *testing.T) {
|
||||||
var h histogram
|
var h histogram
|
||||||
// Known ranks, including a tail much slower than the median.
|
// Known ranks, including a tail much slower than the median.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ def main():
|
|||||||
parser.add_argument("--go", default="go")
|
parser.add_argument("--go", default="go")
|
||||||
parser.add_argument("--count", type=int, default=3)
|
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("--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("--duration", default="2s", help="Duration per load scenario")
|
||||||
parser.add_argument("--output", help="JSON output path for --load")
|
parser.add_argument("--output", help="JSON output path for --load")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
@@ -32,6 +33,8 @@ def main():
|
|||||||
if args.load:
|
if args.load:
|
||||||
command = [args.go, "run", "./internal/perfcheck", "--duration", args.duration,
|
command = [args.go, "run", "./internal/perfcheck", "--duration", args.duration,
|
||||||
"--count", str(args.count)]
|
"--count", str(args.count)]
|
||||||
|
if args.tls:
|
||||||
|
command.append("--tls")
|
||||||
if args.output:
|
if args.output:
|
||||||
command += ["--output", str(Path(args.output).resolve())]
|
command += ["--output", str(Path(args.output).resolve())]
|
||||||
checks.append((command, "isolated server concurrency and idle-memory matrix"))
|
checks.append((command, "isolated server concurrency and idle-memory matrix"))
|
||||||
|
|||||||
@@ -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 {
|
func testCertificate(t *testing.T) tls.Certificate {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
|||||||
Reference in New Issue
Block a user