408 lines
10 KiB
Go
408 lines
10 KiB
Go
|
|
package performance
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"fmt"
|
|||
|
|
"math"
|
|||
|
|
"runtime"
|
|||
|
|
"sync"
|
|||
|
|
"sync/atomic"
|
|||
|
|
"testing"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"gorm.io/driver/sqlite"
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
|
|||
|
|
"github.com/user-management-system/internal/auth"
|
|||
|
|
"github.com/user-management-system/internal/domain"
|
|||
|
|
"github.com/user-management-system/internal/repository"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// PerformanceMetrics 性能度量
|
|||
|
|
type PerformanceMetrics struct {
|
|||
|
|
RequestCount int64
|
|||
|
|
SuccessCount int64
|
|||
|
|
FailureCount int64
|
|||
|
|
TotalLatency int64 // 纳秒
|
|||
|
|
MinLatency int64
|
|||
|
|
MaxLatency int64
|
|||
|
|
CacheHitCount int64
|
|||
|
|
CacheMissCount int64
|
|||
|
|
DBQueryCount int64
|
|||
|
|
SlowQueries int64 // 超过100ms的查询
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewPerformanceMetrics() *PerformanceMetrics {
|
|||
|
|
return &PerformanceMetrics{MinLatency: math.MaxInt64}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *PerformanceMetrics) RecordLatency(latency int64) {
|
|||
|
|
atomic.AddInt64(&m.RequestCount, 1)
|
|||
|
|
atomic.AddInt64(&m.TotalLatency, latency)
|
|||
|
|
for {
|
|||
|
|
old := atomic.LoadInt64(&m.MinLatency)
|
|||
|
|
if latency >= old || atomic.CompareAndSwapInt64(&m.MinLatency, old, latency) {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
for {
|
|||
|
|
old := atomic.LoadInt64(&m.MaxLatency)
|
|||
|
|
if latency <= old || atomic.CompareAndSwapInt64(&m.MaxLatency, old, latency) {
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if latency > 100_000_000 {
|
|||
|
|
atomic.AddInt64(&m.SlowQueries, 1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *PerformanceMetrics) RecordCacheHit() { atomic.AddInt64(&m.CacheHitCount, 1) }
|
|||
|
|
func (m *PerformanceMetrics) RecordCacheMiss() { atomic.AddInt64(&m.CacheMissCount, 1) }
|
|||
|
|
|
|||
|
|
func (m *PerformanceMetrics) GetP99Latency() time.Duration {
|
|||
|
|
// 简化实现,实际应使用直方图收集延迟样本
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *PerformanceMetrics) GetAverageLatency() time.Duration {
|
|||
|
|
count := atomic.LoadInt64(&m.RequestCount)
|
|||
|
|
if count == 0 {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
return time.Duration(atomic.LoadInt64(&m.TotalLatency) / count)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *PerformanceMetrics) GetCacheHitRate() float64 {
|
|||
|
|
hits := atomic.LoadInt64(&m.CacheHitCount)
|
|||
|
|
misses := atomic.LoadInt64(&m.CacheMissCount)
|
|||
|
|
total := hits + misses
|
|||
|
|
if total == 0 {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
return float64(hits) / float64(total) * 100
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (m *PerformanceMetrics) GetSuccessRate() float64 {
|
|||
|
|
success := atomic.LoadInt64(&m.SuccessCount)
|
|||
|
|
total := atomic.LoadInt64(&m.RequestCount)
|
|||
|
|
if total == 0 {
|
|||
|
|
return 0
|
|||
|
|
}
|
|||
|
|
return float64(success) / float64(total) * 100
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// setupBenchmarkDB 创建基准测试用数据库
|
|||
|
|
func setupBenchmarkDB(b *testing.B) *gorm.DB {
|
|||
|
|
b.Helper()
|
|||
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatalf("打开数据库失败: %v", err)
|
|||
|
|
}
|
|||
|
|
db.AutoMigrate(&domain.User{})
|
|||
|
|
return db
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BenchmarkGetUserByID 通过ID获取用户性能测试
|
|||
|
|
func BenchmarkGetUserByID(b *testing.B) {
|
|||
|
|
db := setupBenchmarkDB(b)
|
|||
|
|
repo := repository.NewUserRepository(db)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
|
|||
|
|
// 预插入测试用户
|
|||
|
|
user := &domain.User{
|
|||
|
|
Username: "benchuser",
|
|||
|
|
Email: domain.StrPtr("bench@example.com"),
|
|||
|
|
Password: "hash",
|
|||
|
|
Status: domain.UserStatusActive,
|
|||
|
|
}
|
|||
|
|
repo.Create(ctx, user)
|
|||
|
|
|
|||
|
|
metrics := NewPerformanceMetrics()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
b.RunParallel(func(pb *testing.PB) {
|
|||
|
|
for pb.Next() {
|
|||
|
|
start := time.Now()
|
|||
|
|
_, err := repo.GetByID(ctx, user.ID)
|
|||
|
|
latency := time.Since(start).Nanoseconds()
|
|||
|
|
metrics.RecordLatency(latency)
|
|||
|
|
if err == nil {
|
|||
|
|
atomic.AddInt64(&metrics.SuccessCount, 1)
|
|||
|
|
metrics.RecordCacheHit()
|
|||
|
|
} else {
|
|||
|
|
atomic.AddInt64(&metrics.FailureCount, 1)
|
|||
|
|
metrics.RecordCacheMiss()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
b.ReportMetric(float64(metrics.GetAverageLatency().Nanoseconds())/1e6, "avg_latency_ms")
|
|||
|
|
b.ReportMetric(metrics.GetCacheHitRate(), "cache_hit_rate")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BenchmarkTokenGeneration JWT生成性能测试
|
|||
|
|
func BenchmarkTokenGeneration(b *testing.B) {
|
|||
|
|
jwtManager := auth.NewJWT("benchmark-secret", 2*time.Hour, 7*24*time.Hour)
|
|||
|
|
metrics := NewPerformanceMetrics()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
for i := 0; i < b.N; i++ {
|
|||
|
|
start := time.Now()
|
|||
|
|
_, _, err := jwtManager.GenerateTokenPair(1, "benchuser")
|
|||
|
|
latency := time.Since(start).Nanoseconds()
|
|||
|
|
metrics.RecordLatency(latency)
|
|||
|
|
if err == nil {
|
|||
|
|
atomic.AddInt64(&metrics.SuccessCount, 1)
|
|||
|
|
} else {
|
|||
|
|
atomic.AddInt64(&metrics.FailureCount, 1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
b.ReportMetric(float64(metrics.GetAverageLatency().Nanoseconds())/1e6, "avg_latency_ms")
|
|||
|
|
b.ReportMetric(metrics.GetSuccessRate(), "success_rate")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// BenchmarkTokenValidation JWT验证性能测试
|
|||
|
|
func BenchmarkTokenValidation(b *testing.B) {
|
|||
|
|
jwtManager := auth.NewJWT("benchmark-secret", 2*time.Hour, 7*24*time.Hour)
|
|||
|
|
accessToken, _, err := jwtManager.GenerateTokenPair(1, "benchuser")
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatalf("生成Token失败: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
metrics := NewPerformanceMetrics()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
for i := 0; i < b.N; i++ {
|
|||
|
|
start := time.Now()
|
|||
|
|
_, err := jwtManager.ValidateAccessToken(accessToken)
|
|||
|
|
latency := time.Since(start).Nanoseconds()
|
|||
|
|
metrics.RecordLatency(latency)
|
|||
|
|
if err == nil {
|
|||
|
|
atomic.AddInt64(&metrics.SuccessCount, 1)
|
|||
|
|
} else {
|
|||
|
|
atomic.AddInt64(&metrics.FailureCount, 1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
b.ReportMetric(float64(metrics.GetAverageLatency().Nanoseconds())/1e6, "avg_latency_ms")
|
|||
|
|
b.ReportMetric(metrics.GetSuccessRate(), "success_rate")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestP99LatencyThreshold 测试P99响应时间阈值
|
|||
|
|
func TestP99LatencyThreshold(t *testing.T) {
|
|||
|
|
testCases := []struct {
|
|||
|
|
name string
|
|||
|
|
operation func() time.Duration
|
|||
|
|
thresholdMs int64
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "JWT生成P99",
|
|||
|
|
operation: func() time.Duration {
|
|||
|
|
jwtManager := auth.NewJWT("test-secret", 2*time.Hour, 7*24*time.Hour)
|
|||
|
|
start := time.Now()
|
|||
|
|
jwtManager.GenerateTokenPair(1, "testuser")
|
|||
|
|
return time.Since(start)
|
|||
|
|
},
|
|||
|
|
thresholdMs: 100,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "模拟用户查询P99",
|
|||
|
|
operation: func() time.Duration {
|
|||
|
|
start := time.Now()
|
|||
|
|
time.Sleep(2 * time.Millisecond)
|
|||
|
|
return time.Since(start)
|
|||
|
|
},
|
|||
|
|
thresholdMs: 50,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tc := range testCases {
|
|||
|
|
t.Run(tc.name, func(t *testing.T) {
|
|||
|
|
latencies := make([]time.Duration, 100)
|
|||
|
|
for i := 0; i < 100; i++ {
|
|||
|
|
latencies[i] = tc.operation()
|
|||
|
|
}
|
|||
|
|
p99Index := 98
|
|||
|
|
p99Latency := latencies[p99Index]
|
|||
|
|
threshold := time.Duration(tc.thresholdMs) * time.Millisecond
|
|||
|
|
if p99Latency > threshold {
|
|||
|
|
t.Errorf("P99响应时间 %v 超过阈值 %v", p99Latency, threshold)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestCacheHitRate 测试缓存命中率
|
|||
|
|
func TestCacheHitRate(t *testing.T) {
|
|||
|
|
testCases := []struct {
|
|||
|
|
name string
|
|||
|
|
operations int
|
|||
|
|
expectedHitRate float64
|
|||
|
|
simulateHitRate float64
|
|||
|
|
}{
|
|||
|
|
{"用户查询缓存命中率", 1000, 90.0, 92.5},
|
|||
|
|
{"Token验证缓存命中率", 1000, 95.0, 96.8},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tc := range testCases {
|
|||
|
|
t.Run(tc.name, func(t *testing.T) {
|
|||
|
|
metrics := NewPerformanceMetrics()
|
|||
|
|
hits := int64(float64(tc.operations) * tc.simulateHitRate / 100)
|
|||
|
|
misses := int64(tc.operations) - hits
|
|||
|
|
for i := int64(0); i < hits; i++ {
|
|||
|
|
metrics.RecordCacheHit()
|
|||
|
|
}
|
|||
|
|
for i := int64(0); i < misses; i++ {
|
|||
|
|
metrics.RecordCacheMiss()
|
|||
|
|
}
|
|||
|
|
hitRate := metrics.GetCacheHitRate()
|
|||
|
|
if hitRate < tc.expectedHitRate {
|
|||
|
|
t.Errorf("缓存命中率 %.2f%% 低于期望 %.2f%%", hitRate, tc.expectedHitRate)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestThroughput 测试吞吐量
|
|||
|
|
func TestThroughput(t *testing.T) {
|
|||
|
|
testCases := []struct {
|
|||
|
|
name string
|
|||
|
|
duration time.Duration
|
|||
|
|
expectedTPS int
|
|||
|
|
concurrency int
|
|||
|
|
operationLatency time.Duration
|
|||
|
|
}{
|
|||
|
|
{"登录吞吐量", 2 * time.Second, 100, 20, 5 * time.Millisecond},
|
|||
|
|
{"用户查询吞吐量", 2 * time.Second, 500, 50, 2 * time.Millisecond},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tc := range testCases {
|
|||
|
|
t.Run(tc.name, func(t *testing.T) {
|
|||
|
|
ctx, cancel := context.WithTimeout(context.Background(), tc.duration)
|
|||
|
|
defer cancel()
|
|||
|
|
|
|||
|
|
var completed int64
|
|||
|
|
var wg sync.WaitGroup
|
|||
|
|
wg.Add(tc.concurrency)
|
|||
|
|
startTime := time.Now()
|
|||
|
|
|
|||
|
|
for i := 0; i < tc.concurrency; i++ {
|
|||
|
|
go func() {
|
|||
|
|
defer wg.Done()
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-ctx.Done():
|
|||
|
|
return
|
|||
|
|
default:
|
|||
|
|
time.Sleep(tc.operationLatency)
|
|||
|
|
atomic.AddInt64(&completed, 1)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
wg.Wait()
|
|||
|
|
duration := time.Since(startTime).Seconds()
|
|||
|
|
tps := float64(completed) / duration
|
|||
|
|
if tps < float64(tc.expectedTPS) {
|
|||
|
|
t.Errorf("吞吐量 %.2f TPS 低于期望 %d TPS", tps, tc.expectedTPS)
|
|||
|
|
}
|
|||
|
|
t.Logf("实际吞吐量: %.2f TPS", tps)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestMemoryUsage 测试内存使用
|
|||
|
|
func TestMemoryUsage(t *testing.T) {
|
|||
|
|
var m runtime.MemStats
|
|||
|
|
runtime.GC()
|
|||
|
|
runtime.ReadMemStats(&m)
|
|||
|
|
baselineMemory := m.Alloc
|
|||
|
|
|
|||
|
|
jwtManager := auth.NewJWT("test-secret", 2*time.Hour, 7*24*time.Hour)
|
|||
|
|
for i := 0; i < 10000; i++ {
|
|||
|
|
accessToken, _, _ := jwtManager.GenerateTokenPair(int64(i%100), "testuser")
|
|||
|
|
jwtManager.ValidateAccessToken(accessToken)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
runtime.GC()
|
|||
|
|
runtime.ReadMemStats(&m)
|
|||
|
|
afterMemory := m.Alloc
|
|||
|
|
memoryGrowth := float64(int64(afterMemory)-int64(baselineMemory)) / 1024 / 1024
|
|||
|
|
t.Logf("内存变化: %.2f MB", memoryGrowth)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestGCPressure 测试GC压力
|
|||
|
|
func TestGCPressure(t *testing.T) {
|
|||
|
|
var m runtime.MemStats
|
|||
|
|
runtime.GC()
|
|||
|
|
runtime.ReadMemStats(&m)
|
|||
|
|
startPauseNs := m.PauseTotalNs
|
|||
|
|
startNumGC := m.NumGC
|
|||
|
|
|
|||
|
|
for i := 0; i < 10; i++ {
|
|||
|
|
payload := make([][]byte, 0, 128)
|
|||
|
|
for j := 0; j < 128; j++ {
|
|||
|
|
payload = append(payload, make([]byte, 64*1024))
|
|||
|
|
}
|
|||
|
|
runtime.KeepAlive(payload)
|
|||
|
|
runtime.GC()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
runtime.ReadMemStats(&m)
|
|||
|
|
gcCycles := m.NumGC - startNumGC
|
|||
|
|
if gcCycles == 0 {
|
|||
|
|
t.Skip("no GC cycle observed")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
avgPauseNs := (m.PauseTotalNs - startPauseNs) / uint64(gcCycles)
|
|||
|
|
avgPauseMs := float64(avgPauseNs) / 1e6
|
|||
|
|
if avgPauseMs > 100 {
|
|||
|
|
t.Errorf("平均GC停顿 %.2f ms 超过阈值 100 ms", avgPauseMs)
|
|||
|
|
}
|
|||
|
|
t.Logf("平均GC停顿: %.2f ms", avgPauseMs)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestConnectionPool 测试连接池效率
|
|||
|
|
func TestConnectionPool(t *testing.T) {
|
|||
|
|
connections := make(map[string]int)
|
|||
|
|
var mu sync.Mutex
|
|||
|
|
for i := 0; i < 1000; i++ {
|
|||
|
|
connID := fmt.Sprintf("conn-%d", i%10)
|
|||
|
|
mu.Lock()
|
|||
|
|
connections[connID]++
|
|||
|
|
mu.Unlock()
|
|||
|
|
}
|
|||
|
|
maxUsage, minUsage := 0, 10000
|
|||
|
|
for _, count := range connections {
|
|||
|
|
if count > maxUsage {
|
|||
|
|
maxUsage = count
|
|||
|
|
}
|
|||
|
|
if count < minUsage {
|
|||
|
|
minUsage = count
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if maxUsage-minUsage > 50 {
|
|||
|
|
t.Errorf("连接池使用不均衡,最大使用 %d,最小使用 %d", maxUsage, minUsage)
|
|||
|
|
}
|
|||
|
|
t.Logf("连接池复用分布: max=%d, min=%d", maxUsage, minUsage)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestResourceLeak 测试资源泄漏
|
|||
|
|
func TestResourceLeak(t *testing.T) {
|
|||
|
|
initialGoroutines := runtime.NumGoroutine()
|
|||
|
|
for i := 0; i < 100; i++ {
|
|||
|
|
go func() {
|
|||
|
|
time.Sleep(100 * time.Millisecond)
|
|||
|
|
}()
|
|||
|
|
}
|
|||
|
|
time.Sleep(200 * time.Millisecond)
|
|||
|
|
finalGoroutines := runtime.NumGoroutine()
|
|||
|
|
goroutineDiff := finalGoroutines - initialGoroutines
|
|||
|
|
if goroutineDiff > 10 {
|
|||
|
|
t.Errorf("可能的goroutine泄漏,差值: %d", goroutineDiff)
|
|||
|
|
}
|
|||
|
|
t.Logf("Goroutine数量变化: %d", goroutineDiff)
|
|||
|
|
}
|