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
293 lines
6.5 KiB
Go
293 lines
6.5 KiB
Go
//go:build integration
|
||
|
||
// Package integration 提供后端故障切换 E2E 基准测试。
|
||
//
|
||
// 该文件测试负载均衡器剔除/恢复后端的开销。
|
||
//
|
||
// 测试场景:
|
||
// - 健康后端正常选择
|
||
// - 后端标记不健康后的剔除开销
|
||
// - 后端重新标记健康后的恢复开销
|
||
//
|
||
// 作者:xfy
|
||
package integration
|
||
|
||
import (
|
||
"strconv"
|
||
"sync/atomic"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/valyala/fasthttp"
|
||
"rua.plus/lolly/internal/config"
|
||
"rua.plus/lolly/internal/loadbalance"
|
||
"rua.plus/lolly/internal/proxy"
|
||
)
|
||
|
||
// setupFailoverBackends 创建多后端用于故障切换测试。
|
||
//
|
||
// 参数:
|
||
// - count: 后端数量
|
||
// - healthyCount: 初始健康后端数量
|
||
//
|
||
// 返回值:
|
||
// - targets: 目标列表
|
||
// - cleanups: 清理函数列表
|
||
func setupFailoverBackends(b *testing.B, count, healthyCount int) ([]*loadbalance.Target, []func()) {
|
||
b.Helper()
|
||
|
||
targets := make([]*loadbalance.Target, count)
|
||
cleanups := make([]func(), count)
|
||
|
||
for i := range count {
|
||
addr, cleanup := setupNetworkBackend(b, fasthttp.StatusOK, []byte(`{"backend":`+strconv.Itoa(i)+`}`))
|
||
cleanups[i] = cleanup
|
||
targets[i] = &loadbalance.Target{
|
||
URL: "http://" + addr,
|
||
Weight: 1,
|
||
}
|
||
// 设置健康状态
|
||
if i < healthyCount {
|
||
targets[i].Healthy.Store(true)
|
||
} else {
|
||
targets[i].Healthy.Store(false)
|
||
}
|
||
}
|
||
|
||
return targets, cleanups
|
||
}
|
||
|
||
// BenchmarkE2EFailover_NormalSelect 测试健康后端正常选择。
|
||
//
|
||
// 所有后端健康时的负载均衡选择开销。
|
||
func BenchmarkE2EFailover_NormalSelect(b *testing.B) {
|
||
targets, cleanups := setupFailoverBackends(b, 5, 5)
|
||
defer func() {
|
||
for _, c := range cleanups {
|
||
c()
|
||
}
|
||
}()
|
||
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{
|
||
Connect: 5 * time.Second,
|
||
Read: 30 * time.Second,
|
||
Write: 30 * time.Second,
|
||
},
|
||
}
|
||
|
||
p, err := proxy.NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
b.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
warmupProxy(p, "/api/test", 10)
|
||
|
||
b.ReportAllocs()
|
||
b.ResetTimer()
|
||
|
||
b.RunParallel(func(pb *testing.PB) {
|
||
for pb.Next() {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.SetRequestURI("/api/test")
|
||
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
|
||
p.ServeHTTP(ctx)
|
||
}
|
||
})
|
||
}
|
||
|
||
// BenchmarkE2EFailover_OneUnhealthy 测试一个后端不健康。
|
||
//
|
||
// 4/5 后端健康时的选择开销。
|
||
func BenchmarkE2EFailover_OneUnhealthy(b *testing.B) {
|
||
targets, cleanups := setupFailoverBackends(b, 5, 4)
|
||
defer func() {
|
||
for _, c := range cleanups {
|
||
c()
|
||
}
|
||
}()
|
||
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{
|
||
Connect: 5 * time.Second,
|
||
Read: 30 * time.Second,
|
||
Write: 30 * time.Second,
|
||
},
|
||
}
|
||
|
||
p, err := proxy.NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
b.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
warmupProxy(p, "/api/test", 10)
|
||
|
||
b.ReportAllocs()
|
||
b.ResetTimer()
|
||
|
||
b.RunParallel(func(pb *testing.PB) {
|
||
for pb.Next() {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.SetRequestURI("/api/test")
|
||
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
|
||
p.ServeHTTP(ctx)
|
||
}
|
||
})
|
||
}
|
||
|
||
// BenchmarkE2EFailover_MostUnhealthy 测试多数后端不健康。
|
||
//
|
||
// 1/5 后端健康时的选择开销(剔除开销增大)。
|
||
func BenchmarkE2EFailover_MostUnhealthy(b *testing.B) {
|
||
targets, cleanups := setupFailoverBackends(b, 5, 1)
|
||
defer func() {
|
||
for _, c := range cleanups {
|
||
c()
|
||
}
|
||
}()
|
||
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{
|
||
Connect: 5 * time.Second,
|
||
Read: 30 * time.Second,
|
||
Write: 30 * time.Second,
|
||
},
|
||
}
|
||
|
||
p, err := proxy.NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
b.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
warmupProxy(p, "/api/test", 10)
|
||
|
||
b.ReportAllocs()
|
||
b.ResetTimer()
|
||
|
||
b.RunParallel(func(pb *testing.PB) {
|
||
for pb.Next() {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.SetRequestURI("/api/test")
|
||
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
|
||
p.ServeHTTP(ctx)
|
||
}
|
||
})
|
||
}
|
||
|
||
// BenchmarkE2EFailover_DynamicToggle 测试动态健康状态切换。
|
||
//
|
||
// 模拟后端健康状态在测试中变化。
|
||
func BenchmarkE2EFailover_DynamicToggle(b *testing.B) {
|
||
targets, cleanups := setupFailoverBackends(b, 3, 3)
|
||
defer func() {
|
||
for _, c := range cleanups {
|
||
c()
|
||
}
|
||
}()
|
||
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{
|
||
Connect: 5 * time.Second,
|
||
Read: 30 * time.Second,
|
||
Write: 30 * time.Second,
|
||
},
|
||
}
|
||
|
||
p, err := proxy.NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
b.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
warmupProxy(p, "/api/test", 10)
|
||
|
||
var toggleCounter atomic.Uint64
|
||
|
||
b.ReportAllocs()
|
||
b.ResetTimer()
|
||
|
||
b.RunParallel(func(pb *testing.PB) {
|
||
for pb.Next() {
|
||
// 每 100 次请求切换一个后端的健康状态
|
||
count := toggleCounter.Add(1)
|
||
if count%100 == 0 {
|
||
targetIdx := int(count/100) % len(targets)
|
||
current := targets[targetIdx].Healthy.Load()
|
||
targets[targetIdx].Healthy.Store(!current)
|
||
}
|
||
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.SetRequestURI("/api/test")
|
||
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
|
||
p.ServeHTTP(ctx)
|
||
}
|
||
})
|
||
}
|
||
|
||
// BenchmarkE2EFailover_AllUnhealthy 测试所有后端不健康。
|
||
//
|
||
// 无可用后端时的选择开销(应该返回错误)。
|
||
func BenchmarkE2EFailover_AllUnhealthy(b *testing.B) {
|
||
targets, cleanups := setupFailoverBackends(b, 3, 0)
|
||
defer func() {
|
||
for _, c := range cleanups {
|
||
c()
|
||
}
|
||
}()
|
||
|
||
cfg := &config.ProxyConfig{
|
||
Path: "/api",
|
||
LoadBalance: "round_robin",
|
||
Timeout: config.ProxyTimeout{
|
||
Connect: 5 * time.Second,
|
||
Read: 30 * time.Second,
|
||
Write: 30 * time.Second,
|
||
},
|
||
}
|
||
|
||
p, err := proxy.NewProxy(cfg, targets, nil, nil)
|
||
if err != nil {
|
||
b.Fatalf("NewProxy() error: %v", err)
|
||
}
|
||
|
||
b.ReportAllocs()
|
||
b.ResetTimer()
|
||
|
||
for b.Loop() {
|
||
ctx := &fasthttp.RequestCtx{}
|
||
ctx.Request.SetRequestURI("/api/test")
|
||
ctx.Request.Header.SetMethod(fasthttp.MethodGet)
|
||
p.ServeHTTP(ctx)
|
||
}
|
||
}
|
||
|
||
// BenchmarkE2EFailover_SelectOnly 测试纯选择开销(无实际请求)。
|
||
//
|
||
// 验证负载均衡器选择逻辑的分配。
|
||
func BenchmarkE2EFailover_SelectOnly(b *testing.B) {
|
||
targets := make([]*loadbalance.Target, 5)
|
||
for i := range 5 {
|
||
targets[i] = &loadbalance.Target{
|
||
URL: "http://backend" + strconv.Itoa(i) + ":8080",
|
||
Weight: 1,
|
||
}
|
||
targets[i].Healthy.Store(true)
|
||
}
|
||
|
||
rr := loadbalance.NewRoundRobin()
|
||
|
||
b.ReportAllocs()
|
||
b.ResetTimer()
|
||
|
||
for b.Loop() {
|
||
_ = rr.Select(targets)
|
||
}
|
||
}
|