Files
junhong_cmp_fiber/scripts/perf_query/bench.go
huang f32d32cd36
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m6s
perf: IoT 卡 30M 行分页查询优化(P95 17.9s → <500ms)
- 新增 is_standalone 物化列 + 触发器自动维护(迁移 056)
- 并行查询拆分:多店铺 IN 查询拆为 per-shop goroutine 并行 Index Scan
- 两阶段延迟 Join:深度分页(page≥50)走覆盖索引 Index Only Scan 取 ID 再回表
- COUNT 缓存:per-shop 并行 COUNT + Redis 30 分钟 TTL
- 索引优化:删除有害全局索引、新增 partial composite indexes(迁移 057/058)
- ICCID 模糊搜索路径隔离:trigram GIN 索引走独立查询路径
- 慢查询阈值从 100ms 调整为 500ms
- 新增 30M 测试数据种子脚本和 benchmark 工具
2026-02-24 16:23:02 +08:00

343 lines
9.5 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//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
}