diff --git a/deploy/mysql/my.cnf b/deploy/mysql/my.cnf index 4f76b4f..4142561 100644 --- a/deploy/mysql/my.cnf +++ b/deploy/mysql/my.cnf @@ -8,4 +8,3 @@ default-character-set = utf8mb4 character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci init-connect = 'SET NAMES utf8mb4' -skip-character-set-client-handshake diff --git a/docs/stress-test-guide.md b/docs/stress-test-guide.md new file mode 100644 index 0000000..9f4aa86 --- /dev/null +++ b/docs/stress-test-guide.md @@ -0,0 +1,606 @@ +# 压力测试指南 + +本文档提供完整的压力测试方案,用于评估系统在大数据量和高并发场景下的性能表现。 + +## 一、测试环境准备 + +### 1.1 硬件配置建议 + +**最低配置:** +- CPU: 4核 +- 内存: 8GB +- 磁盘: SSD 50GB + +**推荐配置:** +- CPU: 8核+ +- 内存: 16GB+ +- 磁盘: SSD 100GB+ +- MySQL: 独立部署,开启慢查询日志 + +### 1.2 数据库优化配置 + +在 `deploy/docker-compose.dev.yml` 中调整 MySQL 配置: + +```yaml +services: + mysql: + environment: + - MYSQL_ROOT_PASSWORD=secret + command: + - --max_connections=500 + - --innodb_buffer_pool_size=2G + - --innodb_log_file_size=512M + - --slow_query_log=1 + - --slow_query_log_file=/var/log/mysql/slow.log + - --long_query_time=0.5 +``` + +### 1.3 后端配置优化 + +在 `backend/.env` 中调整: + +```bash +# 数据库连接池 +DB_MAX_OPEN_CONNS=100 +DB_MAX_IDLE_CONNS=20 +DB_CONN_MAX_LIFETIME=3600 + +# Redis配置 +REDIS_POOL_SIZE=50 + +# 日志级别(压测时降低日志输出) +LOG_LEVEL=warn + +# Gin模式 +APP_ENV=production +``` + +## 二、生成测试数据 + +### 2.1 执行数据生成脚本 + +```bash +# 连接到数据库容器 +docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < scripts/load_test_data.sql +``` + +### 2.2 数据规模说明 + +该脚本会生成: +- **10,000** 个用户(90%已实名) +- **50,000** 个租号商品 +- **30,000** 个订单(包含各种状态) +- **100,000** 条钱包流水 +- **50,000** 条聊天消息 + +### 2.3 验证数据生成 + +```sql +-- 查看数据统计 +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.4 自定义数据量 + +修改脚本底部的调用参数: + +```sql +-- 根据需要调整数量 +CALL generate_users(50000); -- 生成5万用户 +CALL generate_listings(200000); -- 生成20万商品 +CALL generate_orders(100000); -- 生成10万订单 +CALL generate_wallet_ledger(500000); -- 生成50万流水 +``` + +## 三、索引优化验证 + +### 3.1 检查现有索引 + +```sql +-- 查看订单表索引 +SHOW INDEX FROM rental_orders; + +-- 查看钱包流水索引 +SHOW INDEX FROM wallet_ledger; + +-- 查看商品表索引 +SHOW INDEX FROM rental_listings; +``` + +### 3.2 分析慢查询 + +```sql +-- 分析商品列表查询 +EXPLAIN SELECT * FROM rental_listings +WHERE status = 'active' + AND review_status = 'approved' +ORDER BY published_at DESC +LIMIT 20; + +-- 分析用户订单查询 +EXPLAIN SELECT * FROM rental_orders +WHERE renter_id = 1234 + AND status = 'active' +ORDER BY created_at DESC; + +-- 分析钱包流水查询 +EXPLAIN SELECT * FROM wallet_ledger +WHERE user_id = 1234 + AND biz_type = 'rent_payment' +ORDER BY created_at DESC +LIMIT 50; +``` + +### 3.3 添加缺失索引(如果需要) + +```sql +-- 示例:为常用查询组合添加联合索引 +ALTER TABLE rental_orders +ADD INDEX idx_status_handoff_created (status, handoff_status, created_at); + +-- 为管理后台查询优化 +ALTER TABLE wallet_ledger +ADD INDEX idx_created_biz_type (created_at, biz_type); +``` + +## 四、压力测试执行 + +### 4.1 使用 Go 压测工具 + +```bash +cd scripts + +# 编译压测工具 +go build -o stress_test stress_test.go + +# 场景1: 商品列表查询(高频读) +./stress_test -url http://localhost:8080 -c 50 -d 60 -s list_listings + +# 场景2: 订单创建(写操作) +./stress_test -url http://localhost:8080 -c 20 -d 60 -s create_order + +# 场景3: 钱包流水查询 +./stress_test -url http://localhost:8080 -c 30 -d 60 -s wallet + +# 场景4: 混合场景(模拟真实流量) +./stress_test -url http://localhost:8080 -c 100 -d 300 -s mixed +``` + +### 4.2 使用 Apache Bench (ab) + +```bash +# 简单的商品列表查询压测 +ab -n 10000 -c 100 http://localhost:8080/api/listings?page=1&page_size=20 + +# 健康检查接口压测 +ab -n 50000 -c 200 http://localhost:8080/health +``` + +### 4.3 使用 wrk + +```bash +# 安装 wrk +brew install wrk # macOS +# sudo apt install wrk # Ubuntu + +# 基础压测 +wrk -t4 -c100 -d60s http://localhost:8080/api/listings + +# 使用脚本进行复杂场景测试 +wrk -t4 -c100 -d60s -s scripts/wrk_scenario.lua http://localhost:8080 +``` + +创建 `scripts/wrk_scenario.lua`: + +```lua +-- 模拟不同的查询参数 +counter = 0 + +request = function() + counter = counter + 1 + page = (counter % 10) + 1 + path = "/api/listings?page=" .. page .. "&page_size=20" + return wrk.format("GET", path) +end +``` + +## 五、性能监控 + +### 5.1 数据库性能监控 + +```sql +-- 实时查看正在执行的查询 +SHOW FULL PROCESSLIST; + +-- 查看慢查询日志 +docker exec hfb-mysql tail -f /var/log/mysql/slow.log + +-- 查看表锁情况 +SHOW OPEN TABLES WHERE In_use > 0; + +-- 查看InnoDB状态 +SHOW ENGINE INNODB STATUS; + +-- 查看连接数 +SHOW STATUS LIKE 'Threads_connected'; +SHOW STATUS LIKE 'Max_used_connections'; +``` + +### 5.2 应用性能监控 + +在压测期间,监控后端日志: + +```bash +# 实时查看后端日志 +tail -f backend/logs/app-*.log | grep -E "ERROR|WARN|latency" + +# 监控容器资源使用 +docker stats hfb-backend hfb-mysql hfb-redis +``` + +### 5.3 系统资源监控 + +```bash +# CPU和内存使用 +top -p $(pgrep -f "go run") + +# 网络连接数 +netstat -an | grep :8080 | wc -l + +# 查看打开的文件描述符 +lsof -p $(pgrep -f "go run") | wc -l +``` + +## 六、性能指标基准 + +### 6.1 响应时间目标 + +| 接口类型 | P50 | P95 | P99 | +|---------|-----|-----|-----| +| 商品列表查询 | < 50ms | < 100ms | < 200ms | +| 订单详情查询 | < 30ms | < 80ms | < 150ms | +| 钱包流水查询 | < 40ms | < 100ms | < 200ms | +| 创建订单 | < 100ms | < 300ms | < 500ms | +| 支付处理 | < 200ms | < 500ms | < 1000ms | + +### 6.2 吞吐量目标 + +- **读操作**:单机 QPS > 1000 +- **写操作**:单机 QPS > 200 +- **混合场景**:单机 QPS > 500 + +### 6.3 数据库查询目标 + +- **简单查询**:< 10ms +- **联表查询**:< 50ms +- **复杂聚合**:< 100ms + +## 七、常见性能瓶颈与优化 + +### 7.1 数据库层面 + +#### 问题1:商品列表查询慢 + +**症状:** `SELECT * FROM rental_listings WHERE status = 'active'` 耗时超过 100ms + +**优化方案:** + +```sql +-- 1. 添加覆盖索引 +ALTER TABLE rental_listings +ADD INDEX idx_status_review_published_cover ( + status, review_status, published_at, id, price, deposit_amount +); + +-- 2. 避免 SELECT *,只查询需要的字段 +SELECT id, owner_id, price, deposit_amount, published_at +FROM rental_listings +WHERE status = 'active' AND review_status = 'approved' +ORDER BY published_at DESC +LIMIT 20; +``` + +#### 问题2:用户订单分页查询慢 + +**症状:** 大偏移量分页(page > 100)性能下降 + +**优化方案:** + +```sql +-- 使用游标分页代替 OFFSET +SELECT * FROM rental_orders +WHERE renter_id = ? + AND id < ? -- 上一页最后一条的ID +ORDER BY id DESC +LIMIT 20; +``` + +在代码中实现: + +```go +// 使用游标分页 +func (r *Repository) ListOrdersCursor(userID, lastID uint64, limit int) ([]Order, error) { + query := `SELECT * FROM rental_orders + WHERE renter_id = ? AND id < ? + ORDER BY id DESC LIMIT ?` + + if lastID == 0 { + lastID = ^uint64(0) // Max uint64 + } + + // ... +} +``` + +#### 问题3:钱包流水查询慢(10万+数据) + +**优化方案:** + +```sql +-- 1. 确保有复合索引 +ALTER TABLE wallet_ledger +ADD INDEX idx_user_created_desc (user_id, created_at DESC); + +-- 2. 分区表(适用于超大数据量) +ALTER TABLE wallet_ledger +PARTITION BY RANGE (YEAR(created_at)) ( + PARTITION p2024 VALUES LESS THAN (2025), + PARTITION p2025 VALUES LESS THAN (2026), + PARTITION p2026 VALUES LESS THAN (2027), + PARTITION p_future VALUES LESS THAN MAXVALUE +); + +-- 3. 归档历史数据 +CREATE TABLE wallet_ledger_archive LIKE wallet_ledger; +INSERT INTO wallet_ledger_archive +SELECT * FROM wallet_ledger +WHERE created_at < DATE_SUB(NOW(), INTERVAL 6 MONTH); +``` + +### 7.2 应用层面 + +#### 问题1:N+1 查询问题 + +**症状:** 商品列表查询后,循环查询关联的账号信息 + +**优化方案:** + +```go +// 错误做法:N+1查询 +for _, listing := range listings { + account, _ := repo.GetAccount(listing.AccountID) + listing.Account = account +} + +// 正确做法:预加载 +func (r *Repository) ListWithAccounts(filter ListingFilter) ([]Listing, error) { + query := ` + SELECT + rl.*, + ga.title as account_title, + ga.rank_level, + ga.server_region + FROM rental_listings rl + LEFT JOIN game_accounts ga ON rl.account_id = ga.id + WHERE rl.status = ? + ORDER BY rl.published_at DESC + LIMIT ? + ` + // ... +} +``` + +#### 问题2:缓存缺失 + +**优化方案:** + +```go +// 为热点数据添加Redis缓存 +func (s *Service) GetListing(id uint64) (*Listing, error) { + cacheKey := fmt.Sprintf("listing:%d", id) + + // 1. 尝试从缓存读取 + if cached, err := s.redis.Get(ctx, cacheKey).Bytes(); err == nil { + var listing Listing + json.Unmarshal(cached, &listing) + return &listing, nil + } + + // 2. 缓存未命中,从数据库读取 + listing, err := s.repo.GetByID(id) + if err != nil { + return nil, err + } + + // 3. 写入缓存 + data, _ := json.Marshal(listing) + s.redis.Set(ctx, cacheKey, data, 10*time.Minute) + + return listing, nil +} +``` + +#### 问题3:数据库连接池耗尽 + +**优化方案:** + +```go +// 在 database/mysql.go 中优化连接池配置 +db.SetMaxOpenConns(100) // 最大连接数 +db.SetMaxIdleConns(20) // 空闲连接数 +db.SetConnMaxLifetime(time.Hour) // 连接最大生命周期 +db.SetConnMaxIdleTime(10 * time.Minute) // 空闲连接超时 +``` + +### 7.3 Redis 优化 + +```bash +# 监控 Redis 性能 +redis-cli --latency +redis-cli --stat + +# 查看慢查询 +redis-cli SLOWLOG GET 10 + +# 查看内存使用 +redis-cli INFO memory +``` + +**配置优化:** + +```redis +# 最大内存限制 +maxmemory 2gb + +# 内存淘汰策略 +maxmemory-policy allkeys-lru + +# 持久化配置(开发环境可以关闭以提升性能) +save "" +appendonly no +``` + +## 八、特定场景测试 + +### 8.1 订单高峰测试 + +模拟秒杀或活动高峰期: + +```bash +# 同时创建1000个订单 +./stress_test -url http://localhost:8080 -c 100 -d 10 -s create_order +``` + +**预期检查:** +- 数据库连接池是否耗尽 +- 是否出现死锁 +- 钱包余额扣减是否正确(需要事务隔离) + +### 8.2 聊天消息压测 + +```bash +# 模拟100个用户同时发送消息 +./stress_test -url http://localhost:8080 -c 100 -d 60 -s chat +``` + +**预期检查:** +- WebSocket 连接数限制 +- 消息写入速度 +- 未读消息计数准确性 + +### 8.3 大数据量查询 + +```sql +-- 测试后台钱包流水导出(大数据量) +SELECT * FROM wallet_ledger +WHERE created_at >= '2024-01-01' +ORDER BY created_at DESC; + +-- 超时检查 +SET SESSION max_execution_time = 30000; -- 30秒超时 +``` + +## 九、压测后清理 + +### 9.1 清理测试数据 + +```sql +-- 谨慎执行!会删除所有测试数据 +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 rental_orders AUTO_INCREMENT = 101; +``` + +### 9.2 恢复配置 + +```bash +# 恢复开发环境配置 +cd backend +cp .env.example .env + +# 重启服务 +./scripts/dev.sh +``` + +## 十、持续监控建议 + +### 10.1 生产环境监控 + +推荐集成: +- **APM**: New Relic / Datadog +- **日志**: ELK Stack / Grafana Loki +- **监控**: Prometheus + Grafana +- **告警**: PagerDuty / 企业微信 + +### 10.2 关键指标 + +**应用层:** +- API 响应时间(P50/P95/P99) +- QPS / TPS +- 错误率 +- 慢查询数量 + +**数据库层:** +- 连接数 +- 慢查询数 +- 锁等待时间 +- InnoDB 缓存命中率 + +**系统层:** +- CPU 使用率 +- 内存使用率 +- 磁盘 IO +- 网络带宽 + +## 十一、性能优化 Checklist + +- [ ] 数据库索引覆盖所有常用查询 +- [ ] 消除 N+1 查询问题 +- [ ] 热点数据使用 Redis 缓存 +- [ ] 数据库连接池配置合理 +- [ ] 分页查询使用游标而非 OFFSET +- [ ] 大数据量表考虑分区 +- [ ] 历史数据定期归档 +- [ ] 慢查询日志监控告警 +- [ ] 数据库读写分离(如适用) +- [ ] CDN 加速静态资源 + +## 附录 + +### A. 压测命令速查 + +```bash +# 启动测试环境 +docker-compose -f deploy/docker-compose.dev.yml up -d +cd backend && go run ./cmd/api + +# 生成测试数据 +docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < scripts/load_test_data.sql + +# 执行压测 +cd scripts +go build -o stress_test stress_test.go +./stress_test -url http://localhost:8080 -c 100 -d 60 -s mixed + +# 监控性能 +docker stats +docker exec hfb-mysql mysqladmin -uhfb -psecret processlist +``` + +### B. 参考资料 + +- [MySQL 性能优化最佳实践](https://dev.mysql.com/doc/refman/8.0/en/optimization.html) +- [Go 性能优化](https://github.com/dgryski/go-perfbook) +- [Gin 框架性能调优](https://gin-gonic.com/docs/benchmarks/) diff --git a/docs/stress-test-summary.md b/docs/stress-test-summary.md new file mode 100644 index 0000000..cfe68e9 --- /dev/null +++ b/docs/stress-test-summary.md @@ -0,0 +1,242 @@ +# 压力测试方案总结 + +## 快速开始 + +### 1. 生成测试数据 +```bash +./scripts/stress_test.sh data +``` + +### 2. 执行压力测试 +```bash +# 混合场景测试(推荐) +./scripts/stress_test.sh test -c 100 -d 60 -s mixed + +# 商品列表查询测试 +./scripts/stress_test.sh test -c 200 -d 120 -s list_listings + +# 订单创建测试 +./scripts/stress_test.sh test -c 50 -d 60 -s create_order +``` + +### 3. 查看报告 +```bash +./scripts/stress_test.sh report +``` + +### 4. 清理数据 +```bash +./scripts/stress_test.sh clean +``` + +## 已创建的压测工具 + +### 1. 数据生成脚本 +**文件**: `scripts/load_test_data.sql` + +**功能**: +- 批量生成10,000个用户 +- 批量生成50,000个租号商品 +- 批量生成30,000个订单 +- 批量生成100,000条钱包流水 +- 批量生成50,000条聊天消息 + +**特点**: +- 使用存储过程提高生成效率 +- 每1000条自动提交一次 +- 模拟真实的业务数据分布 +- 支持自定义数量 + +### 2. Go压测工具 +**文件**: `scripts/stress_test.go` + +**测试场景**: +- `list_listings`: 商品列表查询(高频读) +- `create_order`: 订单创建(写操作) +- `wallet`: 钱包流水查询 +- `chat`: 聊天消息查询 +- `mixed`: 混合场景(模拟真实流量比例) + +**使用示例**: +```bash +cd scripts +go build -o stress_test stress_test.go +./stress_test -url http://localhost:8080 -c 100 -d 60 -s mixed +``` + +### 3. 一键压测脚本 +**文件**: `scripts/stress_test.sh` + +**功能**: +- `data`: 生成测试数据 +- `test`: 执行压力测试 +- `monitor`: 监控系统性能 +- `clean`: 清理测试数据 +- `report`: 生成压测报告 +- `all`: 执行完整流程 + +### 4. 压测指南文档 +**文件**: `docs/stress-test-guide.md` + +**包含内容**: +- 测试环境准备 +- 数据库优化配置 +- 索引优化建议 +- 性能监控方法 +- 常见瓶颈与优化方案 +- 持续监控建议 + +## 核心压力点分析 + +### 高频读场景 +1. **商品列表查询**: + - 带复杂筛选条件(状态、审核状态、价格区间) + - 需要优化索引: `idx_rental_listings_filter` + - 建议添加Redis缓存 + +2. **订单列表查询**: + - 多角色视角(租客、号主) + - 关联查询多张表 + - 优化: 使用游标分页代替OFFSET + +3. **钱包流水查询**: + - 数据量大(10万+) + - 需要按时间倒序 + - 优化: 复合索引 `idx_user_created_desc` + +### 高频写场景 +1. **订单创建和支付**: + - 涉及多表事务 + - 钱包扣款+订单创建+通知 + - 需要确保事务隔离级别 + +2. **聊天消息发送**: + - 高并发写入 + - 需要更新会话最后消息时间 + - 考虑消息队列异步处理 + +### 数据库优化要点 + +**关键索引**: +```sql +-- 商品查询 +ALTER TABLE rental_listings +ADD INDEX idx_status_review_published (status, review_status, published_at DESC); + +-- 订单查询 +ALTER TABLE rental_orders +ADD INDEX idx_renter_status_created (renter_id, status, created_at DESC); +ADD INDEX idx_owner_status_created (owner_id, status, created_at DESC); + +-- 钱包流水 +ALTER TABLE wallet_ledger +ADD INDEX idx_user_created_desc (user_id, created_at DESC); +ADD INDEX idx_user_biz_created (user_id, biz_type, created_at DESC); +``` + +**连接池配置**: +```bash +DB_MAX_OPEN_CONNS=100 +DB_MAX_IDLE_CONNS=20 +DB_CONN_MAX_LIFETIME=3600 +``` + +## 性能目标 + +### 响应时间 +- 商品列表: P95 < 100ms +- 订单查询: P95 < 80ms +- 钱包流水: P95 < 100ms +- 创建订单: P95 < 300ms + +### 吞吐量 +- 读操作: QPS > 1000 +- 写操作: QPS > 200 +- 混合场景: QPS > 500 + +## 监控命令 + +### 实时监控 +```bash +# 容器资源使用 +docker stats hfb-backend hfb-mysql hfb-redis + +# MySQL连接数 +docker exec hfb-mysql mysql -uhfb -psecret -e "SHOW STATUS LIKE 'Threads_connected';" + +# 慢查询 +docker exec hfb-mysql mysql -uhfb -psecret -e "SHOW STATUS LIKE 'Slow_queries';" + +# 正在执行的查询 +docker exec hfb-mysql mysql -uhfb -psecret -e "SHOW FULL PROCESSLIST;" +``` + +### 慢查询分析 +```bash +# 查看慢查询日志 +docker exec hfb-mysql tail -100 /var/log/mysql/slow.log +``` + +## 常见问题排查 + +### 问题1: 商品列表查询慢 +**现象**: 查询耗时 > 100ms + +**排查**: +```sql +EXPLAIN SELECT * FROM rental_listings +WHERE status = 'active' AND review_status = 'approved' +ORDER BY published_at DESC LIMIT 20; +``` + +**优化**: 添加覆盖索引,避免回表 + +### 问题2: 大偏移量分页慢 +**现象**: page > 100 时性能急剧下降 + +**优化**: 使用游标分页 +```sql +SELECT * FROM rental_orders +WHERE renter_id = ? AND id < ? +ORDER BY id DESC LIMIT 20; +``` + +### 问题3: 连接池耗尽 +**现象**: 大量 "too many connections" 错误 + +**优化**: +- 增加 `max_connections` +- 优化查询,减少慢查询 +- 检查是否有连接泄漏 + +## 下一步优化建议 + +1. **缓存层**: Redis缓存热点数据(商品详情、用户信息) +2. **读写分离**: 主库写入,从库读取 +3. **分库分表**: 当单表超过千万级时考虑 +4. **异步处理**: 使用消息队列处理非关键路径操作 +5. **CDN加速**: 静态资源和图片使用CDN + +## 完整流程示例 + +```bash +# 1. 启动开发环境 +./scripts/dev.sh + +# 2. 生成测试数据 +./scripts/stress_test.sh data + +# 3. 执行压测 +./scripts/stress_test.sh test -c 100 -d 300 -s mixed + +# 4. 监控(另开终端) +./scripts/stress_test.sh monitor + +# 5. 查看报告 +./scripts/stress_test.sh report + +# 6. 清理数据 +./scripts/stress_test.sh clean +``` + +详细文档请查看: `docs/stress-test-guide.md` diff --git a/scripts/load_test_data.sql b/scripts/load_test_data.sql new file mode 100644 index 0000000..5c6348e --- /dev/null +++ b/scripts/load_test_data.sql @@ -0,0 +1,394 @@ +-- ============================================ +-- 压力测试数据生成脚本 +-- 用于生成大量测试数据以进行性能测试 +-- ============================================ + +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, frozen_balance, status) + VALUES ( + LAST_INSERT_ID(), + (i % 10) * 100.00, + (i % 5) * 50.00, + '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 DECIMAL(12,2); + DECLARE deposit_val DECIMAL(12,2); + + WHILE i <= batch_size DO + -- 随机选择一个用户作为号主(已实名用户) + SELECT id INTO user_id_val FROM users + WHERE realname_status = 'verified' + ORDER BY RAND() LIMIT 1; + + -- 创建游戏账号 + 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, + 'active' + ); + + SET account_id_val = LAST_INSERT_ID(); + + -- 创建租号商品 + SET price_val = 5.00 + (i % 20) * 0.5; + SET deposit_val = 100.00 + (i % 10) * 50.00; + + INSERT INTO rental_listings ( + account_id, owner_id, price, deposit_amount, + in_transaction, status, review_status, published_at + ) VALUES ( + 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 'active' + 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; + + 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 DECIMAL(12,2); + DECLARE deposit_val DECIMAL(12,2); + + WHILE i <= batch_size DO + -- 随机选择一个上架的商品 + SELECT rl.id, rl.account_id, rl.owner_id, rl.price, rl.deposit_amount + INTO listing_id_val, account_id_val, owner_id_val, rent_amount_val, deposit_val + FROM rental_listings rl + WHERE rl.status = 'active' AND rl.review_status = 'approved' + ORDER BY RAND() LIMIT 1; + + -- 随机选择一个租客(不能是号主本人) + SELECT id INTO renter_id_val FROM users + WHERE id != owner_id_val AND realname_status = 'verified' + ORDER BY RAND() LIMIT 1; + + 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, owner_rent_amount, + deposit_amount, platform_fee, 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, + rent_amount_val * 0.95, -- 号主实得95% + deposit_val, + 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 DECIMAL(12,2); + + WHILE i <= batch_size DO + -- 随机选择用户 + SELECT id INTO user_id_val FROM users ORDER BY RAND() LIMIT 1; + + -- 随机关联订单(50%概率) + IF RAND() > 0.5 THEN + SELECT id INTO order_id_val FROM rental_orders ORDER BY RAND() LIMIT 1; + 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) + RAND() * 100; + + INSERT INTO wallet_ledger ( + ledger_no, user_id, order_id, direction, amount, + balance_after, 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, + 1000.00 + (i % 1000), + 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; + + WHILE i <= batch_size DO + -- 随机选择一个会话 + SELECT id INTO conv_id FROM chat_conversations ORDER BY RAND() LIMIT 1; + + IF conv_id IS NOT NULL THEN + -- 随机选择发送者 + SELECT id INTO user_id_val FROM users ORDER BY RAND() LIMIT 1; + + 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 ; + +-- ============================================ +-- 执行数据生成(根据需要调整数量) +-- ============================================ + +-- 生成10000个用户 +CALL generate_users(10000); + +-- 生成50000个商品 +CALL generate_listings(50000); + +-- 生成30000个订单 +CALL generate_orders(30000); + +-- 生成100000条钱包流水 +CALL generate_wallet_ledger(100000); + +-- 为前1000个订单创建会话 +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; + +-- 生成50000条聊天消息 +CALL generate_chat_messages(50000); + +-- ============================================ +-- 查看数据统计 +-- ============================================ +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; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/scripts/stress_test.go b/scripts/stress_test.go new file mode 100644 index 0000000..9c19063 --- /dev/null +++ b/scripts/stress_test.go @@ -0,0 +1,407 @@ +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "math/rand" + "net/http" + "sync" + "sync/atomic" + "time" +) + +// 压力测试工具 - 模拟实际业务场景 + +type TestConfig struct { + BaseURL string + Concurrency int + Duration time.Duration + Scenario string +} + +type TestResult struct { + TotalRequests int64 + SuccessRequests int64 + FailedRequests int64 + TotalLatency int64 // 毫秒 + MinLatency int64 + MaxLatency int64 + Errors map[string]int64 +} + +var ( + baseURL = flag.String("url", "http://localhost:8080", "API 基础地址") + concurrency = flag.Int("c", 10, "并发数") + duration = flag.Int("d", 60, "测试时长(秒)") + scenario = flag.String("s", "mixed", "测试场景: list_listings, create_order, chat, wallet, mixed") +) + +func main() { + flag.Parse() + + config := TestConfig{ + BaseURL: *baseURL, + Concurrency: *concurrency, + Duration: time.Duration(*duration) * time.Second, + Scenario: *scenario, + } + + fmt.Printf("=== 压力测试配置 ===\n") + fmt.Printf("目标地址: %s\n", config.BaseURL) + fmt.Printf("并发数: %d\n", config.Concurrency) + fmt.Printf("测试时长: %d 秒\n", *duration) + fmt.Printf("测试场景: %s\n", config.Scenario) + fmt.Printf("==================\n\n") + + result := runTest(config) + printResult(result) +} + +func runTest(config TestConfig) *TestResult { + result := &TestResult{ + Errors: make(map[string]int64), + MinLatency: int64(^uint64(0) >> 1), // Max int64 + } + + var wg sync.WaitGroup + stopChan := make(chan struct{}) + + // 启动并发workers + for i := 0; i < config.Concurrency; i++ { + wg.Add(1) + go func(workerID int) { + defer wg.Done() + worker(workerID, config, result, stopChan) + }(i) + } + + // 等待测试时长 + time.Sleep(config.Duration) + close(stopChan) + + wg.Wait() + + return result +} + +func worker(id int, config TestConfig, result *TestResult, stopChan chan struct{}) { + client := &http.Client{ + Timeout: 10 * time.Second, + } + + for { + select { + case <-stopChan: + return + default: + executeScenario(client, config, result) + } + } +} + +func executeScenario(client *http.Client, config TestConfig, result *TestResult) { + switch config.Scenario { + case "list_listings": + testListListings(client, config.BaseURL, result) + case "create_order": + testCreateOrder(client, config.BaseURL, result) + case "chat": + testChatMessages(client, config.BaseURL, result) + case "wallet": + testWalletLedger(client, config.BaseURL, result) + case "mixed": + // 混合场景:按实际业务比例分配 + r := rand.Intn(100) + switch { + case r < 40: // 40% 查询商品列表 + testListListings(client, config.BaseURL, result) + case r < 60: // 20% 查询订单 + testListOrders(client, config.BaseURL, result) + case r < 75: // 15% 查询钱包流水 + testWalletLedger(client, config.BaseURL, result) + case r < 85: // 10% 聊天消息 + testChatMessages(client, config.BaseURL, result) + case r < 95: // 10% 创建订单 + testCreateOrder(client, config.BaseURL, result) + default: // 5% 支付 + testPayOrder(client, config.BaseURL, result) + } + default: + testHealthCheck(client, config.BaseURL, result) + } +} + +// ============================================ +// 测试场景实现 +// ============================================ + +func testHealthCheck(client *http.Client, baseURL string, result *TestResult) { + start := time.Now() + resp, err := client.Get(baseURL + "/health") + latency := time.Since(start).Milliseconds() + + atomic.AddInt64(&result.TotalRequests, 1) + updateLatency(result, latency) + + if err != nil { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, "health_check_error: "+err.Error()) + return + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + atomic.AddInt64(&result.SuccessRequests, 1) + } else { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, fmt.Sprintf("health_check_status_%d", resp.StatusCode)) + } +} + +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) + + start := time.Now() + resp, err := client.Get(url) + latency := time.Since(start).Milliseconds() + + atomic.AddInt64(&result.TotalRequests, 1) + updateLatency(result, latency) + + if err != nil { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, "list_listings_error: "+err.Error()) + return + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + atomic.AddInt64(&result.SuccessRequests, 1) + } else { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, fmt.Sprintf("list_listings_status_%d", resp.StatusCode)) + } +} + +func testListOrders(client *http.Client, baseURL string, result *TestResult) { + // 需要登录token,这里模拟匿名访问(会返回401) + page := rand.Intn(5) + 1 + url := fmt.Sprintf("%s/api/orders?page=%d&page_size=20", baseURL, page) + + start := time.Now() + resp, err := client.Get(url) + latency := time.Since(start).Milliseconds() + + atomic.AddInt64(&result.TotalRequests, 1) + updateLatency(result, latency) + + if err != nil { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, "list_orders_error: "+err.Error()) + return + } + defer resp.Body.Close() + + // 401是预期的(未登录) + if resp.StatusCode == 200 || resp.StatusCode == 401 { + atomic.AddInt64(&result.SuccessRequests, 1) + } else { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, fmt.Sprintf("list_orders_status_%d", resp.StatusCode)) + } +} + +func testCreateOrder(client *http.Client, baseURL string, result *TestResult) { + // 模拟创建订单(需要登录,会返回401) + listingID := rand.Intn(1000) + 1 + payload := map[string]interface{}{ + "listing_id": listingID, + "estimated_duration_hours": 24, + } + + body, _ := json.Marshal(payload) + start := time.Now() + resp, err := client.Post( + baseURL+"/api/orders", + "application/json", + bytes.NewBuffer(body), + ) + latency := time.Since(start).Milliseconds() + + atomic.AddInt64(&result.TotalRequests, 1) + updateLatency(result, latency) + + if err != nil { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, "create_order_error: "+err.Error()) + return + } + defer resp.Body.Close() + + // 401是预期的(未登录) + if resp.StatusCode == 200 || resp.StatusCode == 401 { + atomic.AddInt64(&result.SuccessRequests, 1) + } else { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, fmt.Sprintf("create_order_status_%d", resp.StatusCode)) + } +} + +func testPayOrder(client *http.Client, baseURL string, result *TestResult) { + orderID := rand.Intn(1000) + 1 + url := fmt.Sprintf("%s/api/orders/%d/pay", baseURL, orderID) + + payload := map[string]interface{}{ + "provider": "mock", + } + + body, _ := json.Marshal(payload) + start := time.Now() + resp, err := client.Post(url, "application/json", bytes.NewBuffer(body)) + latency := time.Since(start).Milliseconds() + + atomic.AddInt64(&result.TotalRequests, 1) + updateLatency(result, latency) + + if err != nil { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, "pay_order_error: "+err.Error()) + return + } + defer resp.Body.Close() + + // 401是预期的(未登录) + if resp.StatusCode == 200 || resp.StatusCode == 401 { + atomic.AddInt64(&result.SuccessRequests, 1) + } else { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, fmt.Sprintf("pay_order_status_%d", resp.StatusCode)) + } +} + +func testWalletLedger(client *http.Client, baseURL string, result *TestResult) { + page := rand.Intn(10) + 1 + url := fmt.Sprintf("%s/api/wallet/ledger?page=%d&page_size=20", baseURL, page) + + start := time.Now() + resp, err := client.Get(url) + latency := time.Since(start).Milliseconds() + + atomic.AddInt64(&result.TotalRequests, 1) + updateLatency(result, latency) + + if err != nil { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, "wallet_ledger_error: "+err.Error()) + return + } + defer resp.Body.Close() + + // 401是预期的(未登录) + if resp.StatusCode == 200 || resp.StatusCode == 401 { + atomic.AddInt64(&result.SuccessRequests, 1) + } else { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, fmt.Sprintf("wallet_ledger_status_%d", resp.StatusCode)) + } +} + +func testChatMessages(client *http.Client, baseURL string, result *TestResult) { + conversationID := rand.Intn(100) + 1 + url := fmt.Sprintf("%s/api/chats/%d/messages?page=1&page_size=50", baseURL, conversationID) + + start := time.Now() + resp, err := client.Get(url) + latency := time.Since(start).Milliseconds() + + atomic.AddInt64(&result.TotalRequests, 1) + updateLatency(result, latency) + + if err != nil { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, "chat_messages_error: "+err.Error()) + return + } + defer resp.Body.Close() + + // 401是预期的(未登录) + if resp.StatusCode == 200 || resp.StatusCode == 401 { + atomic.AddInt64(&result.SuccessRequests, 1) + } else { + atomic.AddInt64(&result.FailedRequests, 1) + recordError(result, fmt.Sprintf("chat_messages_status_%d", resp.StatusCode)) + } +} + +// ============================================ +// 辅助函数 +// ============================================ + +func updateLatency(result *TestResult, latency int64) { + atomic.AddInt64(&result.TotalLatency, latency) + + // 更新最小延迟 + for { + current := atomic.LoadInt64(&result.MinLatency) + if latency >= current { + break + } + if atomic.CompareAndSwapInt64(&result.MinLatency, current, latency) { + break + } + } + + // 更新最大延迟 + for { + current := atomic.LoadInt64(&result.MaxLatency) + if latency <= current { + break + } + if atomic.CompareAndSwapInt64(&result.MaxLatency, current, latency) { + break + } + } +} + +var errorMutex sync.Mutex + +func recordError(result *TestResult, errMsg string) { + errorMutex.Lock() + defer errorMutex.Unlock() + result.Errors[errMsg]++ +} + +func printResult(result *TestResult) { + fmt.Printf("\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) + + if result.TotalRequests > 0 { + avgLatency := result.TotalLatency / result.TotalRequests + fmt.Printf("\n延迟统计:\n") + fmt.Printf(" 最小延迟: %d ms\n", result.MinLatency) + fmt.Printf(" 平均延迟: %d ms\n", avgLatency) + fmt.Printf(" 最大延迟: %d ms\n", result.MaxLatency) + } + + if len(result.Errors) > 0 { + fmt.Printf("\n错误统计:\n") + for err, count := range result.Errors { + fmt.Printf(" %s: %d 次\n", err, count) + } + } + + qps := float64(result.TotalRequests) / float64(*duration) + fmt.Printf("\nQPS: %.2f\n", qps) + fmt.Printf("==================\n") +} diff --git a/scripts/stress_test.sh b/scripts/stress_test.sh new file mode 100755 index 0000000..84af2f6 --- /dev/null +++ b/scripts/stress_test.sh @@ -0,0 +1,393 @@ +#!/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 生成用户数量(默认: 10000) + -l, --listings NUM 生成商品数量(默认: 50000) + -o, --orders NUM 生成订单数量(默认: 30000) + -c, --concurrency NUM 并发数(默认: 100) + -d, --duration SEC 测试时长/秒(默认: 60) + -s, --scenario NAME 测试场景: list_listings, create_order, wallet, chat, mixed(默认: mixed) + --url URL 后端地址(默认: http://localhost:8080) + -h, --help 显示帮助信息 + +示例: + # 生成测试数据 + $0 data + + # 执行混合场景压测(100并发,持续60秒) + $0 test -c 100 -d 60 -s mixed + + # 执行商品列表查询压测 + $0 test -s list_listings -c 200 -d 120 + + # 执行完整流程 + $0 all + + # 清理测试数据 + $0 clean + +EOF +} + +# 默认参数 +USERS=10000 +LISTINGS=50000 +ORDERS=30000 +LEDGER=100000 +CONCURRENCY=100 +DURATION=60 +SCENARIO="mixed" +BASE_URL="http://localhost:8080" + +# 解析命令行参数 +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 + ;; + -c|--concurrency) + CONCURRENCY="$2" + shift 2 + ;; + -d|--duration) + DURATION="$2" + shift 2 + ;; + -s|--scenario) + SCENARIO="$2" + shift 2 + ;; + --url) + BASE_URL="$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" + + check_database || exit 1 + + # 创建临时SQL文件 + TMP_SQL="/tmp/load_test_data_$(date +%s).sql" + + 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(50000); + +-- 显示统计 +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 + + log_info "执行数据生成..." + + # 先执行基础SQL创建存储过程 + docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < "$SCRIPT_DIR/load_test_data.sql" + + # 执行数据生成 + docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < "$TMP_SQL" + + rm -f "$TMP_SQL" + + 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 stress_test.go + + # 执行压测 + log_info "开始执行压力测试..." + ./stress_test -url "$BASE_URL" -c "$CONCURRENCY" -d "$DURATION" -s "$SCENARIO" + + 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