lolly/internal/proxy/proxy_bench_test.go
xfy b5c3c0954f
Some checks failed
CI / Test (push) Failing after 23s
CI / Build (push) Has been skipped
fix: address repository review issues
Security:
- Enforce client_max_body_size in HTTP/2 and HTTP/3 adapters before buffering
- Use trusted-proxy-aware client IP extraction for access control and rate limiting
- Include Host and Vary headers in proxy cache key to prevent cache poisoning
- Harden static file path traversal check with filepath.Clean + prefix validation
- Honor proxy_ssl config for WebSocket upstream TLS

Proxy/handler bugs:
- Decrement WebSocket connection count immediately on return (not deferred in retry loop)
- Remove ineffective headersPool
- Coalesce concurrent background cache refreshes with singleflight

Dev/build:
- Fix Makefile run and test-config targets
- Remove broken wget healthcheck from docker-compose
- Add missing integration build tags
- Fix go vet warnings in tests

CI:
- Run golangci-lint and integration tests in Gitea Actions

Refs: docs/superpowers/plans/2026-06-17-fix-review-issues.md
2026-06-17 11:17:53 +08:00

621 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package proxy 提供反向代理性能的基准测试。
package proxy
import (
"fmt"
"sync/atomic"
"testing"
"time"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttputil"
"rua.plus/lolly/internal/config"
"rua.plus/lolly/internal/loadbalance"
"rua.plus/lolly/internal/testutil"
"rua.plus/lolly/internal/variable"
)
// setupMockBackend 设置一个模拟后端服务器用于基准测试。
// 返回监听器地址和清理函数。
func setupMockBackend(body []byte) (string, func()) {
ln := fasthttputil.NewInmemoryListener()
server := &fasthttp.Server{
Handler: func(ctx *fasthttp.RequestCtx) {
ctx.SetStatusCode(fasthttp.StatusOK)
_, _ = ctx.Write(body)
},
}
go func() {
_ = server.Serve(ln)
}()
// 等待服务器启动
time.Sleep(5 * time.Millisecond)
addr := ln.Addr().String()
cleanup := func() {
_ = ln.Close()
}
return addr, cleanup
}
// BenchmarkProxyForward 基准测试代理转发性能。
func BenchmarkProxyForward(b *testing.B) {
testCases := []struct {
name string
concurrency int
}{
{"concurrency1", 1},
{"concurrency10", 10},
{"concurrency100", 100},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
addr, cleanup := setupMockBackend([]byte("Hello, World!"))
defer cleanup()
cfg := &config.ProxyConfig{
Path: "/api",
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://" + addr},
}
targets[0].Healthy.Store(true)
p, err := NewProxy(cfg, targets, nil, nil)
if err != nil {
b.Fatalf("NewProxy() error: %v", err)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
ctx.Request.SetRequestURI("/api/test")
p.ServeHTTP(ctx)
}
})
})
}
}
// BenchmarkProxyForwardSmallRequest 基准测试小请求/小响应代理转发。
func BenchmarkProxyForwardSmallRequest(b *testing.B) {
smallBody := make([]byte, 100)
for i := range smallBody {
smallBody[i] = byte('a' + i%26)
}
addr, cleanup := setupMockBackend(smallBody)
defer cleanup()
cfg := &config.ProxyConfig{
Path: "/api",
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://" + addr},
}
targets[0].Healthy.Store(true)
p, err := NewProxy(cfg, targets, nil, nil)
if err != nil {
b.Fatalf("NewProxy() error: %v", err)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(fasthttp.MethodPost)
ctx.Request.SetRequestURI("/api/test")
ctx.Request.SetBodyString(string(smallBody))
p.ServeHTTP(ctx)
}
})
}
// BenchmarkProxyForwardLargeRequest 基准测试大请求/大响应代理转发。
func BenchmarkProxyForwardLargeRequest(b *testing.B) {
// 1KB 请求10KB 响应
requestBody := make([]byte, 1024)
for i := range requestBody {
requestBody[i] = byte('a' + i%26)
}
responseBody := make([]byte, 10*1024)
for i := range responseBody {
responseBody[i] = byte('A' + i%26)
}
addr, cleanup := setupMockBackend(responseBody)
defer cleanup()
cfg := &config.ProxyConfig{
Path: "/api",
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://" + addr},
}
targets[0].Healthy.Store(true)
p, err := NewProxy(cfg, targets, nil, nil)
if err != nil {
b.Fatalf("NewProxy() error: %v", err)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(fasthttp.MethodPost)
ctx.Request.SetRequestURI("/api/test")
ctx.Request.SetBody(requestBody)
p.ServeHTTP(ctx)
}
})
}
// BenchmarkProxyForwardMultipleTargets 基准测试多目标代理转发。
func BenchmarkProxyForwardMultipleTargets(b *testing.B) {
smallBody := []byte("OK")
numTargets := 5
targets := make([]*loadbalance.Target, numTargets)
cleanups := make([]func(), numTargets)
for i := range numTargets {
addr, cleanup := setupMockBackend(smallBody)
cleanups[i] = cleanup
targets[i] = &loadbalance.Target{
URL: "http://" + addr,
Weight: i + 1,
}
targets[i].Healthy.Store(true)
}
defer func() {
for _, cleanup := range cleanups {
cleanup()
}
}()
cfg := &config.ProxyConfig{
Path: "/api",
LoadBalance: "weighted_round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
},
}
p, err := NewProxy(cfg, targets, nil, nil)
if err != nil {
b.Fatalf("NewProxy() error: %v", err)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
ctx.Request.SetRequestURI("/api/test")
p.ServeHTTP(ctx)
}
})
}
// BenchmarkProxyHostClient 基准测试 HostClient 性能。
func BenchmarkProxyHostClient(b *testing.B) {
smallBody := []byte("Hello")
addr, cleanup := setupMockBackend(smallBody)
defer cleanup()
timeout := config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
}
client := createHostClient("http://"+addr, timeout, nil, nil, "", nil)
b.ResetTimer()
for b.Loop() {
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
req.SetRequestURI("http://" + addr + "/api/test")
req.Header.SetMethod(fasthttp.MethodGet)
_ = client.Do(req, resp)
fasthttp.ReleaseRequest(req)
fasthttp.ReleaseResponse(resp)
}
}
// BenchmarkProxyHostClientParallel 基准测试 HostClient 并行性能。
func BenchmarkProxyHostClientParallel(b *testing.B) {
smallBody := []byte("Hello")
addr, cleanup := setupMockBackend(smallBody)
defer cleanup()
timeout := config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
}
client := createHostClient("http://"+addr, timeout, nil, nil, "", nil)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
req := fasthttp.AcquireRequest()
resp := fasthttp.AcquireResponse()
req.SetRequestURI("http://" + addr + "/api/test")
req.Header.SetMethod(fasthttp.MethodGet)
_ = client.Do(req, resp)
fasthttp.ReleaseRequest(req)
fasthttp.ReleaseResponse(resp)
}
})
}
// BenchmarkProxyWithMockBackend 基准测试使用 mock_backend 工具的代理转发。
func BenchmarkProxyWithMockBackend(b *testing.B) {
// 使用 testutil 包启动 mock 后端
addr, cleanup := testutil.SimpleMockBackend(fasthttp.StatusOK, []byte("Hello from mock backend"))
defer cleanup()
// 等待服务器完全启动
time.Sleep(10 * time.Millisecond)
cfg := &config.ProxyConfig{
Path: "/api",
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
},
}
targets := []*loadbalance.Target{
{URL: "http://" + addr},
}
targets[0].Healthy.Store(true)
p, err := NewProxy(cfg, targets, nil, nil)
if err != nil {
b.Fatalf("NewProxy() error: %v", err)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
ctx.Request.SetRequestURI("/api/test")
p.ServeHTTP(ctx)
}
})
}
// BenchmarkProxyLoadBalancerSelection 基准测试代理负载均衡器选择性能。
func BenchmarkProxyLoadBalancerSelection(b *testing.B) {
testCases := []struct {
name string
loadBalance string
targetCount int
}{
{"round_robin_3", "round_robin", 3},
{"round_robin_50", "round_robin", 50},
{"weighted_round_robin_3", "weighted_round_robin", 3},
{"least_conn_3", "least_conn", 3},
{"ip_hash_3", "ip_hash", 3},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
targets := make([]*loadbalance.Target, tc.targetCount)
for i := 0; i < tc.targetCount; i++ {
targets[i] = &loadbalance.Target{
URL: fmt.Sprintf("http://backend%d:8080", i),
Weight: i + 1,
}
targets[i].Healthy.Store(true)
}
cfg := &config.ProxyConfig{
Path: "/api",
LoadBalance: tc.loadBalance,
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
},
}
p, err := NewProxy(cfg, targets, nil, nil)
if err != nil {
b.Fatalf("NewProxy() error: %v", err)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
counter := uint64(0)
// 每个 goroutine 使用独立的上下文
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("/api/test")
for pb.Next() {
idx := atomic.AddUint64(&counter, 1)
ctx.Request.Header.Set("X-Forwarded-For", fmt.Sprintf("192.168.1.%d", idx%255))
p.selectTarget(ctx)
}
})
})
}
}
// BenchmarkProxyHeaderProcessing 基准测试代理请求头处理性能。
func BenchmarkProxyHeaderProcessing(b *testing.B) {
target := &loadbalance.Target{URL: "http://localhost:8080"}
cfg := &config.ProxyConfig{
Path: "/api",
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
},
Headers: config.ProxyHeaders{
SetRequest: map[string]string{
"X-Custom-Header": "custom-value",
"X-Another": "another-value",
},
Remove: []string{"X-Remove-Me"},
},
}
targets := []*loadbalance.Target{
target,
}
targets[0].Healthy.Store(true)
p, err := NewProxy(cfg, targets, nil, nil)
if err != nil {
b.Fatalf("NewProxy() error: %v", err)
}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
ctx.Request.SetRequestURI("/api/test")
ctx.Request.Header.Set("X-Forwarded-For", "192.168.1.100")
ctx.Request.Header.Set("X-Remove-Me", "should-be-removed")
p.modifyRequestHeaders(ctx, target)
}
})
}
// BenchmarkBuildCacheKeyHash 基准测试缓存键哈希计算性能。
func BenchmarkBuildCacheKeyHash(b *testing.B) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
ctx.Request.SetRequestURI("/api/test?query=1")
p, err := NewProxy(&config.ProxyConfig{
Path: "/api",
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
},
}, []*loadbalance.Target{{URL: "http://localhost:8080"}}, nil, nil)
if err != nil {
b.Fatalf("NewProxy() error: %v", err)
}
b.Run("buildCacheKeyHash_with_string", func(b *testing.B) {
b.ResetTimer()
for b.Loop() {
hashKey, _ := p.buildCacheKeyHash(ctx, nil)
_ = hashKey
}
})
b.Run("buildCacheKeyHashValue_direct", func(b *testing.B) {
b.ResetTimer()
for b.Loop() {
hashKey := p.buildCacheKeyHashValue(ctx, nil)
_ = hashKey
}
})
}
// BenchmarkProxyObjectPoolGetRelease 基准测试 proxy 中对象池的获取/释放效果。
// 验证 UpstreamTiming 和变量上下文的池复用性能,对比有无池化的差异。
func BenchmarkProxyObjectPoolGetRelease(b *testing.B) {
addr, cleanup := setupMockBackend([]byte("OK"))
defer cleanup()
cfg := &config.ProxyConfig{
Path: "/api",
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
},
}
targets := []*loadbalance.Target{{URL: "http://" + addr}}
targets[0].Healthy.Store(true)
_, err := NewProxy(cfg, targets, nil, nil)
if err != nil {
b.Fatalf("NewProxy() error: %v", err)
}
// 本测试聚焦上游计时器和变量上下文池化效果Proxy 仅用于初始化验证
b.Run("UpstreamTiming_Pooled", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
timing := NewUpstreamTiming()
timing.MarkConnectStart()
time.Sleep(time.Microsecond)
timing.MarkConnectEnd()
timing.MarkHeaderReceived()
timing.MarkResponseEnd()
_ = timing.GetConnectTime()
_ = timing.GetHeaderTime()
_ = timing.GetResponseTime()
}
})
b.Run("VariableContext_Pooled", func(b *testing.B) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
ctx.Request.SetRequestURI("/api/test")
ctx.Request.Header.Set("X-Forwarded-For", "192.168.1.1")
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
vc := variable.NewContext(ctx)
vc.Set("key", "value")
_ = vc.Expand("$key")
variable.ReleaseContext(vc)
}
})
}
// BenchmarkProxyResponsePoolParallel 基准测试并行场景下的响应池获取/释放性能。
// 验证 fasthttp.Request/Response 对象池在并发代理请求中的表现。
func BenchmarkProxyResponsePoolParallel(b *testing.B) {
addr, cleanup := setupMockBackend([]byte("parallel response"))
defer cleanup()
cfg := &config.ProxyConfig{
Path: "/api",
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
},
}
targets := []*loadbalance.Target{{URL: "http://" + addr}}
targets[0].Healthy.Store(true)
p, err := NewProxy(cfg, targets, nil, nil)
if err != nil {
b.Fatalf("NewProxy() error: %v", err)
}
b.ReportAllocs()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
ctx.Request.SetRequestURI("/api/test")
p.ServeHTTP(ctx)
}
})
}
// BenchmarkProxyZeroAllocPath 基准测试零分配路径性能。
// 验证 buildCacheKeyHashValue 的零分配优化效果,
// 对比旧的字符串构建方式与直接哈希写入的差异。
func BenchmarkProxyZeroAllocPath(b *testing.B) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
ctx.Request.SetRequestURI("/api/test?query=value&foo=bar")
p, err := NewProxy(&config.ProxyConfig{
Path: "/api",
LoadBalance: "round_robin",
Timeout: config.ProxyTimeout{
Connect: 5 * time.Second,
Read: 30 * time.Second,
Write: 30 * time.Second,
},
}, []*loadbalance.Target{{URL: "http://localhost:8080"}}, nil, nil)
if err != nil {
b.Fatalf("NewProxy() error: %v", err)
}
b.Run("ZeroAlloc_buildCacheKeyHashValue", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
hash := p.buildCacheKeyHashValue(ctx, nil)
_ = hash
}
})
b.Run("WithAlloc_buildCacheKeyHash", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
hash, key := p.buildCacheKeyHash(ctx, nil)
_ = hash
_ = key
}
})
b.Run("ForwardedHeaders_ExtractSet", func(b *testing.B) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
ctx.Request.SetRequestURI("/api/test")
ctx.Request.Header.Set("X-Forwarded-For", "10.0.0.1")
ctx.Request.Header.Set("X-Real-IP", "10.0.0.2")
ctx.Request.Header.Set("Forwarded", "for=10.0.0.3")
ctx.Request.Header.Set("X-Forwarded-Proto", "https")
ctx.Request.Header.Set("X-Forwarded-Host", "example.com")
b.ReportAllocs()
b.ResetTimer()
for b.Loop() {
fh := ExtractForwardedHeaders(ctx)
_ = fh
}
})
}