test(integration): 后端故障切换 E2E 基准测试

测试负载均衡器剔除/恢复后端的开销:
- NormalSelect: 43 allocs/op (正常场景)
- AllUnhealthy: 9 allocs/op (无可用后端)
- SelectOnly: 2 allocs/op (纯选择开销)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-29 10:47:47 +08:00
parent 6ac5fda431
commit abf0315fd8

View File

@ -0,0 +1,290 @@
// 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 := 0; i < count; i++ {
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 := 0; i < 5; i++ {
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)
}
}