//go:build ignore // IoT 卡分页查询性能基准测试 // 用法: // go run ./scripts/perf_query/bench.go // go run ./scripts/perf_query/bench.go -base-url http://localhost:3000 -n 20 package main import ( "encoding/json" "flag" "fmt" "io" "log" "net/http" "os" "sort" "strings" "time" ) var ( baseURL = flag.String("base-url", "", "API 服务地址 (默认读取 JUNHONG_SERVER_ADDRESS 环境变量)") username = flag.String("username", "perf_test_agent", "登录用户名") password = flag.String("password", "PerfTest@123456", "登录密码") n = flag.Int("n", 20, "每个场景执行次数") warmup = flag.Int("warmup", 3, "预热请求次数") ) type apiResponse struct { Code int `json:"code"` Msg string `json:"msg"` Data json.RawMessage `json:"data"` } type loginData struct { AccessToken string `json:"access_token"` } type listData struct { Total int64 `json:"total"` Page int `json:"page"` PageSize int `json:"page_size"` } type scenario struct { Name string Path string Params string } func main() { flag.Parse() if *baseURL == "" { addr := os.Getenv("JUNHONG_SERVER_ADDRESS") if addr == "" { addr = ":3000" } if strings.HasPrefix(addr, ":") { addr = "http://localhost" + addr } *baseURL = addr } fmt.Println("╔══════════════════════════════════════════════════════════════╗") fmt.Println("║ IoT 卡分页查询性能基准测试 ║") fmt.Println("╚══════════════════════════════════════════════════════════════╝") fmt.Printf(" 服务地址: %s\n", *baseURL) fmt.Printf(" 用户: %s\n", *username) fmt.Printf(" 每场景次数: %d (预热 %d 次)\n", *n, *warmup) fmt.Println() token := login() fmt.Printf("✅ 登录成功 (token: %s...)\n\n", token[:20]) scenarios := []scenario{ { Name: "A: 无过滤分页 (第1页)", Path: "/api/admin/iot-cards/standalone", Params: "page=1&page_size=20", }, { Name: "B: status+carrier 过滤", Path: "/api/admin/iot-cards/standalone", Params: "page=1&page_size=20&status=3&carrier_id=2", }, { Name: "C: 深分页 (第500页)", Path: "/api/admin/iot-cards/standalone", Params: "page=500&page_size=20", }, { Name: "D: ICCID 模糊搜索", Path: "/api/admin/iot-cards/standalone", Params: "page=1&page_size=20&iccid=12345", }, { Name: "E: 批次号精确过滤", Path: "/api/admin/iot-cards/standalone", Params: "page=1&page_size=20&batch_no=PERF-TEST-30M", }, { Name: "F: 运营商+状态+分页", Path: "/api/admin/iot-cards/standalone", Params: "page=10&page_size=50&status=1&carrier_id=1", }, } results := make([]benchResult, 0, len(scenarios)) for _, sc := range scenarios { r := runScenario(token, sc) results = append(results, r) } printSummary(results) } type benchResult struct { Name string Total int64 Timings []time.Duration Min time.Duration Max time.Duration Avg time.Duration P50 time.Duration P95 time.Duration P99 time.Duration Pass bool ErrorMsg string } func runScenario(token string, sc scenario) benchResult { fmt.Printf("🔄 %s\n", sc.Name) fmt.Printf(" URL: %s?%s\n", sc.Path, sc.Params) url := fmt.Sprintf("%s%s?%s", *baseURL, sc.Path, sc.Params) result := benchResult{Name: sc.Name} for i := 0; i < *warmup; i++ { dur, _, err := doRequest(url, token) if err != nil { result.ErrorMsg = fmt.Sprintf("预热失败: %v", err) fmt.Printf(" ❌ %s\n\n", result.ErrorMsg) return result } fmt.Printf(" 预热 %d/%d: %v\n", i+1, *warmup, dur.Round(time.Millisecond)) } timings := make([]time.Duration, 0, *n) var totalCount int64 for i := 0; i < *n; i++ { dur, data, err := doRequest(url, token) if err != nil { result.ErrorMsg = fmt.Sprintf("第 %d 次请求失败: %v", i+1, err) fmt.Printf(" ❌ %s\n\n", result.ErrorMsg) return result } timings = append(timings, dur) if i == 0 { totalCount = data.Total } if (i+1)%5 == 0 || i == 0 { fmt.Printf(" [%d/%d] %v\n", i+1, *n, dur.Round(time.Millisecond)) } } result.Total = totalCount result.Timings = timings result.Pass = true calcStats(&result) passStr := "✅" if result.P95 > 1500*time.Millisecond { passStr = "❌" result.Pass = false } else if result.P95 > 1000*time.Millisecond { passStr = "⚠️" } fmt.Printf(" 匹配行数: %d\n", result.Total) fmt.Printf(" %s P50=%v P95=%v P99=%v (min=%v max=%v)\n\n", passStr, result.P50, result.P95, result.P99, result.Min, result.Max) return result } func doRequest(url, token string) (time.Duration, *listData, error) { req, _ := http.NewRequest("GET", url, nil) req.Header.Set("Authorization", "Bearer "+token) start := time.Now() resp, err := http.DefaultClient.Do(req) dur := time.Since(start) if err != nil { return dur, nil, fmt.Errorf("HTTP 请求失败: %w", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != 200 { return dur, nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body[:min(len(body), 200)])) } var apiResp apiResponse if err := json.Unmarshal(body, &apiResp); err != nil { return dur, nil, fmt.Errorf("JSON 解析失败: %w", err) } if apiResp.Code != 0 { return dur, nil, fmt.Errorf("API 错误 code=%d: %s", apiResp.Code, apiResp.Msg) } var data listData json.Unmarshal(apiResp.Data, &data) return dur, &data, nil } func login() string { url := *baseURL + "/api/auth/login" payload := fmt.Sprintf(`{"username":"%s","password":"%s","device":"web"}`, *username, *password) resp, err := http.Post(url, "application/json", strings.NewReader(payload)) if err != nil { log.Fatalf("❌ 登录请求失败: %v", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != 200 { log.Fatalf("❌ 登录 HTTP %d: %s", resp.StatusCode, string(body[:min(len(body), 500)])) } var apiResp apiResponse if err := json.Unmarshal(body, &apiResp); err != nil { log.Fatalf("❌ 登录响应解析失败: %v", err) } if apiResp.Code != 0 { log.Fatalf("❌ 登录失败 code=%d: %s", apiResp.Code, apiResp.Msg) } var data loginData if err := json.Unmarshal(apiResp.Data, &data); err != nil { log.Fatalf("❌ 解析 token 失败: %v", err) } if data.AccessToken == "" { log.Fatalf("❌ 登录成功但 token 为空") } return data.AccessToken } func calcStats(r *benchResult) { sorted := make([]time.Duration, len(r.Timings)) copy(sorted, r.Timings) sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] }) var sum time.Duration for _, d := range sorted { sum += d } n := len(sorted) r.Min = sorted[0] r.Max = sorted[n-1] r.Avg = sum / time.Duration(n) r.P50 = percentile(sorted, 50) r.P95 = percentile(sorted, 95) r.P99 = percentile(sorted, 99) } func percentile(sorted []time.Duration, pct int) time.Duration { if len(sorted) == 0 { return 0 } idx := (pct * len(sorted)) / 100 if idx >= len(sorted) { idx = len(sorted) - 1 } return sorted[idx] } func printSummary(results []benchResult) { fmt.Println("╔══════════════════════════════════════════════════════════════════════════════╗") fmt.Println("║ 测试结果汇总 ║") fmt.Println("╠══════════════════════════════════════════════════════════════════════════════╣") fmt.Printf("║ %-32s │ %8s │ %8s │ %8s │ %8s ║\n", "场景", "匹配行数", "P50", "P95", "结果") fmt.Println("╠══════════════════════════════════════════════════════════════════════════════╣") allPass := true for _, r := range results { status := "✅ PASS" if r.ErrorMsg != "" { status = "❌ ERROR" allPass = false } else if !r.Pass { status = "❌ FAIL" allPass = false } else if r.P95 > 1000*time.Millisecond { status = "⚠️ SLOW" } p50Str := "-" p95Str := "-" totalStr := "-" if r.ErrorMsg == "" { p50Str = r.P50.Round(time.Millisecond).String() p95Str = r.P95.Round(time.Millisecond).String() totalStr = fmt.Sprintf("%d", r.Total) } fmt.Printf("║ %-32s │ %8s │ %8s │ %8s │ %8s ║\n", r.Name, totalStr, p50Str, p95Str, status) } fmt.Println("╚══════════════════════════════════════════════════════════════════════════════╝") if allPass { fmt.Println("\n🎉 所有场景 P95 < 1.5s,满足性能要求!") } else { fmt.Println("\n⚠️ 部分场景未达标,建议:") fmt.Println(" 1. 运行 go run ./scripts/perf_query/seed.go -action add-index 创建优化索引") fmt.Println(" 2. 重新运行本测试对比效果") } } func min(a, b int) int { if a < b { return a } return b }