From 0979b60ff29cf20df1bd63bd9edf7569f62bd7c5 Mon Sep 17 00:00:00 2001 From: xfy Date: Tue, 7 Apr 2026 17:06:08 +0800 Subject: [PATCH] =?UTF-8?q?test(cache,loadbalance,proxy,benchmark):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=A0=B8=E5=BF=83=E6=A8=A1=E5=9D=97=E5=9F=BA?= =?UTF-8?q?=E5=87=86=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - cache: LRU Get/Set 性能测试 - loadbalance: 负载均衡算法性能测试 - proxy: 代理处理性能测试 - benchmark/tools: 负载生成器和模拟后端工具 Co-Authored-By: Claude --- internal/benchmark/tools/loadgen.go | 164 ++++++++ internal/benchmark/tools/mock_backend.go | 147 +++++++ internal/benchmark/tools/testdata.go | 182 +++++++++ internal/cache/cache_bench_test.go | 252 ++++++++++++ internal/loadbalance/balancer_bench_test.go | 265 ++++++++++++ internal/proxy/proxy_bench_test.go | 430 ++++++++++++++++++++ 6 files changed, 1440 insertions(+) create mode 100644 internal/benchmark/tools/loadgen.go create mode 100644 internal/benchmark/tools/mock_backend.go create mode 100644 internal/benchmark/tools/testdata.go create mode 100644 internal/cache/cache_bench_test.go create mode 100644 internal/loadbalance/balancer_bench_test.go create mode 100644 internal/proxy/proxy_bench_test.go diff --git a/internal/benchmark/tools/loadgen.go b/internal/benchmark/tools/loadgen.go new file mode 100644 index 0000000..ed9d9e1 --- /dev/null +++ b/internal/benchmark/tools/loadgen.go @@ -0,0 +1,164 @@ +// Package tools provides testing utilities and mock infrastructure for benchmark tests. +package tools + +import ( + "math" + "sort" + "sync" + "testing" + "time" + + "github.com/valyala/fasthttp" +) + +// FasthttpLoadGenerator is a load generator using fasthttp client. +type FasthttpLoadGenerator struct { + client *fasthttp.HostClient + addr string + stats LoadGenStats + mu sync.Mutex +} + +// LoadGenStats contains load generator statistics. +type LoadGenStats struct { + TotalRequests int + SuccessCount int + ErrorCount int + TotalDuration time.Duration + MinLatency time.Duration + MaxLatency time.Duration + MeanLatency time.Duration + P50Latency time.Duration + P90Latency time.Duration + P99Latency time.Duration + QPS float64 + Latencies []time.Duration // For percentile calculation +} + +// NewFasthttpLoadGenerator creates a new load generator for the given address. +func NewFasthttpLoadGenerator(addr string) *FasthttpLoadGenerator { + return &FasthttpLoadGenerator{ + client: &fasthttp.HostClient{ + Addr: addr, + MaxConns: 1000, + }, + addr: addr, + stats: LoadGenStats{ + MinLatency: time.Duration(math.MaxInt64), + }, + } +} + +// Run executes a load test with n requests using the specified concurrency. +// Returns collected statistics. +func (lg *FasthttpLoadGenerator) Run(n int, concurrency int) *LoadGenStats { + var wg sync.WaitGroup + requestsPerWorker := n / concurrency + + // Channels for collecting metrics + latencyChan := make(chan time.Duration, n) + errorChan := make(chan error, n) + + start := time.Now() + + for i := 0; i < concurrency; i++ { + wg.Add(1) + go func() { + defer wg.Done() + req := fasthttp.AcquireRequest() + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseRequest(req) + defer fasthttp.ReleaseResponse(resp) + + for j := 0; j < requestsPerWorker; j++ { + req.SetRequestURI("http://" + lg.addr + "/") + req.Header.SetMethod("GET") + + reqStart := time.Now() + err := lg.client.Do(req, resp) + latency := time.Since(reqStart) + + latencyChan <- latency + if err != nil { + errorChan <- err + } + } + }() + } + + wg.Wait() + close(latencyChan) + close(errorChan) + + totalDuration := time.Since(start) + + // Collect latencies + latencies := make([]time.Duration, 0, n) + for lat := range latencyChan { + latencies = append(latencies, lat) + } + + // Count errors + errorCount := 0 + for err := range errorChan { + _ = err // Error recorded, used for counting + errorCount++ + } + + // Calculate statistics + lg.mu.Lock() + defer lg.mu.Unlock() + + lg.stats.TotalRequests = n + lg.stats.ErrorCount = errorCount + lg.stats.SuccessCount = n - errorCount + lg.stats.TotalDuration = totalDuration + lg.stats.QPS = float64(n) / totalDuration.Seconds() + lg.stats.Latencies = latencies + + // Calculate latency distribution + if len(latencies) > 0 { + sort.Slice(latencies, func(i, j int) bool { + return latencies[i] < latencies[j] + }) + + lg.stats.MinLatency = latencies[0] + lg.stats.MaxLatency = latencies[len(latencies)-1] + + // Calculate mean + var sum time.Duration + for _, l := range latencies { + sum += l + } + lg.stats.MeanLatency = sum / time.Duration(len(latencies)) + + // Calculate percentiles + lg.stats.P50Latency = latencies[len(latencies)*50/100] + lg.stats.P90Latency = latencies[len(latencies)*90/100] + lg.stats.P99Latency = latencies[len(latencies)*99/100] + } + + return &lg.stats +} + +// GetStats returns current statistics without running a test. +func (lg *FasthttpLoadGenerator) GetStats() *LoadGenStats { + lg.mu.Lock() + defer lg.mu.Unlock() + return &lg.stats +} + +// RunParallel runs requests in parallel using testing.PB. +// This is designed for use with Go benchmark functions. +func (lg *FasthttpLoadGenerator) RunParallel(pb *testing.PB) { + req := fasthttp.AcquireRequest() + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseRequest(req) + defer fasthttp.ReleaseResponse(resp) + + for pb.Next() { + req.SetRequestURI("http://" + lg.addr + "/") + req.Header.SetMethod("GET") + lg.client.Do(req, resp) + } +} diff --git a/internal/benchmark/tools/mock_backend.go b/internal/benchmark/tools/mock_backend.go new file mode 100644 index 0000000..93ada34 --- /dev/null +++ b/internal/benchmark/tools/mock_backend.go @@ -0,0 +1,147 @@ +// Package tools provides testing utilities and mock infrastructure for benchmark tests. +package tools + +import ( + "math/rand" + "sync" + "time" + + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttputil" +) + +// BackendMode defines the response behavior of the mock backend. +type BackendMode int + +const ( + // ModeFixed returns a fixed response with configurable status and body. + ModeFixed BackendMode = iota + // ModeDelay adds artificial delay before responding. + ModeDelay + // ModeError returns errors for a percentage of requests. + ModeError + // ModeRandomResponse returns random status codes. + ModeRandomResponse +) + +// MockBackendConfig configures the mock backend behavior. +type MockBackendConfig struct { + Mode BackendMode + StatusCode int + Body []byte + Delay time.Duration + ErrorRate float64 // 0.0 to 1.0, for ModeError +} + +// MockBackend represents a mock fasthttp server for testing. +type MockBackend struct { + server *fasthttp.Server + config MockBackendConfig + mu sync.RWMutex +} + +// StartMockFasthttpBackend starts a mock fasthttp backend server. +// Returns the server address and a cleanup function. +func StartMockFasthttpBackend(config MockBackendConfig) (string, func()) { + mb := &MockBackend{ + config: config, + } + + mb.server = &fasthttp.Server{ + Handler: mb.handler, + } + + // Use in-memory listener for testing + ln := fasthttputil.NewInmemoryListener() + + // Start server in background + go func() { + mb.server.Serve(ln) + }() + + addr := "127.0.0.1:0" // In-memory listener address + + cleanup := func() { + mb.server.Shutdown() + ln.Close() + } + + return addr, cleanup +} + +// handler processes incoming requests based on the configured mode. +func (mb *MockBackend) handler(ctx *fasthttp.RequestCtx) { + mb.mu.RLock() + config := mb.config + mb.mu.RUnlock() + + switch config.Mode { + case ModeDelay: + time.Sleep(config.Delay) + ctx.SetStatusCode(config.StatusCode) + ctx.Write(config.Body) + + case ModeError: + if rand.Float64() < config.ErrorRate { + ctx.SetStatusCode(fasthttp.StatusInternalServerError) + ctx.WriteString("internal server error") + return + } + ctx.SetStatusCode(config.StatusCode) + ctx.Write(config.Body) + + case ModeRandomResponse: + codes := []int{ + fasthttp.StatusOK, + fasthttp.StatusCreated, + fasthttp.StatusNoContent, + fasthttp.StatusBadRequest, + fasthttp.StatusNotFound, + } + ctx.SetStatusCode(codes[rand.Intn(len(codes))]) + ctx.Write(config.Body) + + default: // ModeFixed + ctx.SetStatusCode(config.StatusCode) + ctx.Write(config.Body) + } +} + +// SetConfig updates the backend configuration at runtime. +func (mb *MockBackend) SetConfig(config MockBackendConfig) { + mb.mu.Lock() + defer mb.mu.Unlock() + mb.config = config +} + +// SimpleMockBackend creates a simple backend with fixed response. +// Returns address and cleanup function. +func SimpleMockBackend(statusCode int, body []byte) (string, func()) { + return StartMockFasthttpBackend(MockBackendConfig{ + Mode: ModeFixed, + StatusCode: statusCode, + Body: body, + }) +} + +// DelayedMockBackend creates a backend with delayed responses. +// Returns address and cleanup function. +func DelayedMockBackend(delay time.Duration, body []byte) (string, func()) { + return StartMockFasthttpBackend(MockBackendConfig{ + Mode: ModeDelay, + StatusCode: fasthttp.StatusOK, + Body: body, + Delay: delay, + }) +} + +// ErrorMockBackend creates a backend that returns errors at the specified rate. +// Returns address and cleanup function. +func ErrorMockBackend(errorRate float64, body []byte) (string, func()) { + return StartMockFasthttpBackend(MockBackendConfig{ + Mode: ModeError, + StatusCode: fasthttp.StatusOK, + Body: body, + ErrorRate: errorRate, + }) +} diff --git a/internal/benchmark/tools/testdata.go b/internal/benchmark/tools/testdata.go new file mode 100644 index 0000000..235c1bf --- /dev/null +++ b/internal/benchmark/tools/testdata.go @@ -0,0 +1,182 @@ +// Package tools provides testing utilities and mock infrastructure for benchmark tests. +package tools + +import ( + "fmt" + "math/rand" + "time" +) + +// TestDataSize represents predefined test data sizes. +type TestDataSize int + +const ( + // Size1KB represents 1KB test data. + Size1KB TestDataSize = 1024 + // Size10KB represents 10KB test data. + Size10KB TestDataSize = 10 * 1024 + // Size100KB represents 100KB test data. + Size100KB TestDataSize = 100 * 1024 + // Size1MB represents 1MB test data. + Size1MB TestDataSize = 1024 * 1024 + // Size10MB represents 10MB test data. + Size10MB TestDataSize = 10 * 1024 * 1024 +) + +// GenerateTestData generates test data of the specified size. +func GenerateTestData(size TestDataSize) []byte { + data := make([]byte, size) + // Fill with random data for compression testing + for i := range data { + data[i] = byte(rand.Intn(256)) + } + return data +} + +// GenerateTestDataString generates test data as string. +func GenerateTestDataString(size TestDataSize) string { + return string(GenerateTestData(size)) +} + +// TestTarget represents a backend target for testing. +type TestTarget struct { + Address string + Weight int +} + +// createTestTargets creates n test backend targets with mock servers. +// Returns the target configurations and a cleanup function. +func createTestTargets(n int) ([]TestTarget, func()) { + targets := make([]TestTarget, n) + cleanups := make([]func(), n) + + for i := 0; i < n; i++ { + body := GenerateTestData(Size1KB) + addr, cleanup := SimpleMockBackend(200, body) + targets[i] = TestTarget{ + Address: addr, + Weight: 1, + } + cleanups[i] = cleanup + } + + cleanupAll := func() { + for _, cleanup := range cleanups { + if cleanup != nil { + cleanup() + } + } + } + + return targets, cleanupAll +} + +// CreateTestTargets creates n test backend targets. +// Returns the target configurations and a cleanup function. +func CreateTestTargets(n int) ([]TestTarget, func()) { + return createTestTargets(n) +} + +// CreateWeightedTestTargets creates n test backend targets with varying weights. +// Returns the target configurations and a cleanup function. +func CreateWeightedTestTargets(n int) ([]TestTarget, func()) { + targets := make([]TestTarget, n) + cleanups := make([]func(), n) + + for i := 0; i < n; i++ { + body := GenerateTestData(Size1KB) + addr, cleanup := SimpleMockBackend(200, body) + // Vary weights: 1, 2, 3, etc. + targets[i] = TestTarget{ + Address: addr, + Weight: i + 1, + } + cleanups[i] = cleanup + } + + cleanupAll := func() { + for _, cleanup := range cleanups { + if cleanup != nil { + cleanup() + } + } + } + + return targets, cleanupAll +} + +// CreateDelayedTestTargets creates n test backend targets with varying delays. +// Useful for testing timeout and latency-related behavior. +func CreateDelayedTestTargets(n int, baseDelay time.Duration) ([]TestTarget, func()) { + targets := make([]TestTarget, n) + cleanups := make([]func(), n) + + for i := 0; i < n; i++ { + body := GenerateTestData(Size1KB) + // Each target has increasing delay + delay := baseDelay * time.Duration(i+1) + addr, cleanup := DelayedMockBackend(delay, body) + targets[i] = TestTarget{ + Address: addr, + Weight: 1, + } + cleanups[i] = cleanup + } + + cleanupAll := func() { + for _, cleanup := range cleanups { + if cleanup != nil { + cleanup() + } + } + } + + return targets, cleanupAll +} + +// CreateErrorTestTargets creates n test backend targets with varying error rates. +// Useful for testing error handling and circuit breaker behavior. +func CreateErrorTestTargets(n int, baseErrorRate float64) ([]TestTarget, func()) { + targets := make([]TestTarget, n) + cleanups := make([]func(), n) + + for i := 0; i < n; i++ { + body := GenerateTestData(Size1KB) + // Vary error rates slightly per target + errorRate := baseErrorRate + float64(i)*0.05 + if errorRate > 1.0 { + errorRate = 1.0 + } + addr, cleanup := ErrorMockBackend(errorRate, body) + targets[i] = TestTarget{ + Address: addr, + Weight: 1, + } + cleanups[i] = cleanup + } + + cleanupAll := func() { + for _, cleanup := range cleanups { + if cleanup != nil { + cleanup() + } + } + } + + return targets, cleanupAll +} + +// GenerateCacheKey generates a unique cache key for testing. +func GenerateCacheKey(prefix string, index int) string { + return fmt.Sprintf("%s-key-%d", prefix, index) +} + +// GenerateCacheValue generates a cache value of specified size. +func GenerateCacheValue(size TestDataSize) []byte { + return GenerateTestData(size) +} + +// GenerateRandomCacheKey generates a random cache key. +func GenerateRandomCacheKey() string { + return fmt.Sprintf("random-key-%d-%d", time.Now().UnixNano(), rand.Int()) +} diff --git a/internal/cache/cache_bench_test.go b/internal/cache/cache_bench_test.go new file mode 100644 index 0000000..90794db --- /dev/null +++ b/internal/cache/cache_bench_test.go @@ -0,0 +1,252 @@ +// Package cache 提供缓存模块的基准测试。 +// +// 该文件测试缓存模块的性能,包括: +// - LRU Get/Set 操作性能 +// - 并发读写性能 +// - 不同缓存大小下的性能表现 +// +// 作者:xfy +package cache + +import ( + "fmt" + "testing" + "time" +) + +// BenchmarkFileCacheGet 测试热点读取场景下的 Get 性能。 +// 模拟缓存命中率高的场景,测试 LRU 链表的访问效率。 +func BenchmarkFileCacheGet(b *testing.B) { + sizes := []int{100, 1000, 10000} + + for _, size := range sizes { + b.Run(fmt.Sprintf("Size%d", size), func(b *testing.B) { + fc := NewFileCache(int64(size), 0, 1*time.Hour) + + // 预填充缓存 + for i := 0; i < size; i++ { + path := fmt.Sprintf("/file%d.txt", i) + data := []byte("cached data content") + _ = fc.Set(path, data, int64(len(data)), time.Now()) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + // 热点读取:集中在前面 10% 的数据 + path := fmt.Sprintf("/file%d.txt", i%(size/10+1)) + fc.Get(path) + i++ + } + }) + }) + } +} + +// BenchmarkFileCacheSet 测试 Set 操作性能,包括 LRU 淘汰开销。 +// 测试在缓存达到容量限制后的写入性能。 +func BenchmarkFileCacheSet(b *testing.B) { + sizes := []int{100, 1000, 10000} + + for _, size := range sizes { + b.Run(fmt.Sprintf("Size%d", size), func(b *testing.B) { + fc := NewFileCache(int64(size), 0, 1*time.Hour) + + // 预填充到容量上限 + for i := 0; i < size; i++ { + path := fmt.Sprintf("/file%d.txt", i) + data := []byte("cached data content") + _ = fc.Set(path, data, int64(len(data)), time.Now()) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + path := fmt.Sprintf("/newfile%d.txt", i) + data := []byte("new cached data content") + _ = fc.Set(path, data, int64(len(data)), time.Now()) + } + }) + } +} + +// BenchmarkFileCacheSetNoEviction 测试无淘汰场景下的 Set 性能。 +// 此时缓存未满,没有 LRU 淘汰开销。 +func BenchmarkFileCacheSetNoEviction(b *testing.B) { + fc := NewFileCache(int64(b.N+1000), 0, 1*time.Hour) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + path := fmt.Sprintf("/file%d.txt", i) + data := []byte("cached data content") + _ = fc.Set(path, data, int64(len(data)), time.Now()) + } +} + +// BenchmarkFileCacheConcurrent 测试并发读写混合负载性能。 +// 使用 90% Get / 10% Set 的混合负载模拟真实场景。 +func BenchmarkFileCacheConcurrent(b *testing.B) { + sizes := []int{100, 1000, 10000} + + for _, size := range sizes { + b.Run(fmt.Sprintf("Size%d", size), func(b *testing.B) { + fc := NewFileCache(int64(size), 0, 1*time.Hour) + + // 预填充缓存 + for i := 0; i < size; i++ { + path := fmt.Sprintf("/file%d.txt", i) + data := []byte("cached data content") + _ = fc.Set(path, data, int64(len(data)), time.Now()) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + // 90% Get, 10% Set + if i%10 == 0 { + path := fmt.Sprintf("/newfile%d.txt", i) + data := []byte("updated data content") + _ = fc.Set(path, data, int64(len(data)), time.Now()) + } else { + path := fmt.Sprintf("/file%d.txt", i%size) + fc.Get(path) + } + i++ + } + }) + }) + } +} + +// BenchmarkFileCacheGetOnly 测试纯读场景下的性能。 +// 模拟静态文件服务的缓存读取。 +func BenchmarkFileCacheGetOnly(b *testing.B) { + fc := NewFileCache(1000, 0, 1*time.Hour) + + // 预填充缓存 + for i := 0; i < 1000; i++ { + path := fmt.Sprintf("/static/file%d.css", i) + data := make([]byte, 1024) // 1KB 数据 + _ = fc.Set(path, data, int64(len(data)), time.Now()) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + path := fmt.Sprintf("/static/file%d.css", i%1000) + fc.Get(path) + i++ + } + }) +} + +// BenchmarkFileCacheSizeEviction 测试基于内存大小的淘汰性能。 +// 测试当缓存超过内存限制时的淘汰开销。 +func BenchmarkFileCacheSizeEviction(b *testing.B) { + // 限制最大 1MB 内存 + maxSize := int64(1024 * 1024) + fc := NewFileCache(0, maxSize, 1*time.Hour) + + // 预填充到接近容量上限 + data := make([]byte, 1024) // 1KB 每条 + for i := 0; i < 1000; i++ { + path := fmt.Sprintf("/file%d.txt", i) + _ = fc.Set(path, data, int64(len(data)), time.Now()) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + path := fmt.Sprintf("/newfile%d.txt", i) + newData := make([]byte, 1024) + _ = fc.Set(path, newData, int64(len(newData)), time.Now()) + } +} + +// BenchmarkFileCacheLRUTouch 测试 LRU 链表更新开销。 +// 频繁访问同一批条目,观察 LRU 移动性能。 +func BenchmarkFileCacheLRUTouch(b *testing.B) { + fc := NewFileCache(100, 0, 1*time.Hour) + + // 预填充缓存 + for i := 0; i < 100; i++ { + path := fmt.Sprintf("/file%d.txt", i) + data := []byte("cached data") + _ = fc.Set(path, data, int64(len(data)), time.Now()) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + // 按顺序访问,触发 LRU 链表更新 + path := fmt.Sprintf("/file%d.txt", i%100) + fc.Get(path) + } +} + +// BenchmarkProxyCacheGet 测试代理缓存 Get 性能。 +func BenchmarkProxyCacheGet(b *testing.B) { + pc := NewProxyCache(nil, false, 0) + + // 预填充缓存 + for i := 0; i < 1000; i++ { + key := fmt.Sprintf("key%d", i) + data := []byte("response body") + headers := map[string]string{"Content-Type": "application/json"} + pc.Set(key, data, headers, 200, 10*time.Minute) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + key := fmt.Sprintf("key%d", i%1000) + pc.Get(key) + i++ + } + }) +} + +// BenchmarkProxyCacheSet 测试代理缓存 Set 性能。 +func BenchmarkProxyCacheSet(b *testing.B) { + pc := NewProxyCache(nil, false, 0) + data := []byte("response body") + headers := map[string]string{"Content-Type": "application/json"} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + key := fmt.Sprintf("key%d", i) + pc.Set(key, data, headers, 200, 10*time.Minute) + } +} + +// BenchmarkProxyCacheConcurrent 测试代理缓存并发混合负载。 +// 使用 90% Get / 10% Set 的混合负载。 +func BenchmarkProxyCacheConcurrent(b *testing.B) { + pc := NewProxyCache(nil, false, 0) + + // 预填充缓存 + for i := 0; i < 1000; i++ { + key := fmt.Sprintf("key%d", i) + data := []byte("response body") + headers := map[string]string{"Content-Type": "application/json"} + pc.Set(key, data, headers, 200, 10*time.Minute) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := 0 + for pb.Next() { + if i%10 == 0 { + key := fmt.Sprintf("newkey%d", i) + data := []byte("new response body") + headers := map[string]string{"Content-Type": "application/json"} + pc.Set(key, data, headers, 200, 10*time.Minute) + } else { + key := fmt.Sprintf("key%d", i%1000) + pc.Get(key) + } + i++ + } + }) +} diff --git a/internal/loadbalance/balancer_bench_test.go b/internal/loadbalance/balancer_bench_test.go new file mode 100644 index 0000000..2aa09a5 --- /dev/null +++ b/internal/loadbalance/balancer_bench_test.go @@ -0,0 +1,265 @@ +// Package loadbalance 提供负载均衡算法的基准测试。 +package loadbalance + +import ( + "fmt" + "sync/atomic" + "testing" +) + +// generateTargets 生成指定数量的健康目标用于基准测试。 +func generateTargets(count int) []*Target { + targets := make([]*Target, count) + for i := 0; i < count; i++ { + targets[i] = &Target{ + URL: fmt.Sprintf("http://backend%d:8080", i), + Weight: 1, + } + targets[i].Healthy.Store(true) + } + return targets +} + +// generateWeightedTargets 生成带权重的目标用于基准测试。 +func generateWeightedTargets(count int, weights []int) []*Target { + targets := make([]*Target, count) + for i := 0; i < count; i++ { + weight := 1 + if i < len(weights) { + weight = weights[i] + } + targets[i] = &Target{ + URL: fmt.Sprintf("http://backend%d:8080", i), + Weight: weight, + } + targets[i].Healthy.Store(true) + } + return targets +} + +// BenchmarkRoundRobinSelect 基准测试轮询算法。 +func BenchmarkRoundRobinSelect(b *testing.B) { + testCases := []struct { + name string + targets int + }{ + {"3targets", 3}, + {"50targets", 50}, + {"200targets", 200}, + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + targets := generateTargets(tc.targets) + rr := NewRoundRobin() + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + rr.Select(targets) + } + }) + }) + } +} + +// BenchmarkWeightedRoundRobin 基准测试加权轮询算法。 +func BenchmarkWeightedRoundRobin(b *testing.B) { + testCases := []struct { + name string + targets int + weights []int + }{ + {"3targets_equal", 3, []int{1, 1, 1}}, + {"3targets_weighted", 3, []int{1, 5, 10}}, + {"50targets_equal", 50, nil}, + {"50targets_weighted", 50, []int{1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5}}, + {"200targets_equal", 200, nil}, + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + targets := generateWeightedTargets(tc.targets, tc.weights) + wrr := NewWeightedRoundRobin() + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + wrr.Select(targets) + } + }) + }) + } +} + +// BenchmarkConsistentHashSelect 基准测试一致性哈希算法。 +func BenchmarkConsistentHashSelect(b *testing.B) { + testCases := []struct { + name string + targets int + virtualNodes int + }{ + {"10targets_50vnodes", 10, 50}, + {"10targets_150vnodes", 10, 150}, + {"10targets_200vnodes", 10, 200}, + {"50targets_150vnodes", 50, 150}, + {"100targets_150vnodes", 100, 150}, + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + targets := generateTargets(tc.targets) + ch := NewConsistentHash(tc.virtualNodes, "ip") + ch.Rebuild(targets) + + keys := make([]string, 100) + for i := 0; i < 100; i++ { + keys[i] = fmt.Sprintf("192.168.1.%d", i) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := uint64(0) + for pb.Next() { + keyIdx := atomic.AddUint64(&i, 1) % uint64(len(keys)) + ch.SelectByKey(targets, keys[keyIdx]) + } + }) + }) + } +} + +// BenchmarkConsistentHashRebuild 基准测试一致性哈希环重建性能。 +func BenchmarkConsistentHashRebuild(b *testing.B) { + testCases := []struct { + name string + targets int + virtualNodes int + }{ + {"10targets_150vnodes", 10, 150}, + {"50targets_150vnodes", 50, 150}, + {"100targets_150vnodes", 100, 150}, + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + targets := generateTargets(tc.targets) + ch := NewConsistentHash(tc.virtualNodes, "ip") + + b.ResetTimer() + for i := 0; i < b.N; i++ { + ch.Rebuild(targets) + } + }) + } +} + +// BenchmarkLeastConnSelect 基准测试最少连接算法。 +func BenchmarkLeastConnSelect(b *testing.B) { + testCases := []struct { + name string + targets int + }{ + {"3targets", 3}, + {"50targets", 50}, + {"200targets", 200}, + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + targets := generateTargets(tc.targets) + // 设置不同的连接数以模拟真实场景 + for i, t := range targets { + t.Connections = int64(i * 10) + } + lc := NewLeastConnections() + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + lc.Select(targets) + } + }) + }) + } +} + +// BenchmarkIPHashSelect 基准测试 IP 哈希算法。 +func BenchmarkIPHashSelect(b *testing.B) { + testCases := []struct { + name string + targets int + }{ + {"3targets", 3}, + {"50targets", 50}, + {"200targets", 200}, + } + + for _, tc := range testCases { + b.Run(tc.name, func(b *testing.B) { + targets := generateTargets(tc.targets) + iph := NewIPHash() + + ips := make([]string, 100) + for i := 0; i < 100; i++ { + ips[i] = fmt.Sprintf("192.168.1.%d", i) + } + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + i := uint64(0) + for pb.Next() { + ipIdx := atomic.AddUint64(&i, 1) % uint64(len(ips)) + iph.SelectByIP(targets, ips[ipIdx]) + } + }) + }) + } +} + +// BenchmarkAllBalancers 对比所有负载均衡算法的性能。 +func BenchmarkAllBalancers(b *testing.B) { + targets := generateTargets(50) + weightedTargets := generateWeightedTargets(50, nil) + + b.Run("RoundRobin", func(b *testing.B) { + rr := NewRoundRobin() + b.ResetTimer() + for i := 0; i < b.N; i++ { + rr.Select(targets) + } + }) + + b.Run("WeightedRoundRobin", func(b *testing.B) { + wrr := NewWeightedRoundRobin() + b.ResetTimer() + for i := 0; i < b.N; i++ { + wrr.Select(weightedTargets) + } + }) + + b.Run("LeastConnections", func(b *testing.B) { + lc := NewLeastConnections() + b.ResetTimer() + for i := 0; i < b.N; i++ { + lc.Select(targets) + } + }) + + b.Run("IPHash", func(b *testing.B) { + iph := NewIPHash() + b.ResetTimer() + for i := 0; i < b.N; i++ { + iph.SelectByIP(targets, "192.168.1.100") + } + }) + + b.Run("ConsistentHash", func(b *testing.B) { + ch := NewConsistentHash(150, "ip") + ch.Rebuild(targets) + b.ResetTimer() + for i := 0; i < b.N; i++ { + ch.SelectByKey(targets, "192.168.1.100") + } + }) +} diff --git a/internal/proxy/proxy_bench_test.go b/internal/proxy/proxy_bench_test.go new file mode 100644 index 0000000..63815a2 --- /dev/null +++ b/internal/proxy/proxy_bench_test.go @@ -0,0 +1,430 @@ +// Package proxy 提供反向代理性能的基准测试。 +package proxy + +import ( + "fmt" + "sync/atomic" + "testing" + "time" + + "github.com/valyala/fasthttp" + "github.com/valyala/fasthttp/fasthttputil" + + "rua.plus/lolly/internal/benchmark/tools" + "rua.plus/lolly/internal/config" + "rua.plus/lolly/internal/loadbalance" +) + +// 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) + 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) + 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) + 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 := 0; i < numTargets; i++ { + 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) + 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) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + 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) + + 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) { + // 使用 tools 包启动 mock 后端 + addr, cleanup := tools.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) + 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) + 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) + 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) + } + }) +}