Files
dafuweng-saiadmin6.x/server/docs/PERFORMANCE_AND_QPS_ANALYSIS_CN.md

294 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 游戏性能与 QPS 分析Dice 项目)
> 本文基于当前骰子游戏dice项目的实际代码结构对后端接口的性能瓶颈、QPS 能力和优化方向做说明,便于后续压测、扩容和排查问题。
---
## 1. 运行环境与架构概览
| 项目 | 说明 |
|----------------|----------------------------------------------------------------------|
| 应用框架 | Webman基于 Workerman 的常驻内存 PHP 框架) |
| 业务模块 | `app/dice`(玩家、抽奖记录、奖励配置、奖池配置、看板等) |
| HTTP 监听地址 | 通常为 `http://0.0.0.0:6688` |
| Worker 数量 | 推荐:`cpu_count() * 4`(例如 8 核 CPU → 32 个 Worker 进程) |
| 数据库 | MySQL + ThinkORM使用连接池例如 max=20 / min=2 |
| 缓存 | Redis + think-cache使用连接池例如 max=20 / min=2 |
**生产环境配置建议:**
-`.env` 中统一使用 Redis 作为默认缓存:`CACHE_MODE=redis`
- 调整 `DB_POOL_MAX` / `REDIS_POOL_MAX`,使其与 Worker 数量和 MySQL `max_connections` 匹配
---
## 2. 核心接口与负载特征
从当前项目代码来看,游戏相关接口大致可以分为三类:
- **高负载型**:强依赖 DB + Redis多次写入或复杂逻辑QPS 瓶颈主要在这里)
- **中负载型**:部分写 DB 或复杂读 DB
- **轻量型**:读缓存或简单 DB 查询
### 2.1 主要接口梳理
| 接口 | 用途 | 负载级别 | 说明 / 风险点 |
|-------------------------------------|--------------------|----------|--------------------------------------------------------|
| `POST /api/game/playStart` | 开始一局游戏 / 抽奖 | 高 | 多次 DB + Redis 操作,是整体 QPS 的核心瓶颈 |
| `POST /api/game/buyLotteryTickets` | 购买门票 / 次数 | 中-高 | 多表写入:门票记录、钱包记录等 |
| `GET /api/game/config` | 获取游戏配置 | 中 | 依赖 `DiceConfig`,若无缓存会有全表扫描风险 |
| `GET /api/game/lotteryPool` | 获取奖池 / 奖励配置 | 中 | 依赖 `DiceLotteryPoolConfig` / `DiceRewardConfig` |
| `POST /api/v1/getGameUrl` | 获取游戏 URL | 中 | JWT + Redis 鉴权DB 较少Redis 压力较大 |
| `POST /api/v1/getPlayerInfo` | 获取玩家信息 | 中 | 需要 `dice_player.username` 索引,否则易产生慢查询 |
| `POST /api/v1/getPlayerGameRecord` | 获取玩家历史记录 | 中-高 | 若未优化,容易出现 N+1 查询问题 |
| `POST /api/v1/setPlayerWallet` | 调整玩家钱包余额 | 中 | 涉及余额变更与流水写入,需处理好并发与幂等 |
在进行 QPS 能力评估或性能优化时,优先关注:
- `playStart` 接口;
- 门票 / 钱包 / 流水相关接口。
---
## 3. `playStart` 调用链与资源消耗
以下是典型的一次 `playStart` 请求在后端的大致流程(抽象描述):
### 3.1 数据库访问路径(典型情况)
1. **加载玩家信息**
- 调用 `DicePlayer::find($playerId)`
- 建议:每个请求只查询一次,并在后续逻辑中复用该对象。
2. **加载奖励 EV / 最小实际 EV**
- 调用 `DiceRewardConfig::getCachedMinRealEv()`
- 首次会访问 DB 并写入 Redis后续命中 Redis 即可。
3. **获取或创建奖池LotteryService**
- 调用 `LotteryService::getOrCreate()`
- 优先从 Redis 中读取奖池信息;
- 若缓存不存在,则从 DB 中加载:如 `DiceLotteryPoolConfig::where('name','default/killScore')->find()`
4. **根据本次玩法加载奖池配置**
- 调用 `DiceLotteryPoolConfig::find($configId)`
5. **核心业务落库(可能在事务中执行)**
- `DicePlayRecord::create()`:写入本次抽奖记录;
- `DicePlayer::save()`:更新玩家余额、状态等;
- `DicePlayerTicketRecord::create()`:写入门票/次数流水;
- `DiceLotteryPoolConfig::where('id')->update(['ev' => Db::raw(...)]`:更新 EV 统计;
- `DicePlayerWalletRecord::create()`:写入钱包流水。
6. **刷新玩家缓存**
- 再次使用(或重新加载)玩家信息,调用 `UserCache::setUser($player)` 同步到 Redis。
**DB / Redis 调用次数粗略估算:**
- 在写法合理、缓存命中良好的情况下,每次 `playStart` 大致会产生:
- 3 ~ 6 次 DB 访问(读 + 写);
- 3 ~ 6 次 Redis 操作。
- 若在同一请求中多次重复调用 `DicePlayer::find($playerId)`DB 调用次数和耗时都会显著上升。
### 3.2 Redis 使用路径
在当前设计中Redis 主要用于:
- 奖励 / 奖池配置缓存:`DiceRewardConfig::getCachedInstance()``getCachedByTier()``LotteryService::getOrCreate()` 等;
- 玩家缓存:`UserCache::getUser()``UserCache::setUser()`
- 统计计数:可以使用 Redis `INCRBY` 等命令维护 EV 或其他统计数据。
**目标:** 热路径尽量命中 RedisDB 主要负责落库和冷数据读取。
---
## 4. 性能瓶颈与优化方向
### 4.1 数据库连接与 SQL
1. **连接池大小**
- 在 32 个 Worker 且 `DB_POOL_MAX=20` 的情况下,高并发时容易出现连接耗尽和等待。
- 建议:根据 CPU 核心数和目标 QPS`DB_POOL_MAX` 调整到 **32 ~ 64** 左右,并保证 MySQL `max_connections` 足够大。
2. **避免重复查询**
-`playStart` 中,玩家信息应只查询一次:`$player = DicePlayer::find($playerId)`,后续逻辑统一使用 `$player`,避免重复 `find`
3. **EV 更新策略**
- 频繁在在线请求中执行 `UPDATE dice_lottery_pool_config SET ev = ev - ?`,会造成该行热点锁竞争;
- 建议:
- 在线请求仅将 EV 变动累加到 Redis 计数器;
- 通过定时任务批量同步 Redis 中的统计数据回 MySQL。
4. **索引覆盖**
- 高频查询条件(如 `username``player_id``create_time``status` 等)必须有合适索引(见第 6 节)。
### 4.2 缓存与序列化开销
1. **统一使用 Redis 缓存驱动**
- `.env` 中设置:`CACHE_MODE=redis`
- `config/cache.php` 中,生产环境默认缓存驱动应为 Redis。
2. **UserCache 加解密成本**
-`UserCache` 中对玩家信息做 AES 加解密,在高 QPS 场景下会占用较多 CPU
- 可以:
- 精简缓存字段,只缓存必要信息;
- 控制 `setUser` 调用频率,避免无意义重复写入;
- 评估哪些字段确实需要加密,非敏感字段可不加密。
### 4.3 N+1 查询问题
历史记录、钱包流水等接口非常容易出现 N+1 查询,例如:
- 循环按玩家逐条查询记录;
- 循环对每条记录再单独查询玩家信息。
建议:
- 使用 `whereIn('player_id', $playerIds)` 做批量查询;
- 使用 `with(['dicePlayer'])` 或 join 预加载关联的玩家信息;
- 控制单页 `limit`,例如不超过 100 条。
### 4.4 Redis 与 DB 的整体协同
- Worker 数、DB 连接池、Redis 连接池三者要相互匹配:
- 出现 CPU 未打满但 DB/Redis 已经“顶满”的情况,多半是配置不平衡。
- 对 Redis 的监控要关注:
- 慢日志(慢命令);
- 大 Key 或热点 Key必要时做拆分或分桶
---
## 5. 单机 QPS 能力(估算)
> 以下为在常见生产环境(如 8C32G 单机、MySQL/Redis 与应用在同一内网)且做好上述优化后的大致量级,仅用于评估和对比,实际仍需通过压测确认。
### 5.1 单接口预估
- `playStart`
- 单次耗时:约 50 ~ 150 ms受 DB/Redis 延迟和业务分支影响);
- 单机 QPS**200 ~ 300**
- `buyLotteryTickets`
- 单次耗时:约 20 ~ 60 ms
- 单机 QPS**500 ~ 1000+**
- `getGameUrl` / 登录类接口
- 单次耗时:约 30 ~ 80 ms
- 单机 QPS**400 ~ 800**
### 5.2 混合业务场景
假设:
- 70% 请求是 `playStart`
- 30% 请求为 `getGameUrl``getPlayerInfo` 等轻 / 中量接口;
在 8 核 32 Worker、索引和缓存配置合理的前提下
- 单机综合 QPS 约可达到 **250 ~ 400 QPS**
- 若需要显著更高的整体 QPS应采用多节点水平扩展 + DB/Redis 扩容(读写分离、分库分表等)。
---
## 6. 索引与表结构建议
### 6.1 必备索引
| 表名 | 索引 | 用途说明 |
|------------------------------|-----------------------------------|--------------------------------|
| `dice_player` | 唯一索引 `unique(username)` | 通过用户名快速定位玩家 |
| `dice_play_record` | 普通索引 `(player_id, create_time)` | 玩家历史记录分页 + 时间排序 |
| `dice_player_wallet_record` | 普通索引 `(player_id, create_time)` | 钱包流水查询 |
| `dice_player_ticket_record` | 普通索引 `(player_id, create_time)` | 门票流水查询 |
示例 SQL
```sql
ALTER TABLE dice_player ADD UNIQUE INDEX uk_username (username);
ALTER TABLE dice_play_record ADD INDEX idx_player_create (player_id, create_time);
ALTER TABLE dice_player_wallet_record ADD INDEX idx_player_create (player_id, create_time);
ALTER TABLE dice_player_ticket_record ADD INDEX idx_player_create (player_id, create_time);
```
### 6.2 大表策略
对于体量较大的历史表(抽奖记录、钱包流水、门票流水等),建议提前规划:
- 分区或分表策略(按时间或按玩家 ID
- 归档 / 删除过旧数据,例如只保留最近 N 个月的在线查询。
---
## 7. 压测步骤与指标观测
### 7.1 压测 `playStart`
1. 准备数据:
- 预先创建足够多的测试玩家及初始余额;
- 获取一批可用的 JWT token。
2. 使用 `ab` 压测示例:
```bash
ab -n 2000 -c 32 \
-p post_body.json \
-T application/json \
-H "token: YOUR_JWT" \
http://127.0.0.1:6688/api/game/playStart
```
3. 使用 `wrk` 压测示例:
```bash
wrk -t4 -c32 -d60s -s play_start.lua http://127.0.0.1:6688/api/game/playStart
```
4. 重点关注指标:
- QPSRequests per second
- P95 / P99 延迟;
- 错误率HTTP 5xx、超时、业务失败
- MySQL / Redis 的 CPU 使用率与连接数。
### 7.2 常见问题排查思路
1. **QPS 上不去CPU 却不高**
- 检查 DB / Redis 连接池是否经常“打满”;
- 是否存在慢 SQL全表扫描、缺索引
- 是否有外部依赖(如第三方接口)拖慢整体。
2. **P99 延迟偶尔飙高**
- 查看是否有大事务 / 大批量更新与压测同时进行;
- 观察磁盘 I/O 和 GC 情况;
- 检查 Redis 是否有大 Key 或阻塞命令(如 `KEYS`)。
---
## 8. 配置与代码自查清单
> 建议在压测前和上线前都按此清单进行一次自查。
### 8.1 配置层面
- `.env`
- [ ] `CACHE_MODE=redis` 已开启;
- [ ] `DB_POOL_MAX` / `DB_POOL_MIN` 已根据 CPU 与 MySQL `max_connections` 调整;
- [ ] `REDIS_POOL_MAX` 能够支撑预期并发量。
- MySQL
- [ ] `max_connections` 不小于所有应用实例 DB 连接池上限之和;
- [ ] 已开启慢查询日志,并设置了合理阈值。
### 8.2 代码层面
- [ ] `playStart` 在一次请求中只加载一次玩家信息并全程复用;
- [ ] 奖励/奖池配置统一通过缓存访问,修改后有刷新缓存逻辑(如 `refreshCache()`
- [ ] 历史记录、钱包流水接口避免 N+1 查询,使用批量查询或预加载;
- [ ] EV 等高频统计字段不直接在在线请求中频繁 `UPDATE`,而是通过 Redis 聚合后批量回写。
---
## 9. 总结
1. 对于当前 dice 项目,`playStart` 以及与之相关的奖池、钱包、门票逻辑,是后端 QPS 能力的关键瓶颈所在。
2. 通过合理配置 DB/Redis 连接池、充分利用缓存、补齐必要索引、减少重复查询和热点 UPDATE可以大幅提升单机可承载的 QPS并降低 P99 延迟。
3. 真正的容量边界必须通过 `ab` / `wrk` / `k6` 等工具在接近真实数据和流量模型的情况下进行压测验证,本文档主要为定位瓶颈和指导优化提供一个工程实践层面的参考。