diff --git a/scripts/deploy-prod.sh b/scripts/deploy-prod.sh index d8ad823..19f03b1 100755 --- a/scripts/deploy-prod.sh +++ b/scripts/deploy-prod.sh @@ -64,6 +64,7 @@ show_help() { --reset-db 重置数据库(删除并重建) --logs 部署完成后跟随查看后端日志 --skip-check 跳过部署前代码门禁(不推荐) + --no-clean 跳过部署后的 Docker 垃圾清理 -h, --help 显示帮助信息 环境变量: @@ -87,6 +88,7 @@ NO_MIGRATE=0 RESET_DB=0 FOLLOW_LOGS=0 SKIP_CHECK=0 +NO_CLEAN=0 while [[ $# -gt 0 ]]; do case "$1" in @@ -110,6 +112,10 @@ while [[ $# -gt 0 ]]; do SKIP_CHECK=1 shift ;; + --no-clean) + NO_CLEAN=1 + shift + ;; -h|--help) show_help exit 0 @@ -319,6 +325,23 @@ wait_http_ready() { exit 1 } +# 清理部署过程中产生的 Docker 垃圾(旧镜像、构建缓存) +# 每次 docker compose build 会生成新镜像,旧镜像变为 : +# 构建缓存(BuildKit)也会持续增长,不清理会轻松堆积几十 GB +docker_cleanup() { + log "清理 Docker 垃圾..." + local before after + + before=$(docker system df --format '{{.TotalCount}}' 2>/dev/null || echo "?") + + docker image prune -f 2>/dev/null || true + docker builder prune -f 2>/dev/null || true + + after=$(docker system df --format '{{.TotalCount}}' 2>/dev/null || echo "?") + + log_success "Docker 清理完成(对象数: ${before} -> ${after})" +} + main() { cd "${ROOT_DIR}" need_cmd docker @@ -348,6 +371,9 @@ main() { log "启动生产服务..." compose up -d else + log "清理旧镜像以释放空间..." + docker image prune -f 2>/dev/null || true + log "构建后端镜像..." compose build backend @@ -355,7 +381,7 @@ main() { compose build caddy log "启动生产服务..." - compose up -d --no-build + compose up -d --no-build --remove-orphans fi wait_service_healthy mysql "MySQL" @@ -372,6 +398,10 @@ main() { wait_service_healthy caddy "Caddy" wait_http_ready + if [[ "${NO_CLEAN}" != "1" ]]; then + docker_cleanup + fi + log_success "部署完成:$(public_url)" log "常用命令:$(compose_user_cmd) logs -f caddy backend" diff --git a/scripts/load_stress.go b/scripts/load_stress.go deleted file mode 100644 index 82f3092..0000000 --- a/scripts/load_stress.go +++ /dev/null @@ -1,840 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "flag" - "fmt" - "io" - "math/rand" - "net/http" - "os" - "sort" - "sync" - "sync/atomic" - "time" -) - -// 改进的压力测试工具 - 支持真实认证、梯度压测、详细统计 - -type TestConfig struct { - BaseURL string - Scenario string - Duration time.Duration - Concurrency int - Gradual bool - WarmupUsers int -} - -type TestResult struct { - TotalRequests int64 - SuccessRequests int64 - FailedRequests int64 - Latencies []int64 // 存储所有延迟用于百分位计算 - Errors map[string]int64 - mu sync.Mutex -} - -type AuthPool struct { - tokens []string - mu sync.RWMutex -} - -type AdminToken struct { - token string - expiresAt time.Time - mu sync.RWMutex -} - -// 可用商品ID池 -type ListingIDPool struct { - ids []int - mu sync.RWMutex -} - -var globalListingIDs *ListingIDPool - -var ( - baseURL = flag.String("url", "http://localhost:8080", "API 基础地址") - concurrency = flag.Int("c", 50, "并发数") - duration = flag.Int("d", 60, "测试时长(秒)") - scenario = flag.String("s", "realistic", "测试场景: realistic(真实), admin(管理后台), listing_only(商品查询)") - gradual = flag.Bool("gradual", false, "启用梯度压测") - warmupUsers = flag.Int("warmup", 100, "预热用户数(生成token)") -) - -func main() { - flag.Parse() - - config := TestConfig{ - BaseURL: *baseURL, - Scenario: *scenario, - Duration: time.Duration(*duration) * time.Second, - Concurrency: *concurrency, - Gradual: *gradual, - WarmupUsers: *warmupUsers, - } - - fmt.Printf("=== 压力测试配置 ===\n") - fmt.Printf("目标地址: %s\n", config.BaseURL) - fmt.Printf("测试场景: %s\n", config.Scenario) - fmt.Printf("测试时长: %d 秒\n", *duration) - fmt.Printf("并发数: %d\n", config.Concurrency) - fmt.Printf("梯度压测: %v\n", config.Gradual) - fmt.Printf("==================\n\n") - - // 健康检查 - if !healthCheck(config.BaseURL) { - fmt.Println("❌ 后端服务未响应,退出测试") - return - } - - // 预加载可用商品ID - fmt.Println("⏳ 预加载可用商品ID...") - globalListingIDs = loadAvailableListings(config.BaseURL) - if globalListingIDs == nil || len(globalListingIDs.ids) == 0 { - fmt.Println("⚠️ 无法加载商品ID,将使用随机ID(可能导致404)") - } else { - fmt.Printf("✅ 成功加载 %d 个可用商品ID\n\n", len(globalListingIDs.ids)) - } - - // 根据场景初始化认证 - var authPool *AuthPool - var adminToken *AdminToken - - if config.Scenario == "realistic" || config.Scenario == "listing_only" { - fmt.Printf("⏳ 预热:生成 %d 个测试用户 token...\n", config.WarmupUsers) - authPool = initAuthPool(config.BaseURL, config.WarmupUsers) - if authPool == nil || len(authPool.tokens) == 0 { - fmt.Println("⚠️ 无法生成用户token,将使用匿名访问") - } else { - fmt.Printf("✅ 成功生成 %d 个用户 token\n\n", len(authPool.tokens)) - } - } - - if config.Scenario == "admin" { - fmt.Println("⏳ 获取管理员 token...") - adminToken = initAdminToken(config.BaseURL) - if adminToken == nil || adminToken.token == "" { - fmt.Println("❌ 无法获取管理员token,退出测试") - return - } - fmt.Println("✅ 成功获取管理员 token\n") - } - - // 执行压测 - var result *TestResult - if config.Gradual { - result = runGradualTest(config, authPool, adminToken) - } else { - result = runTest(config, authPool, adminToken) - } - - printResult(result, *duration) -} - -// ============================================ -// 健康检查和认证初始化 -// ============================================ - -func healthCheck(baseURL string) bool { - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Get(baseURL + "/health") - if err != nil { - return false - } - defer resp.Body.Close() - return resp.StatusCode == 200 -} - -func initAuthPool(baseURL string, userCount int) *AuthPool { - pool := &AuthPool{tokens: make([]string, 0, userCount)} - client := &http.Client{Timeout: 10 * time.Second} - - successCount := 0 - for i := 1; i <= userCount; i++ { - phone := fmt.Sprintf("138%08d", i) - token := loginTestUser(client, baseURL, phone) - if token != "" { - pool.tokens = append(pool.tokens, token) - successCount++ - } - - // 每10个打印一次进度 - if i%10 == 0 || i == userCount { - fmt.Printf("\r 进度: %d/%d (%d 成功)", i, userCount, successCount) - } - } - fmt.Println() - - return pool -} - -func loginTestUser(client *http.Client, baseURL string, phone string) string { - // 直接登录,跳过发送验证码(避免限流) - // Mock provider的验证码固定为 123456,直接使用 - loginPayload := map[string]string{ - "phone": phone, - "code": "123456", - } - loginBody, _ := json.Marshal(loginPayload) - - resp, err := client.Post(baseURL+"/api/auth/sms/login", "application/json", bytes.NewBuffer(loginBody)) - if err != nil { - return "" - } - defer resp.Body.Close() - - // 如果验证码无效,发送一次验证码后重试 - if resp.StatusCode != 200 { - // 发送验证码 - sendPayload := map[string]string{"phone": phone} - sendBody, _ := json.Marshal(sendPayload) - sendResp, err := client.Post(baseURL+"/api/auth/sms/send", "application/json", bytes.NewBuffer(sendBody)) - if err != nil { - return "" - } - io.Copy(io.Discard, sendResp.Body) - sendResp.Body.Close() - - // 等待验证码写入Redis - time.Sleep(200 * time.Millisecond) - - // 重试登录 - resp2, err := client.Post(baseURL+"/api/auth/sms/login", "application/json", bytes.NewBuffer(loginBody)) - if err != nil { - return "" - } - defer resp2.Body.Close() - - if resp2.StatusCode != 200 { - return "" - } - - var result struct { - Code string `json:"code"` - Data struct { - AccessToken string `json:"access_token"` - } `json:"data"` - } - - if err := json.NewDecoder(resp2.Body).Decode(&result); err != nil { - return "" - } - - if result.Code == "ok" { - return result.Data.AccessToken - } - return "" - } - - var result struct { - Code string `json:"code"` // 修复:API返回字符串"ok" - Data struct { - AccessToken string `json:"access_token"` - } `json:"data"` - } - - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return "" - } - - if result.Code == "ok" { - return result.Data.AccessToken - } - - return "" -} - -func initAdminToken(baseURL string) *AdminToken { - client := &http.Client{Timeout: 10 * time.Second} - - // 获取验证码(为了获取captcha_id) - resp, err := client.Get(baseURL + "/api/admin/auth/captcha") - if err != nil { - fmt.Printf(" 错误: %v\n", err) - return nil - } - defer resp.Body.Close() - - var captchaResp struct { - Data struct { - CaptchaID string `json:"captcha_id"` - } `json:"data"` - } - - if err := json.NewDecoder(resp.Body).Decode(&captchaResp); err != nil { - return nil - } - - adminUsername := os.Getenv("LOAD_STRESS_ADMIN_USERNAME") - adminPassword := os.Getenv("LOAD_STRESS_ADMIN_PASSWORD") - if adminUsername == "" || adminPassword == "" { - fmt.Println(" 跳过后台登录:请设置 LOAD_STRESS_ADMIN_USERNAME 和 LOAD_STRESS_ADMIN_PASSWORD") - return nil - } - - // 登录后台账号 - loginPayload := map[string]string{ - "username": adminUsername, - "password": adminPassword, - "captcha_id": captchaResp.Data.CaptchaID, - "captcha_code": "1234", - } - loginBody, _ := json.Marshal(loginPayload) - - resp, err = client.Post(baseURL+"/api/admin/auth/login", "application/json", bytes.NewBuffer(loginBody)) - if err != nil { - fmt.Printf(" 错误: %v\n", err) - return nil - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - fmt.Printf(" 登录失败: HTTP %d\n", resp.StatusCode) - return nil - } - - var loginResp struct { - Code string `json:"code"` - Data struct { - AccessToken string `json:"access_token"` - ExpiresIn int `json:"expires_in"` - } `json:"data"` - } - - if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil { - return nil - } - - if loginResp.Code != "ok" { - fmt.Printf(" 登录失败: code=%s\n", loginResp.Code) - return nil - } - - return &AdminToken{ - token: loginResp.Data.AccessToken, - expiresAt: time.Now().Add(time.Duration(loginResp.Data.ExpiresIn) * time.Second), - } -} - -func (p *AuthPool) GetRandomToken() string { - if p == nil || len(p.tokens) == 0 { - return "" - } - p.mu.RLock() - defer p.mu.RUnlock() - return p.tokens[rand.Intn(len(p.tokens))] -} - -func (a *AdminToken) Get() string { - if a == nil { - return "" - } - a.mu.RLock() - defer a.mu.RUnlock() - return a.token -} - -func (p *ListingIDPool) GetRandomID() int { - if p == nil || len(p.ids) == 0 { - // 降级:返回随机ID - return rand.Intn(5000) + 1 - } - p.mu.RLock() - defer p.mu.RUnlock() - return p.ids[rand.Intn(len(p.ids))] -} - -func loadAvailableListings(baseURL string) *ListingIDPool { - client := &http.Client{Timeout: 10 * time.Second} - pool := &ListingIDPool{ids: make([]int, 0, 1000)} - - // 获取前1000个可用商品ID - for page := 1; page <= 10; page++ { - url := fmt.Sprintf("%s/api/listings?page=%d&page_size=100", baseURL, page) - resp, err := client.Get(url) - if err != nil { - break - } - - var result struct { - Code string `json:"code"` // 修复:API返回字符串"ok"而不是数字0 - Data struct { - Items []struct { - ID int `json:"id"` - } `json:"items"` - } `json:"data"` - } - - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - resp.Body.Close() - break - } - resp.Body.Close() - - if result.Code != "ok" || len(result.Data.Items) == 0 { - break - } - - for _, item := range result.Data.Items { - pool.ids = append(pool.ids, item.ID) - } - - // 如果不满100个,说明已经到最后一页 - if len(result.Data.Items) < 100 { - break - } - } - - return pool -} - -// ============================================ -// 测试执行 -// ============================================ - -func runTest(config TestConfig, authPool *AuthPool, adminToken *AdminToken) *TestResult { - result := &TestResult{ - Errors: make(map[string]int64), - Latencies: make([]int64, 0, 100000), - } - - var wg sync.WaitGroup - stopChan := make(chan struct{}) - - fmt.Printf("🚀 开始压测 (%d 并发, %v)...\n\n", config.Concurrency, config.Duration) - - // 启动统计goroutine - statsTicker := time.NewTicker(5 * time.Second) - go func() { - for { - select { - case <-statsTicker.C: - printProgress(result) - case <-stopChan: - statsTicker.Stop() - return - } - } - }() - - // 启动并发workers - for i := 0; i < config.Concurrency; i++ { - wg.Add(1) - go func(workerID int) { - defer wg.Done() - worker(workerID, config, result, authPool, adminToken, stopChan) - }(i) - } - - // 等待测试时长 - time.Sleep(config.Duration) - close(stopChan) - - wg.Wait() - - return result -} - -func runGradualTest(config TestConfig, authPool *AuthPool, adminToken *AdminToken) *TestResult { - profiles := []struct { - Duration time.Duration - Concurrency int - }{ - {30 * time.Second, 10}, // 预热 - {60 * time.Second, config.Concurrency / 4}, // 25%负载 - {60 * time.Second, config.Concurrency / 2}, // 50%负载 - {60 * time.Second, config.Concurrency}, // 100%负载 - {30 * time.Second, config.Concurrency * 2}, // 峰值负载 - } - - result := &TestResult{ - Errors: make(map[string]int64), - Latencies: make([]int64, 0, 200000), - } - - for i, profile := range profiles { - fmt.Printf("📊 阶段 %d/%d: %d 并发, 持续 %v\n", i+1, len(profiles), profile.Concurrency, profile.Duration) - - phaseConfig := config - phaseConfig.Duration = profile.Duration - phaseConfig.Concurrency = profile.Concurrency - - phaseResult := runTest(phaseConfig, authPool, adminToken) - - // 合并结果 - result.mu.Lock() - result.TotalRequests += phaseResult.TotalRequests - result.SuccessRequests += phaseResult.SuccessRequests - result.FailedRequests += phaseResult.FailedRequests - result.Latencies = append(result.Latencies, phaseResult.Latencies...) - for k, v := range phaseResult.Errors { - result.Errors[k] += v - } - result.mu.Unlock() - - if i < len(profiles)-1 { - fmt.Println("⏸ 冷却 10 秒...") - time.Sleep(10 * time.Second) - } - } - - return result -} - -func worker(id int, config TestConfig, result *TestResult, authPool *AuthPool, adminToken *AdminToken, stopChan chan struct{}) { - client := &http.Client{ - Timeout: 10 * time.Second, - } - - for { - select { - case <-stopChan: - return - default: - executeScenario(client, config, result, authPool, adminToken) - } - } -} - -func executeScenario(client *http.Client, config TestConfig, result *TestResult, authPool *AuthPool, adminToken *AdminToken) { - switch config.Scenario { - case "realistic": - executeRealisticScenario(client, config.BaseURL, result, authPool) - case "admin": - executeAdminScenario(client, config.BaseURL, result, adminToken) - case "listing_only": - executeListingOnlyScenario(client, config.BaseURL, result, authPool) - default: - testHealthCheck(client, config.BaseURL, result) - } -} - -// ============================================ -// 业务场景实现 -// ============================================ - -func executeRealisticScenario(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) { - r := rand.Intn(1000) - switch { - case r < 350: // 35% 查询商品列表 - testListListings(client, baseURL, result) - case r < 600: // 25% 查询商品详情 - testGetListingDetail(client, baseURL, result) - case r < 750: // 15% 查询我的订单 - testListMyOrders(client, baseURL, result, authPool) - case r < 850: // 10% 查询钱包余额 - testWalletBalance(client, baseURL, result, authPool) - case r < 920: // 7% 聊天列表 - testChatList(client, baseURL, result, authPool) - case r < 970: // 5% 查询订单详情 - testOrderDetail(client, baseURL, result, authPool) - case r < 990: // 2% 创建订单(需要认证) - testCreateOrder(client, baseURL, result, authPool) - default: // 1% 支付订单(需要认证) - testPayOrder(client, baseURL, result, authPool) - } -} - -func executeAdminScenario(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) { - r := rand.Intn(100) - switch { - case r < 25: // 25% 用户管理列表 - testAdminUsers(client, baseURL, result, adminToken) - case r < 45: // 20% 订单管理列表 - testAdminOrders(client, baseURL, result, adminToken) - case r < 60: // 15% 商品审核列表 - testAdminListings(client, baseURL, result, adminToken) - case r < 75: // 15% 钱包流水 - testAdminWalletLedger(client, baseURL, result, adminToken) - case r < 85: // 10% 申诉管理 - testAdminDisputes(client, baseURL, result, adminToken) - case r < 95: // 10% 审计日志 - testAdminAuditLogs(client, baseURL, result, adminToken) - default: // 5% 仪表盘 - testAdminDashboard(client, baseURL, result, adminToken) - } -} - -func executeListingOnlyScenario(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) { - r := rand.Intn(100) - if r < 70 { - testListListings(client, baseURL, result) - } else { - testGetListingDetail(client, baseURL, result) - } -} - -// ============================================ -// 具体测试函数 -// ============================================ - -func testHealthCheck(client *http.Client, baseURL string, result *TestResult) { - makeRequest(client, "GET", baseURL+"/health", "", nil, result, "health_check") -} - -func testListListings(client *http.Client, baseURL string, result *TestResult) { - page := rand.Intn(10) + 1 - pageSize := []int{10, 20, 50}[rand.Intn(3)] - url := fmt.Sprintf("%s/api/listings?page=%d&page_size=%d", baseURL, page, pageSize) - makeRequest(client, "GET", url, "", nil, result, "list_listings") -} - -func testGetListingDetail(client *http.Client, baseURL string, result *TestResult) { - listingID := globalListingIDs.GetRandomID() - url := fmt.Sprintf("%s/api/listings/%d", baseURL, listingID) - makeRequest(client, "GET", url, "", nil, result, "get_listing_detail") -} - -func testListMyOrders(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) { - token := authPool.GetRandomToken() - if token == "" { - return - } - page := rand.Intn(5) + 1 - url := fmt.Sprintf("%s/api/orders?page=%d&page_size=20", baseURL, page) - makeAuthRequest(client, "GET", url, token, nil, result, "list_my_orders") -} - -func testWalletBalance(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) { - token := authPool.GetRandomToken() - if token == "" { - return - } - makeAuthRequest(client, "GET", baseURL+"/api/wallet/balance", token, nil, result, "wallet_balance") -} - -func testChatList(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) { - token := authPool.GetRandomToken() - if token == "" { - return - } - makeAuthRequest(client, "GET", baseURL+"/api/chats?page=1&page_size=20", token, nil, result, "chat_list") -} - -func testOrderDetail(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) { - token := authPool.GetRandomToken() - if token == "" { - return - } - orderID := rand.Intn(3000) + 1 - url := fmt.Sprintf("%s/api/orders/%d", baseURL, orderID) - makeAuthRequest(client, "GET", url, token, nil, result, "order_detail") -} - -func testCreateOrder(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) { - token := authPool.GetRandomToken() - if token == "" { - return - } - listingID := globalListingIDs.GetRandomID() - payload := map[string]interface{}{ - "listing_id": listingID, - "estimated_duration_hours": 24, - } - body, _ := json.Marshal(payload) - makeAuthRequest(client, "POST", baseURL+"/api/orders", token, body, result, "create_order") -} - -func testPayOrder(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) { - token := authPool.GetRandomToken() - if token == "" { - return - } - orderID := rand.Intn(3000) + 1 - url := fmt.Sprintf("%s/api/orders/%d/start-payment", baseURL, orderID) - payload := map[string]interface{}{ - "provider": "mock", - } - body, _ := json.Marshal(payload) - makeAuthRequest(client, "POST", url, token, body, result, "pay_order") -} - -// 管理后台测试函数 -func testAdminUsers(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) { - page := rand.Intn(10) + 1 - url := fmt.Sprintf("%s/api/admin/users?page=%d&page_size=20", baseURL, page) - makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_users") -} - -func testAdminOrders(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) { - page := rand.Intn(10) + 1 - url := fmt.Sprintf("%s/api/admin/orders?page=%d&page_size=20", baseURL, page) - makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_orders") -} - -func testAdminListings(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) { - page := rand.Intn(10) + 1 - url := fmt.Sprintf("%s/api/admin/listings?page=%d&page_size=20", baseURL, page) - makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_listings") -} - -func testAdminWalletLedger(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) { - page := rand.Intn(20) + 1 - url := fmt.Sprintf("%s/api/admin/wallet/ledger?page=%d&page_size=20", baseURL, page) - makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_wallet_ledger") -} - -func testAdminDisputes(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) { - page := rand.Intn(5) + 1 - url := fmt.Sprintf("%s/api/admin/disputes?page=%d&page_size=20", baseURL, page) - makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_disputes") -} - -func testAdminAuditLogs(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) { - page := rand.Intn(20) + 1 - url := fmt.Sprintf("%s/api/admin/audit-logs?page=%d&page_size=20", baseURL, page) - makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_audit_logs") -} - -func testAdminDashboard(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) { - makeAuthRequest(client, "GET", baseURL+"/api/admin/dashboard", adminToken.Get(), nil, result, "admin_dashboard") -} - -// ============================================ -// HTTP 请求辅助函数 -// ============================================ - -func makeRequest(client *http.Client, method, url, token string, body []byte, result *TestResult, apiName string) { - var req *http.Request - var err error - - if body != nil { - req, err = http.NewRequest(method, url, bytes.NewBuffer(body)) - } else { - req, err = http.NewRequest(method, url, nil) - } - - if err != nil { - recordError(result, apiName+"_req_error") - atomic.AddInt64(&result.TotalRequests, 1) - atomic.AddInt64(&result.FailedRequests, 1) - return - } - - if token != "" { - req.Header.Set("Authorization", "Bearer "+token) - } - if body != nil { - req.Header.Set("Content-Type", "application/json") - } - - start := time.Now() - resp, err := client.Do(req) - latency := time.Since(start).Milliseconds() - - atomic.AddInt64(&result.TotalRequests, 1) - recordLatency(result, latency) - - if err != nil { - atomic.AddInt64(&result.FailedRequests, 1) - recordError(result, apiName+"_error: "+err.Error()) - return - } - defer resp.Body.Close() - io.Copy(io.Discard, resp.Body) - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - atomic.AddInt64(&result.SuccessRequests, 1) - } else { - atomic.AddInt64(&result.FailedRequests, 1) - recordError(result, fmt.Sprintf("%s_status_%d", apiName, resp.StatusCode)) - } -} - -func makeAuthRequest(client *http.Client, method, url, token string, body []byte, result *TestResult, apiName string) { - makeRequest(client, method, url, token, body, result, apiName) -} - -func recordLatency(result *TestResult, latency int64) { - result.mu.Lock() - defer result.mu.Unlock() - result.Latencies = append(result.Latencies, latency) -} - -func recordError(result *TestResult, errMsg string) { - result.mu.Lock() - defer result.mu.Unlock() - result.Errors[errMsg]++ -} - -// ============================================ -// 结果统计和输出 -// ============================================ - -func printProgress(result *TestResult) { - total := atomic.LoadInt64(&result.TotalRequests) - success := atomic.LoadInt64(&result.SuccessRequests) - failed := atomic.LoadInt64(&result.FailedRequests) - - if total > 0 { - successRate := float64(success) / float64(total) * 100 - fmt.Printf(" 进行中: %d 请求 | 成功率: %.2f%% | 失败: %d\n", total, successRate, failed) - } -} - -func printResult(result *TestResult, durationSec int) { - fmt.Printf("\n\n=== 压力测试结果 ===\n") - fmt.Printf("总请求数: %d\n", result.TotalRequests) - fmt.Printf("成功请求: %d (%.2f%%)\n", - result.SuccessRequests, - float64(result.SuccessRequests)/float64(result.TotalRequests)*100) - fmt.Printf("失败请求: %d (%.2f%%)\n", - result.FailedRequests, - float64(result.FailedRequests)/float64(result.TotalRequests)*100) - - qps := float64(result.TotalRequests) / float64(durationSec) - fmt.Printf("\nQPS: %.2f\n", qps) - - if len(result.Latencies) > 0 { - result.mu.Lock() - latencies := make([]int64, len(result.Latencies)) - copy(latencies, result.Latencies) - result.mu.Unlock() - - sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] }) - - p50 := latencies[len(latencies)*50/100] - p95 := latencies[len(latencies)*95/100] - p99 := latencies[len(latencies)*99/100] - min := latencies[0] - max := latencies[len(latencies)-1] - - var sum int64 - for _, l := range latencies { - sum += l - } - avg := sum / int64(len(latencies)) - - fmt.Printf("\n延迟统计:\n") - fmt.Printf(" 最小: %d ms\n", min) - fmt.Printf(" P50: %d ms\n", p50) - fmt.Printf(" 平均: %d ms\n", avg) - fmt.Printf(" P95: %d ms\n", p95) - fmt.Printf(" P99: %d ms\n", p99) - fmt.Printf(" 最大: %d ms\n", max) - } - - if len(result.Errors) > 0 { - fmt.Printf("\n错误统计 (Top 10):\n") - type errorPair struct { - msg string - count int64 - } - var errors []errorPair - for msg, count := range result.Errors { - errors = append(errors, errorPair{msg, count}) - } - sort.Slice(errors, func(i, j int) bool { return errors[i].count > errors[j].count }) - - for i, e := range errors { - if i >= 10 { - break - } - fmt.Printf(" %s: %d 次\n", e.msg, e.count) - } - } - - fmt.Printf("==================\n") -} diff --git a/scripts/load_test_data.sql b/scripts/load_test_data.sql deleted file mode 100644 index 5a868dd..0000000 --- a/scripts/load_test_data.sql +++ /dev/null @@ -1,495 +0,0 @@ --- ============================================ --- 压力测试数据生成脚本 --- 用于生成大量测试数据以进行性能测试 --- ============================================ - -SET NAMES utf8mb4; -SET FOREIGN_KEY_CHECKS = 0; - --- 清理测试数据(可选,谨慎使用) --- DELETE FROM wallet_ledger WHERE user_id > 1000; --- DELETE FROM rental_orders WHERE id > 100; --- DELETE FROM rental_listings WHERE id > 100; --- DELETE FROM game_accounts WHERE id > 100; --- DELETE FROM users WHERE id > 1000; - --- ============================================ --- 1. 批量生成用户数据(10000个用户) --- ============================================ -DROP PROCEDURE IF EXISTS generate_users; -DELIMITER $$ -CREATE PROCEDURE generate_users(IN batch_size INT) -BEGIN - DECLARE i INT DEFAULT 1; - DECLARE phone_num VARCHAR(32); - DECLARE nick VARCHAR(64); - - WHILE i <= batch_size DO - SET phone_num = CONCAT('138', LPAD(i, 8, '0')); - SET nick = CONCAT('测试用户', i); - - INSERT INTO users ( - phone, nickname, realname_status, risk_status, - credit_score, status, created_at - ) VALUES ( - phone_num, nick, - CASE WHEN i % 10 = 0 THEN 'unverified' ELSE 'verified' END, - CASE WHEN i % 100 = 0 THEN 'frozen' ELSE 'normal' END, - 80 + (i % 20), - 'active', - DATE_SUB(NOW(), INTERVAL (i % 365) DAY) - ) ON DUPLICATE KEY UPDATE id=id; - - -- 为已实名用户创建实名记录 - IF i % 10 != 0 THEN - INSERT INTO user_realname ( - user_id, provider, status, masked_name, masked_id_no, verified_at - ) VALUES ( - LAST_INSERT_ID(), 'mock', 'success', - CONCAT('张*', CHAR(65 + (i % 26))), - CONCAT('3301**********', LPAD(i % 10000, 4, '0')), - DATE_SUB(NOW(), INTERVAL (i % 300) DAY) - ) ON DUPLICATE KEY UPDATE user_id=user_id; - END IF; - - -- 为用户创建钱包 - INSERT INTO wallet_accounts (user_id, available_balance_cent, frozen_balance_cent, status) - VALUES ( - LAST_INSERT_ID(), - (i % 10) * 10000, - (i % 5) * 5000, - 'active' - ) ON DUPLICATE KEY UPDATE user_id=user_id; - - SET i = i + 1; - - -- 每1000条提交一次 - IF i % 1000 = 0 THEN - COMMIT; - END IF; - END WHILE; - - COMMIT; -END$$ -DELIMITER ; - --- ============================================ --- 2. 批量生成游戏账号和商品(50000个商品) --- ============================================ -DROP PROCEDURE IF EXISTS generate_listings; -DELIMITER $$ -CREATE PROCEDURE generate_listings(IN batch_size INT) -BEGIN - DECLARE i INT DEFAULT 1; - DECLARE user_id_val BIGINT; - DECLARE account_id_val BIGINT; - DECLARE price_val BIGINT; - DECLARE deposit_val BIGINT; - DECLARE verified_user_min BIGINT; - DECLARE verified_user_max BIGINT; - DECLARE user_pick BIGINT; - - SELECT MIN(id), MAX(id) - INTO verified_user_min, verified_user_max - FROM users - WHERE realname_status = 'verified'; - - IF verified_user_min IS NULL THEN - SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的已实名用户'; - END IF; - - WHILE i <= batch_size DO - -- 近似随机选择一个用户作为号主,避免 ORDER BY RAND() 全表排序。 - SET user_id_val = NULL; - SET user_pick = verified_user_min + FLOOR(RAND() * (verified_user_max - verified_user_min + 1)); - SELECT id INTO user_id_val FROM users - WHERE id >= user_pick AND realname_status = 'verified' - ORDER BY id LIMIT 1; - - IF user_id_val IS NULL THEN - SELECT id INTO user_id_val FROM users - WHERE realname_status = 'verified' - ORDER BY id LIMIT 1; - END IF; - - -- 创建游戏账号 - INSERT INTO game_accounts ( - owner_id, game_name, server_region, login_platform, - title, description, rank_level, haf_coin_amount, status - ) VALUES ( - user_id_val, - 'delta_force', - CASE (i % 4) - WHEN 0 THEN '亚服' - WHEN 1 THEN '美服' - WHEN 2 THEN '欧服' - ELSE '国服' - END, - CASE (i % 3) - WHEN 0 THEN 'Steam' - WHEN 1 THEN 'Epic' - ELSE 'WeGame' - END, - CONCAT('账号', i, ' 高分段'), - CONCAT('这是一个测试账号,编号', i), - CASE (i % 5) - WHEN 0 THEN '青铜' - WHEN 1 THEN '白银' - WHEN 2 THEN '黄金' - WHEN 3 THEN '铂金' - ELSE '钻石' - END, - (i % 100) * 10000, - 'published' - ); - - SET account_id_val = LAST_INSERT_ID(); - - -- 创建租号商品 - SET price_val = 500 + (i % 20) * 50; - SET deposit_val = 10000 + (i % 10) * 5000; - - INSERT INTO rental_listings ( - listing_no, account_id, owner_id, price_cent, deposit_amount_cent, - in_transaction, status, review_status, published_at - ) VALUES ( - CONCAT( - DATE_FORMAT(DATE_SUB(NOW(), INTERVAL (i % 90) DAY), '%Y%m%d'), - LPAD(FLOOR((i - 1) / 90) + 1, 4, '0') - ), - account_id_val, - user_id_val, - price_val, - deposit_val, - CASE WHEN i % 10 = 0 THEN 1 ELSE 0 END, - CASE - WHEN i % 20 = 0 THEN 'offline' - WHEN i % 15 = 0 THEN 'draft' - ELSE 'published' - END, - CASE - WHEN i % 15 = 0 THEN 'pending' - WHEN i % 30 = 0 THEN 'rejected' - ELSE 'approved' - END, - DATE_SUB(NOW(), INTERVAL (i % 90) DAY) - ); - - SET i = i + 1; - - IF i % 1000 = 0 THEN - COMMIT; - END IF; - END WHILE; - - INSERT INTO listing_no_sequences (biz_date, next_seq) - SELECT LEFT(listing_no, 8), MAX(CAST(SUBSTRING(listing_no, 9) AS UNSIGNED)) - FROM rental_listings - GROUP BY LEFT(listing_no, 8) - ON DUPLICATE KEY UPDATE next_seq = GREATEST(next_seq, VALUES(next_seq)); - - COMMIT; -END$$ -DELIMITER ; - --- ============================================ --- 3. 批量生成订单数据(30000个订单) --- ============================================ -DROP PROCEDURE IF EXISTS generate_orders; -DELIMITER $$ -CREATE PROCEDURE generate_orders(IN batch_size INT) -BEGIN - DECLARE i INT DEFAULT 1; - DECLARE listing_id_val BIGINT; - DECLARE account_id_val BIGINT; - DECLARE owner_id_val BIGINT; - DECLARE renter_id_val BIGINT; - DECLARE order_no_val VARCHAR(64); - DECLARE rent_amount_val BIGINT; - DECLARE deposit_val BIGINT; - DECLARE listing_min BIGINT; - DECLARE listing_max BIGINT; - DECLARE listing_pick BIGINT; - DECLARE verified_user_min BIGINT; - DECLARE verified_user_max BIGINT; - DECLARE user_pick BIGINT; - - SELECT MIN(id), MAX(id) - INTO listing_min, listing_max - FROM rental_listings - WHERE status IN ('published', 'active') AND review_status = 'approved'; - - SELECT MIN(id), MAX(id) - INTO verified_user_min, verified_user_max - FROM users - WHERE realname_status = 'verified'; - - IF listing_min IS NULL THEN - SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的已上架商品'; - END IF; - IF verified_user_min IS NULL THEN - SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的已实名用户'; - END IF; - - WHILE i <= batch_size DO - -- 近似随机选择一个上架商品,避免 ORDER BY RAND() 全表排序。 - SET listing_id_val = NULL; - SET listing_pick = listing_min + FLOOR(RAND() * (listing_max - listing_min + 1)); - SELECT rl.id, rl.account_id, rl.owner_id, rl.price_cent, rl.deposit_amount_cent - INTO listing_id_val, account_id_val, owner_id_val, rent_amount_val, deposit_val - FROM rental_listings rl - WHERE rl.id >= listing_pick - AND rl.status IN ('published', 'active') - AND rl.review_status = 'approved' - ORDER BY rl.id LIMIT 1; - - IF listing_id_val IS NULL THEN - SELECT rl.id, rl.account_id, rl.owner_id, rl.price_cent, rl.deposit_amount_cent - INTO listing_id_val, account_id_val, owner_id_val, rent_amount_val, deposit_val - FROM rental_listings rl - WHERE rl.status IN ('published', 'active') - AND rl.review_status = 'approved' - ORDER BY rl.id LIMIT 1; - END IF; - - -- 近似随机选择一个租客(不能是号主本人)。 - SET renter_id_val = NULL; - SET user_pick = verified_user_min + FLOOR(RAND() * (verified_user_max - verified_user_min + 1)); - SELECT id INTO renter_id_val FROM users - WHERE id >= user_pick AND id != owner_id_val AND realname_status = 'verified' - ORDER BY id LIMIT 1; - - IF renter_id_val IS NULL THEN - SELECT id INTO renter_id_val FROM users - WHERE id != owner_id_val AND realname_status = 'verified' - ORDER BY id LIMIT 1; - END IF; - - IF renter_id_val IS NULL THEN - SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的租客用户'; - END IF; - - SET order_no_val = CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(i, 8, '0')); - SET rent_amount_val = rent_amount_val * 24; -- 24小时租金 - - INSERT INTO rental_orders ( - order_no, listing_id, account_id, owner_id, renter_id, - estimated_duration_hours, rent_amount_cent, owner_rent_amount_cent, - deposit_amount_cent, deposit_original_amount_cent, platform_fee_cent, status, handoff_status, - settlement_status, rented_at, created_at - ) VALUES ( - order_no_val, - listing_id_val, - account_id_val, - owner_id_val, - renter_id_val, - 24, - rent_amount_val, - ROUND(rent_amount_val * 0.95), -- 号主实得95% - deposit_val, - deposit_val, - ROUND(rent_amount_val * 0.05), -- 平台5%手续费 - CASE (i % 10) - WHEN 0 THEN 'pending_payment' - WHEN 1 THEN 'cancelled' - WHEN 2 THEN 'closed' - ELSE 'completed' - END, - CASE (i % 10) - WHEN 0 THEN 'none' - WHEN 1 THEN 'none' - WHEN 2 THEN 'owner_delivered' - ELSE 'owner_received' - END, - CASE (i % 10) - WHEN 0 THEN 'unsettled' - WHEN 1 THEN 'unsettled' - ELSE 'settled' - END, - DATE_SUB(NOW(), INTERVAL (i % 60) DAY), - DATE_SUB(NOW(), INTERVAL (i % 60) DAY) - ); - - SET i = i + 1; - - IF i % 1000 = 0 THEN - COMMIT; - END IF; - END WHILE; - - COMMIT; -END$$ -DELIMITER ; - --- ============================================ --- 4. 批量生成钱包流水(100000条流水) --- ============================================ -DROP PROCEDURE IF EXISTS generate_wallet_ledger; -DELIMITER $$ -CREATE PROCEDURE generate_wallet_ledger(IN batch_size INT) -BEGIN - DECLARE i INT DEFAULT 1; - DECLARE user_id_val BIGINT; - DECLARE order_id_val BIGINT; - DECLARE ledger_no_val VARCHAR(64); - DECLARE amount_val BIGINT; - DECLARE user_min BIGINT; - DECLARE user_max BIGINT; - DECLARE order_min BIGINT; - DECLARE order_max BIGINT; - DECLARE user_pick BIGINT; - DECLARE order_pick BIGINT; - - SELECT MIN(id), MAX(id) INTO user_min, user_max FROM users; - SELECT MIN(id), MAX(id) INTO order_min, order_max FROM rental_orders; - - IF user_min IS NULL THEN - SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的用户'; - END IF; - - WHILE i <= batch_size DO - -- 近似随机选择用户,避免 ORDER BY RAND() 全表排序。 - SET user_id_val = NULL; - SET user_pick = user_min + FLOOR(RAND() * (user_max - user_min + 1)); - SELECT id INTO user_id_val FROM users - WHERE id >= user_pick - ORDER BY id LIMIT 1; - - IF user_id_val IS NULL THEN - SELECT id INTO user_id_val FROM users ORDER BY id LIMIT 1; - END IF; - - -- 随机关联订单(50%概率) - IF RAND() > 0.5 AND order_min IS NOT NULL THEN - SET order_id_val = NULL; - SET order_pick = order_min + FLOOR(RAND() * (order_max - order_min + 1)); - SELECT id INTO order_id_val FROM rental_orders - WHERE id >= order_pick - ORDER BY id LIMIT 1; - - IF order_id_val IS NULL THEN - SELECT id INTO order_id_val FROM rental_orders ORDER BY id LIMIT 1; - END IF; - ELSE - SET order_id_val = NULL; - END IF; - - SET ledger_no_val = CONCAT('LDG', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(i, 6, '0')); - SET amount_val = ((i % 500) * 100) + FLOOR(RAND() * 10000); - - INSERT INTO wallet_ledger ( - ledger_no, user_id, order_id, direction, amount_cent, - balance_after_cent, balance_type, biz_type, biz_no, remark, created_at - ) VALUES ( - ledger_no_val, - user_id_val, - order_id_val, - CASE WHEN i % 2 = 0 THEN 'in' ELSE 'out' END, - amount_val, - 100000 + (i % 1000) * 100, - CASE WHEN i % 5 = 0 THEN 'frozen' ELSE 'available' END, - CASE (i % 6) - WHEN 0 THEN 'rent_payment' - WHEN 1 THEN 'deposit_freeze' - WHEN 2 THEN 'settlement' - WHEN 3 THEN 'refund' - WHEN 4 THEN 'recharge' - ELSE 'withdraw' - END, - CONCAT('BIZ', LPAD(i, 10, '0')), - CONCAT('测试流水', i), - DATE_SUB(NOW(), INTERVAL (i % 180) DAY) - ); - - SET i = i + 1; - - IF i % 1000 = 0 THEN - COMMIT; - END IF; - END WHILE; - - COMMIT; -END$$ -DELIMITER ; - --- ============================================ --- 5. 批量生成聊天消息(50000条消息) --- ============================================ -DROP PROCEDURE IF EXISTS generate_chat_messages; -DELIMITER $$ -CREATE PROCEDURE generate_chat_messages(IN batch_size INT) -BEGIN - DECLARE i INT DEFAULT 1; - DECLARE conv_id BIGINT; - DECLARE user_id_val BIGINT; - DECLARE conv_min BIGINT; - DECLARE conv_max BIGINT; - DECLARE user_min BIGINT; - DECLARE user_max BIGINT; - DECLARE conv_pick BIGINT; - DECLARE user_pick BIGINT; - - SELECT MIN(id), MAX(id) INTO conv_min, conv_max FROM chat_conversations; - SELECT MIN(id), MAX(id) INTO user_min, user_max FROM users; - - IF conv_min IS NULL THEN - SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的聊天会话'; - END IF; - IF user_min IS NULL THEN - SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的用户'; - END IF; - - WHILE i <= batch_size DO - -- 近似随机选择一个会话和发送者,避免 ORDER BY RAND() 全表排序。 - SET conv_id = NULL; - SET conv_pick = conv_min + FLOOR(RAND() * (conv_max - conv_min + 1)); - SELECT id INTO conv_id FROM chat_conversations - WHERE id >= conv_pick - ORDER BY id LIMIT 1; - - IF conv_id IS NULL THEN - SELECT id INTO conv_id FROM chat_conversations ORDER BY id LIMIT 1; - END IF; - - IF conv_id IS NOT NULL THEN - SET user_id_val = NULL; - SET user_pick = user_min + FLOOR(RAND() * (user_max - user_min + 1)); - SELECT id INTO user_id_val FROM users - WHERE id >= user_pick - ORDER BY id LIMIT 1; - - IF user_id_val IS NULL THEN - SELECT id INTO user_id_val FROM users ORDER BY id LIMIT 1; - END IF; - - INSERT INTO chat_messages ( - conversation_id, sender_type, sender_id, sender_role, - content_type, content, created_at - ) VALUES ( - conv_id, - 'user', - user_id_val, - CASE WHEN i % 2 = 0 THEN 'owner' ELSE 'renter' END, - 'text', - CONCAT('这是测试消息', i, ',内容随机生成用于压力测试'), - DATE_SUB(NOW(), INTERVAL (i % 30) DAY) - ); - END IF; - - SET i = i + 1; - - IF i % 1000 = 0 THEN - COMMIT; - END IF; - END WHILE; - - COMMIT; -END$$ -DELIMITER ; - --- ============================================ --- 本文件只创建存储过程,不直接生成数据。 --- 请使用 scripts/stress_test.sh data 按参数生成,避免意外重复造数。 --- ============================================ - -SET FOREIGN_KEY_CHECKS = 1; diff --git a/scripts/load_test_data_optimized.sql b/scripts/load_test_data_optimized.sql deleted file mode 100644 index 994fe85..0000000 --- a/scripts/load_test_data_optimized.sql +++ /dev/null @@ -1,510 +0,0 @@ --- ============================================ --- 优化的压力测试数据生成脚本 --- 使用批量生成 + 临时表,避免循环中的随机查询 --- ============================================ - -SET NAMES utf8mb4; -SET FOREIGN_KEY_CHECKS = 0; - --- ============================================ --- 1. 批量生成用户数据 --- ============================================ -DROP PROCEDURE IF EXISTS generate_users_batch; -DELIMITER $$ -CREATE PROCEDURE generate_users_batch(IN batch_size INT) -BEGIN - DECLARE batch_limit INT DEFAULT 1000; - DECLARE batches INT; - DECLARE current_batch INT DEFAULT 0; - DECLARE batch_start INT; - DECLARE batch_end INT; - DECLARE current_limit INT; - - SET batches = CEIL(batch_size / batch_limit); - - WHILE current_batch < batches DO - SET batch_start = current_batch * batch_limit + 1; - SET batch_end = LEAST((current_batch + 1) * batch_limit, batch_size); - SET current_limit = batch_end - batch_start + 1; - - -- 使用 INSERT ... SELECT 批量生成 - INSERT INTO users (phone, nickname, realname_status, risk_status, credit_score, status, created_at) - SELECT - CONCAT('138', LPAD(seq, 8, '0')) as phone, - CONCAT('测试用户', seq) as nickname, - CASE WHEN seq % 10 = 0 THEN 'unverified' ELSE 'verified' END as realname_status, - CASE WHEN seq % 100 = 0 THEN 'frozen' ELSE 'normal' END as risk_status, - 80 + (seq % 20) as credit_score, - 'active' as status, - DATE_SUB(NOW(), INTERVAL (seq % 365) DAY) as created_at - FROM ( - SELECT @row := @row + 1 AS seq - FROM - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1, - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2, - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3, - (SELECT @row := batch_start - 1) r - LIMIT current_limit - ) seqs - ON DUPLICATE KEY UPDATE id=id; - - SET current_batch = current_batch + 1; - COMMIT; - END WHILE; - - -- 批量生成实名记录(为已实名用户) - INSERT INTO user_realname (user_id, provider, status, masked_name, masked_id_no, verified_at) - SELECT - u.id, - 'mock', - 'success', - CONCAT('张*', CHAR(65 + (u.id % 26))), - CONCAT('3301**********', LPAD(u.id % 10000, 4, '0')), - DATE_SUB(NOW(), INTERVAL (u.id % 300) DAY) - FROM users u - WHERE u.realname_status = 'verified' - AND NOT EXISTS (SELECT 1 FROM user_realname WHERE user_id = u.id); - - -- 批量生成钱包账户 - INSERT INTO wallet_accounts (user_id, available_balance_cent, frozen_balance_cent, status) - SELECT - u.id, - (u.id % 10) * 10000, - (u.id % 5) * 5000, - 'active' - FROM users u - WHERE NOT EXISTS (SELECT 1 FROM wallet_accounts WHERE user_id = u.id); - - COMMIT; -END$$ -DELIMITER ; - --- ============================================ --- 2. 批量生成游戏账号和商品 --- ============================================ -DROP PROCEDURE IF EXISTS generate_listings_batch; -DELIMITER $$ -CREATE PROCEDURE generate_listings_batch(IN batch_size INT) -BEGIN - DECLARE batch_limit INT DEFAULT 1000; - DECLARE batches INT; - DECLARE current_batch INT DEFAULT 0; - DECLARE verified_user_count INT DEFAULT 0; - - -- 创建临时表存储已实名用户ID - DROP TEMPORARY TABLE IF EXISTS tmp_verified_users; - CREATE TEMPORARY TABLE tmp_verified_users ( - id BIGINT PRIMARY KEY, - row_num INT - ); - - INSERT INTO tmp_verified_users (id, row_num) - SELECT id, (@rn := @rn + 1) as row_num - FROM users, (SELECT @rn := 0) init - WHERE realname_status = 'verified' - ORDER BY id; - - SELECT COUNT(*) INTO verified_user_count FROM tmp_verified_users; - - SET batches = CEIL(batch_size / batch_limit); - - WHILE current_batch < batches DO - -- 批量生成游戏账号 - INSERT INTO game_accounts ( - owner_id, game_name, server_region, login_platform, - title, description, rank_level, haf_coin_amount, status - ) - SELECT - u.id as owner_id, - 'delta_force', - CASE (seq % 4) - WHEN 0 THEN '亚服' - WHEN 1 THEN '美服' - WHEN 2 THEN '欧服' - ELSE '国服' - END, - CASE (seq % 3) - WHEN 0 THEN 'Steam' - WHEN 1 THEN 'Epic' - ELSE 'WeGame' - END, - CONCAT('账号', seq, ' 高分段'), - CONCAT('这是一个测试账号,编号', seq), - CASE (seq % 5) - WHEN 0 THEN '青铜' - WHEN 1 THEN '白银' - WHEN 2 THEN '黄金' - WHEN 3 THEN '铂金' - ELSE '钻石' - END, - (seq % 100) * 10000, - 'published' - FROM ( - SELECT @row2 := @row2 + 1 AS seq - FROM - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1, - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2, - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3, - (SELECT @row2 := current_batch * batch_limit) r - LIMIT batch_limit - ) seqs - INNER JOIN tmp_verified_users u ON u.row_num = (seq % verified_user_count) + 1 - WHERE seq <= batch_size; - - -- 批量生成租号商品 - INSERT INTO rental_listings ( - listing_no, account_id, owner_id, price_cent, deposit_amount_cent, - in_transaction, status, review_status, published_at - ) - SELECT - CONCAT( - DATE_FORMAT(DATE_SUB(NOW(), INTERVAL (ga.id % 90) DAY), '%Y%m%d'), - LPAD(FLOOR((ga.id - 1) / 90) + 1, 4, '0') - ) as listing_no, - ga.id as account_id, - ga.owner_id, - 500 + ((ga.id % 20) * 50) as price_cent, - 10000 + ((ga.id % 10) * 5000) as deposit_amount_cent, - CASE WHEN ga.id % 10 = 0 THEN 1 ELSE 0 END as in_transaction, - CASE - WHEN ga.id % 20 = 0 THEN 'offline' - WHEN ga.id % 15 = 0 THEN 'draft' - ELSE 'published' - END as status, - CASE - WHEN ga.id % 15 = 0 THEN 'pending' - WHEN ga.id % 30 = 0 THEN 'rejected' - ELSE 'approved' - END as review_status, - DATE_SUB(NOW(), INTERVAL (ga.id % 90) DAY) as published_at - FROM game_accounts ga - WHERE ga.id > (SELECT COALESCE(MAX(account_id), 0) FROM rental_listings) - LIMIT batch_limit; - - SET current_batch = current_batch + 1; - COMMIT; - END WHILE; - - INSERT INTO listing_no_sequences (biz_date, next_seq) - SELECT LEFT(listing_no, 8), MAX(CAST(SUBSTRING(listing_no, 9) AS UNSIGNED)) - FROM rental_listings - GROUP BY LEFT(listing_no, 8) - ON DUPLICATE KEY UPDATE next_seq = GREATEST(next_seq, VALUES(next_seq)); - - COMMIT; - - DROP TEMPORARY TABLE IF EXISTS tmp_verified_users; -END$$ -DELIMITER ; - --- ============================================ --- 3. 批量生成订单数据(优化版) --- ============================================ -DROP PROCEDURE IF EXISTS generate_orders_batch; -DELIMITER $$ -CREATE PROCEDURE generate_orders_batch(IN batch_size INT) -BEGIN - DECLARE batch_limit INT DEFAULT 1000; - DECLARE batches INT; - DECLARE current_batch INT DEFAULT 0; - DECLARE available_listing_count INT DEFAULT 0; - DECLARE verified_user_count INT DEFAULT 0; - - -- 创建临时表:可用商品 - DROP TEMPORARY TABLE IF EXISTS tmp_available_listings; - CREATE TEMPORARY TABLE tmp_available_listings ( - id BIGINT PRIMARY KEY, - account_id BIGINT, - owner_id BIGINT, - price_cent BIGINT, - deposit_amount_cent BIGINT, - row_num INT - ); - - INSERT INTO tmp_available_listings (id, account_id, owner_id, price_cent, deposit_amount_cent, row_num) - SELECT id, account_id, owner_id, price_cent, deposit_amount_cent, (@rn := @rn + 1) - FROM rental_listings, (SELECT @rn := 0) init - WHERE status IN ('published', 'active') AND review_status = 'approved' - ORDER BY id; - - SELECT COUNT(*) INTO available_listing_count FROM tmp_available_listings; - - -- 创建临时表:已实名用户 - DROP TEMPORARY TABLE IF EXISTS tmp_verified_users; - CREATE TEMPORARY TABLE tmp_verified_users ( - id BIGINT PRIMARY KEY, - row_num INT - ); - - INSERT INTO tmp_verified_users (id, row_num) - SELECT id, (@rn2 := @rn2 + 1) - FROM users, (SELECT @rn2 := 0) init - WHERE realname_status = 'verified' - ORDER BY id; - - SELECT COUNT(*) INTO verified_user_count FROM tmp_verified_users; - - SET batches = CEIL(batch_size / batch_limit); - - WHILE current_batch < batches DO - INSERT INTO rental_orders ( - order_no, listing_id, account_id, owner_id, renter_id, - estimated_duration_hours, rent_amount_cent, owner_rent_amount_cent, - deposit_amount_cent, deposit_original_amount_cent, platform_fee_cent, status, handoff_status, - settlement_status, rented_at, created_at - ) - SELECT - CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(seq, 8, '0')) as order_no, - l.id as listing_id, - l.account_id, - l.owner_id, - r.id as renter_id, - 24 as estimated_duration_hours, - l.price_cent * 24 as rent_amount_cent, - ROUND(l.price_cent * 24 * 0.95) as owner_rent_amount_cent, - l.deposit_amount_cent, - l.deposit_amount_cent, - ROUND(l.price_cent * 24 * 0.05) as platform_fee_cent, - CASE (seq % 10) - WHEN 0 THEN 'pending_payment' - WHEN 1 THEN 'cancelled' - WHEN 2 THEN 'closed' - ELSE 'completed' - END as status, - CASE (seq % 10) - WHEN 0 THEN 'none' - WHEN 1 THEN 'none' - WHEN 2 THEN 'owner_delivered' - ELSE 'owner_received' - END as handoff_status, - CASE (seq % 10) - WHEN 0 THEN 'unsettled' - WHEN 1 THEN 'unsettled' - ELSE 'settled' - END as settlement_status, - DATE_SUB(NOW(), INTERVAL (seq % 60) DAY) as rented_at, - DATE_SUB(NOW(), INTERVAL (seq % 60) DAY) as created_at - FROM ( - SELECT @row3 := @row3 + 1 AS seq - FROM - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1, - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2, - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3, - (SELECT @row3 := current_batch * batch_limit) r - LIMIT batch_limit - ) seqs - INNER JOIN tmp_available_listings l ON l.row_num = (seq % available_listing_count) + 1 - INNER JOIN tmp_verified_users r ON r.row_num = (seq % verified_user_count) + 1 - WHERE seq <= batch_size AND r.id != l.owner_id - LIMIT batch_limit; - - SET current_batch = current_batch + 1; - COMMIT; - END WHILE; - - DROP TEMPORARY TABLE IF EXISTS tmp_available_listings; - DROP TEMPORARY TABLE IF EXISTS tmp_verified_users; -END$$ -DELIMITER ; - --- ============================================ --- 4. 批量生成钱包流水 --- ============================================ -DROP PROCEDURE IF EXISTS generate_wallet_ledger_batch; -DELIMITER $$ -CREATE PROCEDURE generate_wallet_ledger_batch(IN batch_size INT) -BEGIN - DECLARE batch_limit INT DEFAULT 1000; - DECLARE batches INT; - DECLARE current_batch INT DEFAULT 0; - DECLARE user_count INT DEFAULT 0; - DECLARE order_count INT DEFAULT 0; - - -- 创建临时表:用户列表 - DROP TEMPORARY TABLE IF EXISTS tmp_users; - CREATE TEMPORARY TABLE tmp_users ( - id BIGINT PRIMARY KEY, - row_num INT - ); - - INSERT INTO tmp_users (id, row_num) - SELECT id, (@rn := @rn + 1) - FROM users, (SELECT @rn := 0) init - ORDER BY id; - - SELECT COUNT(*) INTO user_count FROM tmp_users; - - -- 创建临时表:订单列表 - DROP TEMPORARY TABLE IF EXISTS tmp_orders; - CREATE TEMPORARY TABLE tmp_orders ( - id BIGINT PRIMARY KEY, - row_num INT - ); - - INSERT INTO tmp_orders (id, row_num) - SELECT id, (@rn2 := @rn2 + 1) - FROM rental_orders, (SELECT @rn2 := 0) init - ORDER BY id; - - SELECT COUNT(*) INTO order_count FROM tmp_orders; - - SET batches = CEIL(batch_size / batch_limit); - - WHILE current_batch < batches DO - INSERT INTO wallet_ledger ( - ledger_no, user_id, order_id, direction, amount_cent, - balance_after_cent, balance_type, biz_type, biz_no, remark, created_at - ) - SELECT - CONCAT('LDG', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(seq, 6, '0')) as ledger_no, - u.id as user_id, - IF(seq % 2 = 0, o.id, NULL) as order_id, - CASE WHEN seq % 2 = 0 THEN 'in' ELSE 'out' END as direction, - ((seq % 500) * 100) + seq as amount_cent, - 100000 + ((seq % 1000) * 100) as balance_after_cent, - CASE WHEN seq % 5 = 0 THEN 'frozen' ELSE 'available' END as balance_type, - CASE (seq % 6) - WHEN 0 THEN 'rent_payment' - WHEN 1 THEN 'deposit_freeze' - WHEN 2 THEN 'settlement' - WHEN 3 THEN 'refund' - WHEN 4 THEN 'recharge' - ELSE 'withdraw' - END as biz_type, - CONCAT('BIZ', LPAD(seq, 10, '0')) as biz_no, - CONCAT('测试流水', seq) as remark, - DATE_SUB(NOW(), INTERVAL (seq % 180) DAY) as created_at - FROM ( - SELECT @row4 := @row4 + 1 AS seq - FROM - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1, - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2, - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3, - (SELECT @row4 := current_batch * batch_limit) r - LIMIT batch_limit - ) seqs - INNER JOIN tmp_users u ON u.row_num = (seq % user_count) + 1 - LEFT JOIN tmp_orders o ON o.row_num = (seq % order_count) + 1 - WHERE seq <= batch_size - LIMIT batch_limit; - - SET current_batch = current_batch + 1; - COMMIT; - END WHILE; - - DROP TEMPORARY TABLE IF EXISTS tmp_users; - DROP TEMPORARY TABLE IF EXISTS tmp_orders; -END$$ -DELIMITER ; - --- ============================================ --- 5. 批量生成聊天会话和消息 --- ============================================ -DROP PROCEDURE IF EXISTS generate_chat_data_batch; -DELIMITER $$ -CREATE PROCEDURE generate_chat_data_batch(IN message_count INT) -BEGIN - DECLARE batch_limit INT DEFAULT 1000; - DECLARE batches INT; - DECLARE current_batch INT DEFAULT 0; - DECLARE conversation_count INT DEFAULT 0; - DECLARE user_count INT DEFAULT 0; - - -- 先生成聊天会话(基于订单) - INSERT INTO chat_conversations (order_id, type, title, status, last_message_at) - SELECT - ro.id, - 'order_group', - CONCAT('订单', ro.order_no, '群聊'), - 'active', - ro.created_at - FROM rental_orders ro - WHERE NOT EXISTS (SELECT 1 FROM chat_conversations WHERE order_id = ro.id) - LIMIT 1000 - ON DUPLICATE KEY UPDATE order_id=order_id; - - DROP TEMPORARY TABLE IF EXISTS tmp_conversations; - CREATE TEMPORARY TABLE tmp_conversations ( - id BIGINT PRIMARY KEY, - row_num INT - ); - - INSERT INTO tmp_conversations (id, row_num) - SELECT id, (@rn := @rn + 1) - FROM chat_conversations, (SELECT @rn := 0) init - ORDER BY id; - - SELECT COUNT(*) INTO conversation_count FROM tmp_conversations; - - DROP TEMPORARY TABLE IF EXISTS tmp_users; - CREATE TEMPORARY TABLE tmp_users ( - id BIGINT PRIMARY KEY, - row_num INT - ); - - INSERT INTO tmp_users (id, row_num) - SELECT id, (@rn2 := @rn2 + 1) - FROM users, (SELECT @rn2 := 0) init - ORDER BY id; - - SELECT COUNT(*) INTO user_count FROM tmp_users; - - SET batches = CEIL(message_count / batch_limit); - - WHILE current_batch < batches DO - INSERT INTO chat_messages ( - conversation_id, sender_type, sender_id, sender_role, - content_type, content, created_at - ) - SELECT - c.id as conversation_id, - 'user' as sender_type, - u.id as sender_id, - CASE WHEN seq % 2 = 0 THEN 'owner' ELSE 'renter' END as sender_role, - 'text' as content_type, - CONCAT('这是测试消息', seq, ',内容随机生成用于压力测试') as content, - DATE_SUB(NOW(), INTERVAL (seq % 30) DAY) as created_at - FROM ( - SELECT @row5 := @row5 + 1 AS seq - FROM - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1, - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2, - (SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 - UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3, - (SELECT @row5 := current_batch * batch_limit) r - LIMIT batch_limit - ) seqs - INNER JOIN tmp_conversations c ON c.row_num = (seq % conversation_count) + 1 - INNER JOIN tmp_users u ON u.row_num = (seq % user_count) + 1 - WHERE seq <= message_count - LIMIT batch_limit; - - SET current_batch = current_batch + 1; - COMMIT; - END WHILE; - - DROP TEMPORARY TABLE IF EXISTS tmp_conversations; - DROP TEMPORARY TABLE IF EXISTS tmp_users; -END$$ -DELIMITER ; - --- ============================================ --- 本文件只创建存储过程,不直接生成数据。 --- 请使用 scripts/stress_test.sh data 按参数生成 --- ============================================ - -SET FOREIGN_KEY_CHECKS = 1; diff --git a/scripts/stress_test.sh b/scripts/stress_test.sh deleted file mode 100755 index 1ec918f..0000000 --- a/scripts/stress_test.sh +++ /dev/null @@ -1,482 +0,0 @@ -#!/bin/bash - -# 压力测试快速启动脚本 - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -# 颜色输出 -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -function log_info() { - echo -e "${GREEN}[INFO]${NC} $1" -} - -function log_warn() { - echo -e "${YELLOW}[WARN]${NC} $1" -} - -function log_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -function show_usage() { - cat << EOF -压力测试脚本(优化版) - -用法: - $0 [command] [options] - -命令: - data 生成测试数据(使用优化的批量生成) - test 执行压力测试(支持真实认证) - monitor 监控系统性能 - clean 清理测试数据 - report 生成压测报告 - all 执行完整流程(生成数据 + 压测 + 报告) - -选项: - 数据生成: - -u, --users NUM 生成用户数量(默认: 1000) - -l, --listings NUM 生成商品数量(默认: 5000) - -o, --orders NUM 生成订单数量(默认: 3000) - --ledger NUM 生成钱包流水数量(默认: 10000) - --chat-messages NUM 生成聊天消息数量(默认: 5000) - --use-optimized 使用优化的数据生成脚本(推荐) - - 压力测试: - -c, --concurrency NUM 并发数(默认: 50) - -d, --duration SEC 测试时长/秒(默认: 60) - -s, --scenario NAME 测试场景: - - realistic: 真实业务场景(默认) - - admin: 管理后台场景 - - listing_only: 仅商品查询 - --gradual 启用梯度压测 - --warmup NUM 预热用户数(默认: 100) - --use-improved 使用改进的压测工具(支持真实认证) - --url URL 后端地址(默认: http://localhost:8080) - - 通用: - -h, --help 显示帮助信息 - -示例: - # 小规模数据生成(使用优化脚本) - $0 data --use-optimized - - # 真实场景压测(100个用户token,50并发,持续60秒) - $0 test --use-improved -c 50 -d 60 -s realistic --warmup 100 - - # 管理后台压测 - $0 test --use-improved -c 30 -d 120 -s admin - - # 梯度压测(逐步增加负载) - $0 test --use-improved --gradual -d 60 - - # 执行完整流程(优化版) - $0 all --use-optimized --use-improved - - # 清理测试数据 - $0 clean - -EOF -} - -# 默认参数 -USERS=1000 -LISTINGS=5000 -ORDERS=3000 -LEDGER=10000 -CHAT_MESSAGES=5000 -CONCURRENCY=50 -DURATION=60 -SCENARIO="realistic" -BASE_URL="http://localhost:8080" -USE_OPTIMIZED=false -USE_IMPROVED=true -GRADUAL=false -WARMUP=100 - -# 解析命令行参数 -COMMAND="" -while [[ $# -gt 0 ]]; do - case $1 in - data|test|monitor|clean|report|all) - COMMAND="$1" - shift - ;; - -u|--users) - USERS="$2" - shift 2 - ;; - -l|--listings) - LISTINGS="$2" - shift 2 - ;; - -o|--orders) - ORDERS="$2" - shift 2 - ;; - --ledger) - LEDGER="$2" - shift 2 - ;; - --chat-messages) - CHAT_MESSAGES="$2" - shift 2 - ;; - -c|--concurrency) - CONCURRENCY="$2" - shift 2 - ;; - -d|--duration) - DURATION="$2" - shift 2 - ;; - -s|--scenario) - SCENARIO="$2" - shift 2 - ;; - --url) - BASE_URL="$2" - shift 2 - ;; - --use-optimized) - USE_OPTIMIZED=true - shift - ;; - --use-improved) - USE_IMPROVED=true - shift - ;; - --gradual) - GRADUAL=true - shift - ;; - --warmup) - WARMUP="$2" - shift 2 - ;; - -h|--help) - show_usage - exit 0 - ;; - *) - log_error "未知参数: $1" - show_usage - exit 1 - ;; - esac -done - -if [ -z "$COMMAND" ]; then - log_error "请指定命令" - show_usage - exit 1 -fi - -# 检查数据库连接 -function check_database() { - log_info "检查数据库连接..." - if docker exec hfb-mysql mysql -uhfb -psecret -e "SELECT 1" &>/dev/null; then - log_info "数据库连接正常" - return 0 - else - log_error "数据库连接失败,请确保 Docker 容器正在运行" - log_info "提示: 执行 'docker-compose -f deploy/docker-compose.dev.yml up -d' 启动服务" - return 1 - fi -} - -# 检查后端服务 -function check_backend() { - log_info "检查后端服务..." - if curl -s "$BASE_URL/health" &>/dev/null; then - log_info "后端服务正常" - return 0 - else - log_warn "后端服务未响应: $BASE_URL" - log_info "提示: 确保后端服务已启动" - return 1 - fi -} - -# 生成测试数据 -function generate_data() { - log_info "开始生成测试数据..." - log_info "配置: 用户=$USERS, 商品=$LISTINGS, 订单=$ORDERS, 钱包流水=$LEDGER, 聊天消息=$CHAT_MESSAGES" - - check_database || exit 1 - - # 选择使用的SQL脚本 - SQL_FILE="$SCRIPT_DIR/load_test_data.sql" - if [ "$USE_OPTIMIZED" = true ]; then - SQL_FILE="$SCRIPT_DIR/load_test_data_optimized.sql" - log_info "使用优化的数据生成脚本" - fi - - # 创建临时SQL文件 - TMP_SQL="/tmp/load_test_data_$(date +%s).sql" - trap 'rm -f "$TMP_SQL"' RETURN - - if [ "$USE_OPTIMIZED" = true ]; then - cat > "$TMP_SQL" << EOF --- 优化版数据生成 -USE hfb_sys; - --- 调用批量生成存储过程 -CALL generate_users_batch($USERS); -CALL generate_listings_batch($LISTINGS); -CALL generate_orders_batch($ORDERS); -CALL generate_wallet_ledger_batch($LEDGER); -CALL generate_chat_data_batch($CHAT_MESSAGES); - --- 显示统计 -SELECT '用户数' as item, COUNT(*) as count FROM users -UNION ALL SELECT '游戏账号数', COUNT(*) FROM game_accounts -UNION ALL SELECT '商品数', COUNT(*) FROM rental_listings -UNION ALL SELECT '订单数', COUNT(*) FROM rental_orders -UNION ALL SELECT '钱包流水数', COUNT(*) FROM wallet_ledger -UNION ALL SELECT '聊天会话数', COUNT(*) FROM chat_conversations -UNION ALL SELECT '聊天消息数', COUNT(*) FROM chat_messages; -EOF - else - cat > "$TMP_SQL" << EOF --- 原始版数据生成 -USE hfb_sys; - --- 调用存储过程生成数据 -CALL generate_users($USERS); -CALL generate_listings($LISTINGS); -CALL generate_orders($ORDERS); -CALL generate_wallet_ledger($LEDGER); - --- 生成聊天会话 -INSERT INTO chat_conversations (order_id, type, title, status, last_message_at) -SELECT id, 'order_group', CONCAT('订单', order_no, '群聊'), 'active', created_at -FROM rental_orders -WHERE id <= 1000 -ON DUPLICATE KEY UPDATE order_id=order_id; - -CALL generate_chat_messages($CHAT_MESSAGES); - --- 显示统计 -SELECT '用户数' as item, COUNT(*) as count FROM users -UNION ALL SELECT '游戏账号数', COUNT(*) FROM game_accounts -UNION ALL SELECT '商品数', COUNT(*) FROM rental_listings -UNION ALL SELECT '订单数', COUNT(*) FROM rental_orders -UNION ALL SELECT '钱包流水数', COUNT(*) FROM wallet_ledger -UNION ALL SELECT '聊天会话数', COUNT(*) FROM chat_conversations -UNION ALL SELECT '聊天消息数', COUNT(*) FROM chat_messages; -EOF - fi - - log_info "执行数据生成..." - - # 先执行基础SQL创建存储过程 - docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < "$SQL_FILE" - - # 执行数据生成 - docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < "$TMP_SQL" - - rm -f "$TMP_SQL" - trap - RETURN - - log_info "测试数据生成完成!" -} - -# 执行压力测试 -function run_stress_test() { - log_info "开始压力测试..." - log_info "配置: 场景=$SCENARIO, 并发=$CONCURRENCY, 时长=${DURATION}秒, 目标=$BASE_URL" - - check_backend || log_warn "后端服务未响应,测试可能失败" - - # 编译压测工具 - log_info "编译压测工具..." - cd "$SCRIPT_DIR" - - go build -o stress_test load_stress.go - - if [ $? -ne 0 ]; then - log_error "压测工具编译失败" - return 1 - fi - - # 执行压测 - log_info "开始执行压力测试..." - - if [ "$GRADUAL" = true ]; then - ./stress_test -url "$BASE_URL" -c "$CONCURRENCY" -d "$DURATION" -s "$SCENARIO" -warmup "$WARMUP" -gradual - else - ./stress_test -url "$BASE_URL" -c "$CONCURRENCY" -d "$DURATION" -s "$SCENARIO" -warmup "$WARMUP" - fi - - log_info "压力测试完成!" -} - -# 监控系统性能 -function monitor_system() { - log_info "开始监控系统性能(按 Ctrl+C 停止)..." - - echo "" - echo "=== Docker 容器资源使用 ===" - docker stats --no-stream hfb-backend hfb-mysql hfb-redis 2>/dev/null || log_warn "部分容器未运行" - - echo "" - echo "=== MySQL 连接数 ===" - docker exec hfb-mysql mysql -uhfb -psecret -e "SHOW STATUS LIKE 'Threads_connected';" 2>/dev/null - - echo "" - echo "=== MySQL 慢查询统计 ===" - docker exec hfb-mysql mysql -uhfb -psecret -e "SHOW STATUS LIKE 'Slow_queries';" 2>/dev/null - - echo "" - echo "=== Redis 信息 ===" - docker exec hfb-redis redis-cli INFO stats | grep -E "total_connections_received|total_commands_processed|instantaneous_ops_per_sec" 2>/dev/null - - echo "" - log_info "持续监控请使用: docker stats" -} - -# 清理测试数据 -function clean_data() { - log_warn "即将清理所有测试数据,此操作不可恢复!" - read -p "确认继续?(yes/no): " confirm - - if [ "$confirm" != "yes" ]; then - log_info "已取消清理操作" - exit 0 - fi - - check_database || exit 1 - - log_info "开始清理测试数据..." - - docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys << 'EOF' -SET FOREIGN_KEY_CHECKS = 0; - --- 清理测试数据(保留ID < 1000的数据) -DELETE FROM chat_messages WHERE id > 100; -DELETE FROM chat_conversations WHERE id > 100; -DELETE FROM wallet_ledger WHERE id > 100; -DELETE FROM rental_orders WHERE id > 100; -DELETE FROM rental_listings WHERE id > 100; -DELETE FROM game_accounts WHERE id > 100; -DELETE FROM users WHERE id > 1000; - --- 重置自增ID -ALTER TABLE users AUTO_INCREMENT = 1001; -ALTER TABLE game_accounts AUTO_INCREMENT = 101; -ALTER TABLE rental_listings AUTO_INCREMENT = 101; -ALTER TABLE rental_orders AUTO_INCREMENT = 101; -ALTER TABLE wallet_ledger AUTO_INCREMENT = 101; -ALTER TABLE chat_conversations AUTO_INCREMENT = 101; -ALTER TABLE chat_messages AUTO_INCREMENT = 101; - -SET FOREIGN_KEY_CHECKS = 1; - --- 显示清理后的统计 -SELECT '用户数' as item, COUNT(*) as count FROM users -UNION ALL SELECT '商品数', COUNT(*) FROM rental_listings -UNION ALL SELECT '订单数', COUNT(*) FROM rental_orders -UNION ALL SELECT '钱包流水数', COUNT(*) FROM wallet_ledger; -EOF - - log_info "测试数据清理完成!" -} - -# 生成压测报告 -function generate_report() { - log_info "生成压测报告..." - - REPORT_FILE="$PROJECT_ROOT/stress_test_report_$(date +%Y%m%d_%H%M%S).txt" - - { - echo "===================================" - echo "压力测试报告" - echo "生成时间: $(date '+%Y-%m-%d %H:%M:%S')" - echo "===================================" - echo "" - - echo "--- 数据库统计 ---" - docker exec hfb-mysql mysql -uhfb -psecret hfb_sys -e " - SELECT '用户数' as item, COUNT(*) as count FROM users - UNION ALL SELECT '商品数', COUNT(*) FROM rental_listings - UNION ALL SELECT '订单数', COUNT(*) FROM rental_orders - UNION ALL SELECT '钱包流水', COUNT(*) FROM wallet_ledger; - " 2>/dev/null - - echo "" - echo "--- MySQL 性能指标 ---" - docker exec hfb-mysql mysql -uhfb -psecret -e " - SHOW STATUS LIKE 'Threads_connected'; - SHOW STATUS LIKE 'Slow_queries'; - SHOW STATUS LIKE 'Questions'; - " 2>/dev/null - - echo "" - echo "--- 表大小统计 ---" - docker exec hfb-mysql mysql -uhfb -psecret hfb_sys -e " - SELECT - table_name AS '表名', - ROUND(((data_length + index_length) / 1024 / 1024), 2) AS '大小(MB)', - table_rows AS '行数' - FROM information_schema.TABLES - WHERE table_schema = 'hfb_sys' - ORDER BY (data_length + index_length) DESC - LIMIT 10; - " 2>/dev/null - - } > "$REPORT_FILE" - - cat "$REPORT_FILE" - - log_info "报告已保存到: $REPORT_FILE" -} - -# 执行完整流程 -function run_all() { - log_info "执行完整压测流程..." - - generate_data - sleep 3 - - run_stress_test - sleep 2 - - generate_report - - log_info "完整流程执行完成!" -} - -# 执行命令 -case $COMMAND in - data) - generate_data - ;; - test) - run_stress_test - ;; - monitor) - monitor_system - ;; - clean) - clean_data - ;; - report) - generate_report - ;; - all) - run_all - ;; - *) - log_error "未知命令: $COMMAND" - show_usage - exit 1 - ;; -esac