文档
This commit is contained in:
@@ -1,192 +1,286 @@
|
||||
# 歷멩????塋??㎬?訝?QPS ????ε?
|
||||
# Game Performance & QPS Analysis (Dice Project)
|
||||
|
||||
## 訝???????訝?????鰲?
|
||||
| 窈밭? | 瑥닸? |
|
||||
|------|------|
|
||||
| 瓦????? | Webman (?뷰? Workerman)竊?만要삣?耶?|
|
||||
| HTTP ??? | `http://0.0.0.0:6688` |
|
||||
| Worker ?곈? | `cpu_count() * 4`竊?? 8 ??= 32 訝??葉?? |
|
||||
| ?경?佯?| MySQL竊?hinkORM竊???ζ? max=20/min=2 |
|
||||
| 煐?? | Redis竊?hink-cache 容?? `redis`竊??瓦??黎?max=20/min=2 |
|
||||
| 煐??要긷?力ⓩ? | `config/cache.php` 容?? `file`竊??訝??鵝욜?瑥ι?營??煐??壅경?餓띰?`config/think-cache.php` 容?? `redis`竊?PI ?ⓩ?/也????쉰嶺?슴??Redis |
|
||||
> 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.
|
||||
|
||||
---
|
||||
|
||||
## 雅????恙???d????黎????
|
||||
### 2.1 遙?뭉????녔???
|
||||
| ?ε? | ?③??| 訝삭???? | 窯?섟??????竊??謠??쇽? |
|
||||
|------|------|----------|------------------------|
|
||||
| `POST /api/game/playStart` | 凉?冶??掠?歷멩?竊??也?? | 鸚?? DB + Redis + 雅?? | 遙??鰲??竊?|
|
||||
| `POST /api/game/buyLotteryTickets` | 兀?물?썲???| 雅?????烏ⓨ???+ Redis ?닸? | 訝?|
|
||||
| `GET /api/game/config` | 歷멩???쉰 | ?②〃 DiceConfig ?θ?竊??煐?? | 鵝?訝?|
|
||||
| `GET /api/game/lotteryPool` | 也????쉰 | DiceRewardConfig 煐?? | 鵝?|
|
||||
| `POST /api/v1/getGameUrl` | ?룟?歷멩??겼?竊?뭄?곤? | ?삣??삭? + JWT + Redis | 訝?|
|
||||
| `POST /api/v1/getPlayerInfo` | ?⒴?岳→? | ??username ??DicePlayer | 鵝??兩븃? username ???訝?榮℡?竊?|
|
||||
| `POST /api/v1/getPlayerGameRecord` | 歷멩?溫겼???〃 | ??〉 + ??? N+1 ?η?若?| 訝?|
|
||||
| `POST /api/v1/setPlayerWallet` | ?긷?饔??饔?? | 雅??竊???곁?若?+ ???麗?| 訝?|
|
||||
## 1. Runtime Environment Overview
|
||||
|
||||
### 2.2 `playStart` ???瑥룡??????㎗竊??㎬??녜?瓮??竊?
|
||||
???歷멩?訝?轝↑?黎?ㄷ?답?鰲??竊?
|
||||
**?경?佯??**
|
||||
| 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) |
|
||||
|
||||
1. `DicePlayer::find($playerId)` ???↓??⒴?訝??窯?2. `DiceRewardConfig::getCachedMinRealEv()` ??腰??????ε?亮뜹?煐??
|
||||
3. `LotteryService::getOrCreate()` ?????訝?Redis ?띰?`DicePlayer::find` + `DiceLotteryPoolConfig::where('type',0/1)->find()` 訝ㅶ?
|
||||
4. `DiceLotteryPoolConfig::find($configId)` ?????營?ID ???黎?5. **雅????*竊?
|
||||
- `DicePlayRecord::create`
|
||||
- `DicePlayer::find($playerId)`竊??轝→??⒴?竊??鴉??竊?
|
||||
- `DicePlayer::save`
|
||||
- ??? `DicePlayerTicketRecord::create`竊?5 訝??竊?
|
||||
- `DiceLotteryPoolConfig::where('id')->update(['ev' => Db::raw(...)])` ??烏?벨?닸?
|
||||
- `DicePlayerWalletRecord::create`
|
||||
6. 雅?????`DicePlayer::find($playerId)`??UserCache::setUser`
|
||||
Recommended production configuration:
|
||||
|
||||
**Redis竊?*
|
||||
|
||||
- `DiceRewardConfig::getCachedInstance()`竊?? getCachedByTier 嶺????瑥?????깁?營??耶?- `LotteryService::getOrCreate()` ??瑥????若뜹?黎??耶?- `UserCache::setUser()` ??????룝에???耶?
|
||||
?①?耶??訝??也썹????訝????? `playStart` 餓?벧??**6節? 轝?DB 溫욥?**竊??雅????2 轝?find ?⒴??? 轝?update 壤⑶?黎???節? 轝?create竊???
|
||||
?η?耶???썰릎竊??鴉??????깁?營????黎??營????若띄??θ?轝→???
|
||||
---
|
||||
|
||||
## 訝????㎬??띌?訝???⑴?
|
||||
|
||||
### 3.1 ?경?佯?
|
||||
1. **瓦??黎?? Worker ??*
|
||||
- ??? DB 瓦??黎?max=20竊?orker ??= CPU?4??? 32 訝?Worker ?????빨竊??雅?? 20 訝???ο??븀?嶺??訝???뜰??
|
||||
- 兩븃?竊??????? `DB_POOL_MAX`竊?? 32節?4竊??亮띄???MySQL `max_connections` 訝???θ???
|
||||
2. **`playStart` ???鸚???⒴?**
|
||||
- 雅??鸚?럴 `DicePlayer::find($playerId)`竊???▼???`DicePlayer::find($playerId)`??
|
||||
- ???訝뷴?雅?????????ⓨ럴??`$player` ????ε?誤??餘듸????訝?轝?SELECT??
|
||||
3. **`DiceLotteryPoolConfig::where('id', $configId)->update(['ev' => Db::raw(...)])`**
|
||||
- 驪???썸??겼?訝???쉰烏?? `ev`竊??亮뜹?訝??烏??塋?????竊???썸?訝븀?窯???
|
||||
- ?????竊??閭η눕??????띌?/轝→??백??닸?竊??鵝욜? Redis 溫→?????뜹?閭ε? DB??
|
||||
4. **榮℡?**
|
||||
- `dice_player.username`??dice_play_record.player_id`??dice_play_record.create_time`??
|
||||
`dice_player_wallet_record.player_id`??dice_player_ticket_record.player_id` 嶺????뻠榮℡?竊??烏ⓧ?瀯??鴉??????
|
||||
- 兩븃??녑?竊?username` ???榮℡?竊?player_id` + `create_time` 瀯??榮℡?竊??若???θ??▽뻑孃??竊???
|
||||
### 3.2 煐??
|
||||
|
||||
1. **煐??要긷?訝????*
|
||||
- `config/cache.php` 容?? `file`竊??訝?????瑥ι?營?? Cache竊??壅경?餓띰?QPS 遙?? I/O ???鴉??訝븀?窯???
|
||||
- 兩븃?竊??雅㎫?訝?鵝욜? Redis竊?뭉簾?? `CACHE_MODE=redis` 訝?think-cache ??default 訝??담??
|
||||
2. **也????쉰煐??**
|
||||
- `DiceRewardConfig::getCachedInstance()` 藥꿨???????????+ Redis竊??訝???뜻?㎬?也썬??
|
||||
- ??岳????쉰????띈???`refreshCache()`竊?????????룡???
|
||||
3. **UserCache ??㎗野?*
|
||||
- 驪?? `getUser`/`setUser` ??AES ??㎗野??遙?QPS 訝?CPU 鴉?????鵝???만訝??腰???띌?竊???????? CPU 遙?????????↓???섄??value??
|
||||
### 3.3 ??〃?ε? N+1
|
||||
|
||||
- `getPlayerGameRecord`竊????〉??`DicePlayRecord`竊?? `whereIn('id', $playerIds)` ?η?若뜹뭉瀯????
|
||||
壤??若??藥꿨??백??η?若띰?訝???멨? N+1竊???ε?窈?limit 孃?ㄷ餓????????恙??耶???????limit 訝??竊?럴??100 訝??竊???
|
||||
- `getPlayerWalletRecord` / `getPlayerTicketRecord` 鵝욜? `with(['dicePlayer'])` 窯??饔쏙??????
|
||||
### 3.4 瓦??訝????
|
||||
- Worker ??= CPU?4 ?띰??ζ?訝??黎????1 訝?DB 瓦??訝??黎???닻?竊?0 訝???ζ?熬??譯▲??
|
||||
- 兩븃?竊?????訝??野?`DB_POOL_WAIT_TIMEOUT` ???瀯?만鰲??竊???녑????罌?ㄷ黎????????雅????
|
||||
---
|
||||
|
||||
## ????PS 窯?섟竊??謠??쇽???????▼?竊?
|
||||
### 4.1 ????▽뻑
|
||||
|
||||
- 8 ??CPU竊?2 訝?Worker??- MySQL/Redis ??????兩띈????竊??髥???띌???- 煐???썰릎???竊???깁?營???otteryService??serCache 鸚?맏?썰릎竊???
|
||||
### 4.2 ????g?嶸?
|
||||
- **playStart**竊??轝←벧 50節?50 ms竊??雅??訝??轝?DB/Redis竊????Worker 瀛?7節?0 QPS竊?2 Worker 瀛?**220節?40 QPS**??
|
||||
若????DB 烏??竊?? `dice_lottery_config.ev` ?닸?竊?????ζ?嶺??壤긷?竊?*岳??鴉계???? 200節?00 QPS**??- **buyLotteryTickets**竊??轝←벧 20節?0 ms竊???븀벧 **500節?000+ QPS**竊?빳瓦??黎?????譯▽맏???竊???- **getGameUrl / 亮녑??ε?**竊??壅??壤?? DB ?θ?竊??轝←벧 30節?0 ms竊???븀벧 **400節?00 QPS**竊?????壅곁?耶????
|
||||
### 4.3 曆룟?役??
|
||||
|
||||
??70% 訝?`playStart`??0% 訝뷴?餓???o??답???? QPS 鴉?? `playStart` ?얏?竊?*瀯쇔????瀛?250節?00 QPS** ???訝뷴?????담??
|
||||
若??????ab/wrk/k6 嶺?? `playStart` ??말誤???e?役??瀯?? MySQL ?€?瑥???edis ?썰릎??????ζ?嶺??轝→????????
|
||||
---
|
||||
|
||||
## 雅??????뻠溫????
|
||||
| 映삣? | 兩븃? |
|
||||
|------|------|
|
||||
| ??쉰 | ??벨溫양쉰 `CACHE_MODE=redis`竊?뭉瀯??鵝욜? Redis 鵝?맏 Cache 要긷? |
|
||||
| 瓦??黎?| ??? `DB_POOL_MAX`竊?? 32節?4竊??岳?? ??Worker ?곤?亮띄???MySQL max_connections |
|
||||
| 餓g? | `playStart` 雅??????ⓨ럴?η? `$player`竊?????轝?`DicePlayer::find($playerId)` |
|
||||
| ?경?佯?| 訝?`username`??player_id`??create_time` 嶺??窯??瑥℡?餘드?榮℡??????뇨凉?|
|
||||
| ????닸? | `dice_lottery_config.ev` ???掠??닸??밥맏 Redis 榮?? + 若??/?백???? DB竊??鵝?????雅?|
|
||||
| ??? | 野?`playStart`??buyLotteryTickets`??setPlayerWallet` ?????訝??瑥?????竊?뭉??MySQL ?€?瑥??瓦??黎??孃????|
|
||||
| ??? | 鵝욜? ab/wrk/k6 野?`/api/game/playStart` 嶺???뜻????竊???겼???P99 兩띈?訝??鸚?QPS |
|
||||
- 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`.
|
||||
|
||||
---
|
||||
|
||||
## ?????鵝???곁?若?QPS 訝?P99
|
||||
## 2. Key Endpoints and Load Characteristics
|
||||
|
||||
1. **??? playStart**
|
||||
- 鵝욜???? JWT竊??????삣??욕? token竊??野?`POST /api/game/playStart` ??body `direction=0` ??`1`??
|
||||
- 鹽뷰?竊???욘? URL ??token竊??
|
||||
```bash
|
||||
# 鵝욜? ab
|
||||
ab -n 1000 -c 32 -p post_body.json -T application/json -H "token: YOUR_JWT" http://127.0.0.1:6688/api/game/playStart
|
||||
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.
|
||||
|
||||
# 鵝욜? wrk竊?? lua ???鴉?token ??body竊? wrk -t4 -c32 -d30s -s play_start.lua http://127.0.0.1:6688/api/game/playStart
|
||||
```
|
||||
- 餓????릎孃?? Requests per second竊?PS竊?? Time per request竊???®? P99 ?ε램?룡??????
|
||||
2. **??????**
|
||||
- 佯??竊??訝???g?瑥룡?????뭄?? P99 ?????xx 訝???뜻??겹??
|
||||
- MySQL竊???ζ?????θ???nnoDB 烏??嶺????
|
||||
- Redis竊???ζ????訝?????餓ㅸ?????
|
||||
3. **?띌??ㅶ?**
|
||||
- ??QPS 訝????CPU ???譯???鴉?????餓g?????뷴???
|
||||
- ??DB 瓦??嶺??????θ?罌?? ????뇨凉?????????瓦??黎???????〃??
|
||||
- ??Redis 兩띈?訝?? ???εㄷ key????썰빱???瀯??瓦??黎???
|
||||
---
|
||||
|
||||
## 訝????鵝??遙??㎬?竊????????
|
||||
## 3. `playStart` Call Flow and Resource Usage
|
||||
|
||||
??*???雅㎩?驪?*餓???겻????竊????????窈밧?????얏???QPS 訝?㉢若??㎯??
|
||||
### 7.1 ??쉰掠???백?營???????빰??????
|
||||
The exact implementation is in the dice game logic layer and related services. A typical call flow looks like the following.
|
||||
|
||||
| 鴉??瀛?| ??? | 瑥닸? |
|
||||
|--------|------|------|
|
||||
| 遙?| ??벨???溫양쉰 `CACHE_MODE=redis` | ?욕?壅경?餓띄?耶????? I/O 訝??塋??竊?? think-cache 岳??訝??담??|
|
||||
| 遙?| ??? DB 瓦??黎??`DB_POOL_MAX=32` ??`64` | 岳?? ??Worker ?곤?倻?32竊???욕?遙?뭉???瓦??嶺??訝???뜰??|
|
||||
| 訝?| ??? Redis 瓦??黎??`REDIS_POOL_MAX=32` | 訝?Worker ?겼??????? Redis ?룟?瓦??嶺????|
|
||||
| 訝?| 簾?? MySQL `max_connections` ??佯??瓦??黎??삣? | 鸚??堊??營꿩?竊??堊?? ? DB_POOL_MAX ?욤?瓦?MySQL 訝????|
|
||||
### 3.1 DB access (typical)
|
||||
|
||||
**鹽뷰? .env ???竊?*
|
||||
1. Load player:
|
||||
- `DicePlayer::find($playerId)`
|
||||
- Should be done once per request and reused.
|
||||
|
||||
```env
|
||||
CACHE_MODE=redis
|
||||
DB_POOL_MAX=32
|
||||
DB_POOL_MIN=4
|
||||
REDIS_POOL_MAX=32
|
||||
```
|
||||
2. Load reward EV / minimum real EV:
|
||||
- `DiceRewardConfig::getCachedMinRealEv()`
|
||||
- First call hits DB and writes Redis; later calls hit Redis only.
|
||||
|
||||
### 7.2 ?경?佯??竊??榮℡???????
|
||||
3. Get or create lottery pool:
|
||||
- `LotteryService::getOrCreate()`
|
||||
- Reads from Redis; if missing, reads from DB (for example, `DiceLotteryPoolConfig::where('type', 0/1)->find()`).
|
||||
|
||||
| 鴉??瀛?| ??? | 瑥닸? |
|
||||
|--------|------|------|
|
||||
| 遙?| 訝?`dice_player.username` 兩뷴?訝?榮℡? | ?삣???etPlayerInfo??oken 訝??餓뜹???username ?ο???뇨凉???②〃?????|
|
||||
| 遙?| 訝?`dice_play_record(player_id, create_time)` 兩븀???뇨凉?| 歷멩?溫겼???〃???溫→??⒴?+?띌??θ?竊?????窈듕?瀯????|
|
||||
| 訝?| 訝뷸?麗닺〃 `player_id`??create_time` 兩븀뇨凉?| 倻?`dice_player_wallet_record`??dice_player_ticket_record` ???若뜻??띌?嶺?????ⓨ?訝???|
|
||||
4. Load concrete pool config for this play:
|
||||
- `DiceLotteryPoolConfig::find($configId)`
|
||||
|
||||
**鹽뷰? SQL竊??若??烏ⓨ?瘟??竊??**
|
||||
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);
|
||||
```
|
||||
|
||||
### 7.3 餓g?掠????? DB 瘟??訝???백?竊?
|
||||
| 鴉??瀛?| ??? | 瑥닸? |
|
||||
|--------|------|------|
|
||||
| 遙?| `playStart` 雅??????ⓨ럴?η? `$player`竊????`DicePlayer::find($playerId)` | 弱?1 轝?SELECT竊???▼??ⓨ?訝? `$player` ???耶????? 1 轝?SELECT??|
|
||||
| 訝?| `game/config` ?ε?訝?DiceConfig ???耶?| ??쉰???窯??鵝????Redis 煐?? 1節? ???竊??弱??烏ⓩ?瑥???|
|
||||
| 訝?| `dice_lottery_config.ev` ?밥맏 Redis 榮?? + 若????? DB | 驪??訝?? `UPDATE dice_lottery_config SET ev=ev-?`竊?????烏???백?竊??驪?????驪?N 掠????訝?轝▲??|
|
||||
### 6.2 Large-table strategy
|
||||
|
||||
### 7.4 ???訝????
|
||||
| 鴉??瀛?| ??? | 瑥닸? |
|
||||
|--------|------|------|
|
||||
| 訝?| 野?`playStart`??buyLotteryTickets` ?????訝??瑥????? | 堊요?????<3F>?黎??凉?만役????|
|
||||
| 訝?| 凉???MySQL ?€?瑥€?恙??倻?>200ms竊?| 若??????뇨凉??鴉????SQL??|
|
||||
| 鵝?| ???孃????? QPS 訝?P99 | ??ab/wrk/k6 野?`playStart` ???竊?빳?경?若??????⒴??뜻???|
|
||||
For very large history tables (play records, wallet logs, ticket logs), plan ahead:
|
||||
|
||||
### 7.5 窯?????竊??謠??쇽?
|
||||
- Partitioning or sharding (by time or by player ID).
|
||||
- Archival strategy (move very old data to cold storage or delete).
|
||||
|
||||
- ??쉰掠?+ 榮℡?竊???δ????訝븀?窯????〃訝??壤???f??얍?恙???답??????벧 **20%節?0%** ??? QPS??- ?삥? playStart ??2 轝▼?鵝??佯?????黎?? 2 轝?round-trip竊?*playStart ???黎????瀛?? 5%節?5%**??- ev ?밥맏 Redis 榮??竊??亮뜹?訝?**playStart** 烏??塋??訝??竊????playStart QPS ????????**10%節?0%**竊??亮뜹?佯????竊???
|
||||
---
|
||||
|
||||
**瑥닸?**竊??瓦?QPS 訝??????맏?뷰?餓g?訝??營??鴉곁?竊?????㎬?餓ε?役??瀛요????訝뷴???뻠溫??窯??/??벨??????弱??饔?`playStart` 訝??壤????????녔??g??????
|
||||
## 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.
|
||||
|
||||
293
server/docs/PERFORMANCE_AND_QPS_ANALYSIS_CN.md
Normal file
293
server/docs/PERFORMANCE_AND_QPS_ANALYSIS_CN.md
Normal file
@@ -0,0 +1,293 @@
|
||||
# 游戏性能与 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 或其他统计数据。
|
||||
|
||||
**目标:** 热路径尽量命中 Redis,DB 主要负责落库和冷数据读取。
|
||||
|
||||
---
|
||||
|
||||
## 4. 性能瓶颈与优化方向
|
||||
|
||||
### 4.1 数据库连接与 SQL
|
||||
|
||||
1. **连接池大小**
|
||||
- 在 32 个 Worker 且 `DB_POOL_MAX=20` 的情况下,高并发时容易出现连接耗尽和等待。
|
||||
- 建议:根据 CPU 核心数和目标 QPS,将 `DB_POOL_MAX` 调整到 **32 ~ 64** 左右,并保证 MySQL `max_connections` 足够大。
|
||||
|
||||
2. **避免重复查询**
|
||||
- 在 `playStart` 中,玩家信息应只查询一次:`$player = DicePlayer::find($playerId)`,后续逻辑统一使用 `$player`,避免重复 `find`。
|
||||
|
||||
3. **EV 更新策略**
|
||||
- 频繁在在线请求中执行 `UPDATE dice_lottery_config SET ev = ev - ?`,会造成该行热点锁竞争;
|
||||
- 建议:
|
||||
- 在线请求仅将 EV 变动累加到 Redis 计数器;
|
||||
- 通过定时任务批量同步 Redis 中的统计数据回 MySQL。
|
||||
|
||||
4. **索引覆盖**
|
||||
- 高频查询条件(如 `username`、`player_id`、`create_time`、`status` 等)必须有合适索引(见第 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 延迟和业务分支影响);
|
||||
- 单机 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:
|
||||
|
||||
```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` 压测示例:
|
||||
|
||||
```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` 压测示例:
|
||||
|
||||
```bash
|
||||
wrk -t4 -c32 -d60s -s play_start.lua http://127.0.0.1:6688/api/game/playStart
|
||||
```
|
||||
|
||||
4. 重点关注指标:
|
||||
- QPS(Requests 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` 等工具在接近真实数据和流量模型的情况下进行压测验证,本文档主要为定位瓶颈和指导优化提供一个工程实践层面的参考。
|
||||
|
||||
Reference in New Issue
Block a user