增加测试 修复错误
This commit is contained in:
@@ -8,4 +8,3 @@ default-character-set = utf8mb4
|
|||||||
character-set-server = utf8mb4
|
character-set-server = utf8mb4
|
||||||
collation-server = utf8mb4_unicode_ci
|
collation-server = utf8mb4_unicode_ci
|
||||||
init-connect = 'SET NAMES utf8mb4'
|
init-connect = 'SET NAMES utf8mb4'
|
||||||
skip-character-set-client-handshake
|
|
||||||
|
|||||||
@@ -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/)
|
||||||
@@ -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`
|
||||||
@@ -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;
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
Executable
+393
@@ -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
|
||||||
Reference in New Issue
Block a user