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, setCACHE_MODE=redis. - Align
DB_POOL_MAX/REDIS_POOL_MAXwith worker count and MySQLmax_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)
-
Load player:
DicePlayer::find($playerId)- Should be done once per request and reused.
-
Load reward EV / minimum real EV:
DiceRewardConfig::getCachedMinRealEv()- First call hits DB and writes Redis; later calls hit Redis only.
-
Get or create lottery pool:
LotteryService::getOrCreate()
- Reads from Redis; if missing, reads from DB (for example,
DiceLotteryPoolConfig::where('name','default/killScore')->find()).
-
Load concrete pool config for this play:
DiceLotteryPoolConfig::find($configId)
-
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)
-
Refresh player cache:
- Reuse or reload player and call
UserCache::setUser($player).
- Reuse or reload player and call
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
-
Connection pool sizing:
- With 32 workers and
DB_POOL_MAX=20, connection exhaustion and waiting are likely at higher QPS. - Recommendation: set
DB_POOL_MAXto something like 32-64 (depending on CPU and MySQLmax_connections), and check that MySQL allows enough connections.
- With 32 workers and
-
Avoid redundant queries:
- In
playStart, ensure player is loaded once and reused (do not callDicePlayer::findmultiple times per request).
- In
-
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.
- Repeated
-
Index coverage:
- Ensure all high-frequency queries (
username,player_id,create_time, status fields) use proper indexes (see section 6).
- Ensure all high-frequency queries (
4.2 Cache and serialization overhead
-
Use Redis cache driver:
- In
.env, setCACHE_MODE=redis. - In
config/cache.php, the default store should be Redis in production.
- In
-
Player cache encryption:
- If
UserCacheencrypts/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
setUseris called (only on actual state changes). - Avoid double-encryption or repeated heavy serialization in hot paths.
- If
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
-
Prepare:
- Seed enough test players and balances.
- Obtain valid JWT tokens.
-
abexample:
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
wrkexample:
wrk -t4 -c32 -d60s -s play_start.lua http://127.0.0.1:6688/api/game/playStart
- 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.
- Look for big keys, blocking commands (for example,
8. Configuration and Code Checklist
Use this list before load tests or production deployments.
8.1 Configuration
.env:CACHE_MODE=redisDB_POOL_MAX/DB_POOL_MINtuned for CPU and MySQLmax_connectionsREDIS_POOL_MAXlarge enough for concurrent traffic
- MySQL:
max_connectionsgreater than or equal to the sum of all app DB pools- Slow query log enabled with a reasonable threshold
8.2 Code
playStartloads 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
UPDATEon hot rows.
9. Summary
- For the current dice project,
playStartand 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, ork6load tests against your environment; this document serves as a practical guide for identifying and fixing performance bottlenecks.