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

12 KiB

Game Performance & QPS Analysis (Dice Project)

This document describes the current dice game backend architecture, key endpoints, approximate QPS capacity, and concrete optimization suggestions.
It is written in plain ASCII to avoid any encoding or garbled-text issues.


1. Runtime Environment Overview

Item Description
Framework Webman (on top of Workerman, long-running PHP workers)
Business module app/dice (players, play records, rewards, lottery pool, dashboard, etc.)
HTTP endpoint Typically http://0.0.0.0:6688
Worker count Recommended: cpu_count() * 4 (for example, 8 cores -> 32 workers)
Database MySQL + ThinkORM, with connection pool (for example, max=20 / min=2)
Cache Redis + think-cache, with connection pool (for example, max=20 / min=2)

Recommended production configuration:

  • Use Redis as default cache: in .env, set CACHE_MODE=redis.
  • Align DB_POOL_MAX / REDIS_POOL_MAX with worker count and MySQL max_connections.

2. Key Endpoints and Load Characteristics

From the codebase, main game-related endpoints can be roughly grouped into:

  • High-load: multiple DB writes plus Redis and non-trivial business logic.
  • Medium-load: some DB writes or heavier DB reads.
  • Light: mostly cache reads or simple DB queries.

2.1 Main endpoints (current project)

Below is a simplified view; exact routes live in the dice module and API controllers:

Endpoint Purpose Load level Notes / risks
POST /api/game/playStart Start a play / roll High Multiple DB + Redis ops, main QPS bottleneck
POST /api/game/buyLotteryTickets Buy tickets / chances Med-High Multiple table writes, wallet and ticket records
GET /api/game/config Game config for frontend Medium Depends on DiceConfig and cache
GET /api/game/lotteryPool Lottery / reward config Medium Depends on DiceLotteryPoolConfig / DiceRewardConfig
POST /api/v1/getGameUrl Get game URL / entry Medium JWT + Redis auth; light DB usage
POST /api/v1/getPlayerInfo Get player info Medium Needs index on dice_player.username
POST /api/v1/getPlayerGameRecord Player history records Med-High Risk of N+1 if not using batch/with
POST /api/v1/setPlayerWallet Adjust wallet balance Medium Wallet + records, must be concurrency-safe

For QPS capacity analysis, focus on:

  • playStart (core hot path).
  • Ticket, wallet, record-related endpoints.

3. playStart Call Flow and Resource Usage

The exact implementation is in the dice game logic layer and related services. A typical call flow looks like the following.

