# 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('type', 0/1)->find()`). 4. Load concrete pool config for this play: - `DiceLotteryPoolConfig::find($configId)` 5. 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) 6. 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_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: ```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: ```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` example: ```bash wrk -t4 -c32 -d60s -s play_start.lua http://127.0.0.1:6688/api/game/playStart ``` 4. 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.