12 KiB
12 KiB
游戏性能与 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 数量和 MySQLmax_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 数据库访问路径(典型情况)
-
加载玩家信息
- 调用
DicePlayer::find($playerId) - 建议:每个请求只查询一次,并在后续逻辑中复用该对象。
- 调用
-
加载奖励 EV / 最小实际 EV
- 调用
DiceRewardConfig::getCachedMinRealEv() - 首次会访问 DB 并写入 Redis;后续命中 Redis 即可。
- 调用
-
获取或创建奖池(LotteryService)
- 调用
LotteryService::getOrCreate():- 优先从 Redis 中读取奖池信息;
- 若缓存不存在,则从 DB 中加载:如
DiceLotteryPoolConfig::where('type', 0/1)->find()。
- 调用
-
根据本次玩法加载奖池配置
- 调用
DiceLotteryPoolConfig::find($configId)。
- 调用
-
核心业务落库(可能在事务中执行)
DicePlayRecord::create():写入本次抽奖记录;DicePlayer::save():更新玩家余额、状态等;DicePlayerTicketRecord::create():写入门票/次数流水;DiceLotteryPoolConfig::where('id')->update(['ev' => Db::raw(...)]:更新 EV 统计;DicePlayerWalletRecord::create():写入钱包流水。
-
刷新玩家缓存
- 再次使用(或重新加载)玩家信息,调用
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 或其他统计数据。
目标: 热路径尽量命中 Redis,DB 主要负责落库和冷数据读取。
4. 性能瓶颈与优化方向
4.1 数据库连接与 SQL
-
连接池大小
- 在 32 个 Worker 且
DB_POOL_MAX=20的情况下,高并发时容易出现连接耗尽和等待。 - 建议:根据 CPU 核心数和目标 QPS,将
DB_POOL_MAX调整到 32 ~ 64 左右,并保证 MySQLmax_connections足够大。
- 在 32 个 Worker 且
-
避免重复查询
- 在
playStart中,玩家信息应只查询一次:$player = DicePlayer::find($playerId),后续逻辑统一使用$player,避免重复find。
- 在
-
EV 更新策略
- 频繁在在线请求中执行
UPDATE dice_lottery_config SET ev = ev - ?,会造成该行热点锁竞争; - 建议:
- 在线请求仅将 EV 变动累加到 Redis 计数器;
- 通过定时任务批量同步 Redis 中的统计数据回 MySQL。
- 频繁在在线请求中执行
-
索引覆盖
- 高频查询条件(如
username、player_id、create_time、status等)必须有合适索引(见第 6 节)。
- 高频查询条件(如
4.2 缓存与序列化开销
-
统一使用 Redis 缓存驱动
.env中设置:CACHE_MODE=redis;config/cache.php中,生产环境默认缓存驱动应为 Redis。
-
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:
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
-
准备数据:
- 预先创建足够多的测试玩家及初始余额;
- 获取一批可用的 JWT token。
-
使用
ab压测示例:
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
- 使用
wrk压测示例:
wrk -t4 -c32 -d60s -s play_start.lua http://127.0.0.1:6688/api/game/playStart
- 重点关注指标:
- QPS(Requests per second);
- P95 / P99 延迟;
- 错误率(HTTP 5xx、超时、业务失败);
- MySQL / Redis 的 CPU 使用率与连接数。
7.2 常见问题排查思路
-
QPS 上不去,CPU 却不高
- 检查 DB / Redis 连接池是否经常“打满”;
- 是否存在慢 SQL(全表扫描、缺索引);
- 是否有外部依赖(如第三方接口)拖慢整体。
-
P99 延迟偶尔飙高
- 查看是否有大事务 / 大批量更新与压测同时进行;
- 观察磁盘 I/O 和 GC 情况;
- 检查 Redis 是否有大 Key 或阻塞命令(如
KEYS)。
8. 配置与代码自查清单
建议在压测前和上线前都按此清单进行一次自查。
8.1 配置层面
.env:CACHE_MODE=redis已开启;DB_POOL_MAX/DB_POOL_MIN已根据 CPU 与 MySQLmax_connections调整;REDIS_POOL_MAX能够支撑预期并发量。
- MySQL:
max_connections不小于所有应用实例 DB 连接池上限之和;- 已开启慢查询日志,并设置了合理阈值。
8.2 代码层面
playStart在一次请求中只加载一次玩家信息并全程复用;- 奖励/奖池配置统一通过缓存访问,修改后有刷新缓存逻辑(如
refreshCache()); - 历史记录、钱包流水接口避免 N+1 查询,使用批量查询或预加载;
- EV 等高频统计字段不直接在在线请求中频繁
UPDATE,而是通过 Redis 聚合后批量回写。
9. 总结
- 对于当前 dice 项目,
playStart以及与之相关的奖池、钱包、门票逻辑,是后端 QPS 能力的关键瓶颈所在。 - 通过合理配置 DB/Redis 连接池、充分利用缓存、补齐必要索引、减少重复查询和热点 UPDATE,可以大幅提升单机可承载的 QPS,并降低 P99 延迟。
- 真正的容量边界必须通过
ab/wrk/k6等工具在接近真实数据和流量模型的情况下进行压测验证,本文档主要为定位瓶颈和指导优化提供一个工程实践层面的参考。