Files
dafuweng-saiadmin6.x/server/docs/PERFORMANCE_AND_QPS_ANALYSIS_CN.md
2026-03-17 15:36:14 +08:00

12 KiB
Raw Blame History

游戏性能与 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('type', 0/1)->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 核心数和目标 QPSDB_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. 索引覆盖

    • 高频查询条件(如 usernameplayer_idcreate_timestatus 等)必须有合适索引(见第 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 延迟和业务分支影响);
    • 单机 QPS200 ~ 300
  • buyLotteryTickets

    • 单次耗时:约 20 ~ 60 ms
    • 单机 QPS500 ~ 1000+
  • getGameUrl / 登录类接口

    • 单次耗时:约 30 ~ 80 ms
    • 单机 QPS400 ~ 800

5.2 混合业务场景

假设:

  • 70% 请求是 playStart
  • 30% 请求为 getGameUrlgetPlayerInfo 等轻 / 中量接口;

在 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

  1. 准备数据:

    • 预先创建足够多的测试玩家及初始余额;
    • 获取一批可用的 JWT token。
  2. 使用 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
  1. 使用 wrk 压测示例:
wrk -t4 -c32 -d60s -s play_start.lua http://127.0.0.1:6688/api/game/playStart
  1. 重点关注指标:
    • 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 等工具在接近真实数据和流量模型的情况下进行压测验证,本文档主要为定位瓶颈和指导优化提供一个工程实践层面的参考。