3.1 DB access (typical)

  1. Load player:

    • DicePlayer::find($playerId)
    • Should be done once per request and reused.
  2. Load reward EV / minimum real EV:

    • DiceRewardConfig::getCachedMinRealEv()
    • First call hits DB and writes Redis; later calls hit Redis only.
  3. Get or create lottery pool:

    • LotteryService::getOrCreate()
  • Reads from Redis; if missing, reads from DB (for example, DiceLotteryPoolConfig::where('name','default/killScore')->find()).
  1. Load concrete pool config for this play:

    • DiceLotteryPoolConfig::find($configId)
  2. Persist this play and its side effects (within one or several transactions):

    • DicePlayRecord::create() (play record)
    • DicePlayer::save() (balance and state)
    • DicePlayerTicketRecord::create() (ticket usage)
    • DiceLotteryPoolConfig::where('id')->update(['ev' => Db::raw(...)] (EV statistics)
    • DicePlayerWalletRecord::create() (wallet history)
  3. Refresh player cache:

    • Reuse or reload player and call UserCache::setUser($player).

Rough DB count (assuming good caching and no redundant find calls):

  • About 3-6 DB queries/updates per playStart.
  • About 3-6 Redis operations per playStart.

If the code calls DicePlayer::find($playerId) multiple times in one request, DB usage and latency will increase accordingly.

3.2 Redis access

Typical Redis usages in the current design:

  • Reward / pool configs: DiceRewardConfig::getCachedInstance(), getCachedByTier(), LotteryService::getOrCreate().
  • Player cache: UserCache::getUser(), UserCache::setUser().
  • Optional counters: EV / statistics counters can be done via Redis INCRBY, etc.

Goal: hot paths should hit Redis most of the time, and DB should primarily be for persistence and cold reads.


4. Main Bottlenecks and Optimizations

4.1 DB connections and slow SQL

  1. Connection pool sizing:

    • With 32 workers and DB_POOL_MAX=20, connection exhaustion and waiting are likely at higher QPS.
    • Recommendation: set DB_POOL_MAX to something like 32-64 (depending on CPU and MySQL max_connections), and check that MySQL allows enough connections.
  2. Avoid redundant queries:

    • In playStart, ensure player is loaded once and reused (do not call DicePlayer::find multiple times per request).
  3. EV update strategy:

    • Repeated UPDATE dice_lottery_pool_config SET ev = ev - ? on a hot row causes lock contention.
    • Better:
      • Accumulate EV deltas in Redis (per pool or per shard).
      • Periodic cron job to aggregate Redis deltas back into MySQL in batches.
  4. Index coverage:

    • Ensure all high-frequency queries (username, player_id, create_time, status fields) use proper indexes (see section 6).

4.2 Cache and serialization overhead

  1. Use Redis cache driver:

    • In .env, set CACHE_MODE=redis.
    • In config/cache.php, the default store should be Redis in production.
  2. Player cache encryption:

    • If UserCache encrypts/decrypts data (for example, AES), CPU cost can be high at large QPS.
    • Possible mitigations:
      • Keep cached value small (only necessary fields).
      • Limit how often setUser is called (only on actual state changes).
      • Avoid double-encryption or repeated heavy serialization in hot paths.

4.3 N+1 queries

Endpoints like "get player game record" or wallet histories can easily trigger N+1 patterns:

  • Use whereIn('player_id', $playerIds) instead of one query per player.
  • Use eager loading (with(['dicePlayer'])) for related player info.
  • Keep page size (limit) moderate (for example, 100 rows or less per page).

4.4 Redis vs DB coordination

  • Ensure worker count, DB pool and Redis pool are balanced:
    • If CPU is not saturated but DB or Redis is fully blocked, it is a typical sign of misconfiguration.
  • Monitor Redis:
    • Use slowlog for heavy commands.
    • Watch for large keys or hot keys; consider hash or sharding if needed.

5. Approximate QPS Capacity (Single Node)

These are ballpark figures for a single node with something like 8 cores and 32 workers, with MySQL and Redis in the same LAN and the optimizations above applied. Real numbers must come from your own load tests.

5.1 Per-endpoint estimates

  • playStart:

    • Typical latency: about 50-150 ms.
    • Expected QPS: about 200-300 per node.
  • buyLotteryTickets:

    • Typical latency: about 20-60 ms.
    • Expected QPS: about 500-1000+ per node.
  • getGameUrl / login-style endpoints:

    • Typical latency: about 30-80 ms.
    • Expected QPS: about 400-800 per node.

5.2 Mixed traffic scenario

Example traffic mix:

  • 70% playStart
  • 30% lighter endpoints (getGameUrl, getPlayerInfo, and similar)

Under such a mix, with reasonable indexing and caching:

  • A single 8-core node can typically sustain about 250-400 QPS overall.
  • To go significantly beyond that, you will normally use horizontal scaling (multiple app nodes) plus DB/Redis scaling (read replicas, sharding, and so on).

6. Index and Table Design Recommendations

6.1 Must-have indexes

Table Index Purpose
dice_player unique(username) Fast lookup by username (login, API)
dice_play_record index(player_id, create_time) Player record listing plus time sorting
dice_player_wallet_record index(player_id, create_time) Wallet history listing
dice_player_ticket_record index(player_id, create_time) Ticket history listing

Example 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 Large-table strategy

For very large history tables (play records, wallet logs, ticket logs), plan ahead:

  • Partitioning or sharding (by time or by player ID).
  • Archival strategy (move very old data to cold storage or delete).

7. Load Testing and Metrics

7.1 Load testing playStart

  1. Prepare:

    • Seed enough test players and balances.
    • Obtain valid JWT tokens.
  2. ab example:

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 example:
wrk -t4 -c32 -d60s -s play_start.lua http://127.0.0.1:6688/api/game/playStart
  1. Focus on:
    • QPS (Requests per second)
    • P95 / P99 latency
    • Error rates (HTTP 5xx, timeouts, business failures)
    • MySQL / Redis CPU and connection usage

7.2 Typical troubleshooting

  • Low QPS but low CPU:
    • Likely DB or Redis connection pool exhaustion or slow queries.
  • High P99 spikes:
    • Check for large transactions, schema changes, backup jobs, GC, or disk I/O hiccups.
  • Redis issues:
    • Look for big keys, blocking commands (for example, KEYS), or high single-key contention.

8. Configuration and Code Checklist

Use this list before load tests or production deployments.

8.1 Configuration

  • .env:
    • CACHE_MODE=redis
    • DB_POOL_MAX / DB_POOL_MIN tuned for CPU and MySQL max_connections
    • REDIS_POOL_MAX large enough for concurrent traffic
  • MySQL:
    • max_connections greater than or equal to the sum of all app DB pools
    • Slow query log enabled with a reasonable threshold

8.2 Code

  • playStart loads player once and reuses it everywhere.
  • Reward and pool configuration is always accessed via cache; after changes, cache is refreshed.
  • History and wallet endpoints avoid N+1; use batch queries and eager loading.
  • EV and other hot counters use Redis aggregation instead of per-request UPDATE on hot rows.

9. Summary

  • For the current dice project, playStart and related lottery, wallet, and ticket flows define the backend QPS ceiling.
  • With proper connection pool tuning, Redis caching, indexing, and fewer redundant DB operations, a single 8-core node can typically handle a few hundred QPS of mixed traffic.
  • Real capacity must be validated via ab, wrk, or k6 load tests against your environment; this document serves as a practical guide for identifying and fixing performance bottlenecks.