Table of contents
Open Table of contents
系统设计关注什么
DDD 和 Clean Architecture 关注的是单个服务内部怎么组织代码。系统设计关注的是多个服务组成的整体系统如何应对大规模、高可用、高性能的需求。
Clean Architecture: 一个服务内部的代码分层
System Design: 多个服务如何协作、数据如何流动、故障如何应对
核心问题
系统设计要回答的四个问题:
1. 功能设计 → 系统拆成哪些服务?各自负责什么?怎么通信?
2. 性能估算 → 多少用户?多少 QPS?多少数据量?需要多少机器?
3. 数据设计 → 数据存在哪?怎么分表分库?缓存策略?
4. 容错设计 → 某个服务挂了怎么办?数据中心挂了怎么办?
一、功能设计:服务拆分
单体 vs 微服务
单体 (Monolith):
所有功能在一个进程里
┌─────────────────────────────────────┐
│ 登录 + 背包 + 交易 + 匹配 + 战斗 │
│ 一个二进制,一个数据库 │
└─────────────────────────────────────┘
优点: 简单、无网络开销、事务容易
缺点: 改一个功能要部署整体、不能独立扩缩
微服务 (Microservices):
每个功能独立部署
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ 认证 │ │ 背包 │ │ 交易 │ │ 匹配 │
│ 服务 │ │ 服务 │ │ 服务 │ │ 服务 │
└───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘
│ │ │ │
└──── gRPC / 消息队列 ────────────┘
优点: 独立部署、独立扩缩、技术栈可以不同
缺点: 网络通信复杂、分布式事务难、运维成本高
建议:从单体开始,等业务复杂到单体维护不下去时再拆。过早微服务是最常见的过度设计。
mini_tarkov 的服务划分
阶段 1(单体,够用很久):
┌──────────────────────────────┐
│ mini_tarkov_server │
│ │
│ /api/auth → 登录注册 │
│ /api/players → 玩家管理 │
│ /api/items → 物品管理 │
│ /api/trade → 交易市场 │
│ gRPC → 客户端 API │
│ UDP :7777 → 战斗同步 │
│ │
│ PostgreSQL + Redis │
└──────────────────────────────┘
阶段 2(DAU 增长到需要拆分时):
┌─────────┐ ┌─────────┐ ┌────────────┐
│ API 网关 │──│ 大厅服务 │──│ PostgreSQL │
│ │ │ (gRPC) │ │ (主从复制) │
│ (axum) │ └────┬────┘ └────────────┘
└────┬────┘ │
│ ┌────┴────┐ ┌──────┐
│ │ 匹配服务 │──│ Redis│
│ └────┬────┘ └──────┘
│ │
│ ┌────┴────────┐
│ │ 战斗服务集群 │ 每个房间一个实例
│ │ (UDP, 无状态) │ 可水平扩展
│ └──────────────┘
│
└──── 管理后台 (HTTP REST)
二、性能估算
估算方法
从用户规模开始倒推:
DAU (日活用户): 10,000
同时在线: DAU × 10% = 1,000
同时在战斗中: 在线 × 30% = 300
API QPS:
每个在线用户每分钟 2 次 API 调用(打开背包、查看市场等)
1,000 × 2 / 60 ≈ 33 QPS
峰值 × 3 ≈ 100 QPS
→ 单机 axum 轻松处理(万级 QPS)
战斗同步:
300 个玩家 × 每秒 20 个输入包 = 6,000 包/秒 入站
假设平均每局 5 人,60 局同时进行
每局广播: 5 人 × 20 快照/秒 = 100 包/秒 出站
60 局 × 100 = 6,000 包/秒 出站
→ 单机 tokio UDP 轻松处理
数据库:
100 QPS × 平均 2 条 SQL/请求 = 200 SQL/秒
→ 单个 PostgreSQL 实例足够(万级 QPS 能力)
带宽:
每个快照 ~500 字节 × 6,000 包/秒 = 3 MB/s 出站
→ 家用带宽都够
结论: 1 万 DAU 以下,一台 4 核 8G 的机器就够了
什么时候需要扩展
瓶颈 信号 解决方案
──────────────────────────────────────────────────────────────
CPU game tick 耗时超过预算 战斗服务水平扩展(每房间一进程)
数据库 SQL 延迟 > 100ms 读写分离、加 Redis 缓存
内存 在线玩家状态吃满内存 战斗服务分布式部署
带宽 出站带宽打满 增量压缩、兴趣区域过滤
连接数 TCP 连接数达到系统限制 多节点 + 负载均衡
三、数据设计
数据库选型(已有文档 database-fundamentals.md)
PostgreSQL:
玩家账号、物品、交易记录、匹配历史
需要事务、复杂查询、数据不能丢
Redis:
会话 token、在线状态、匹配队列、排行榜
高速访问、可以丢失(重登录就行)
缓存策略
读取路径 (Cache-Aside):
1. 查 Redis → 命中 → 返回
2. 未命中 → 查 PostgreSQL → 写入 Redis(设 TTL)→ 返回
写入路径:
1. 写 PostgreSQL(真相源)
2. 删 Redis 缓存(下次读时重新加载)
不要:
先删缓存再写 DB — 并发时会导致旧数据回填缓存
先写缓存再写 DB — DB 写失败时缓存和 DB 不一致
数据量增长后的应对
阶段 1: 单库
一个 PostgreSQL 实例,所有表
阶段 2: 读写分离
主库写 → WAL 复制 → 从库读
读多写少的查询走从库
阶段 3: 分表
按 player_id 分表(items_0, items_1, ..., items_15)
同一个玩家的数据在同一张表
阶段 4: 分库
不同 player_id 范围在不同 PostgreSQL 实例
跨库事务用 Saga 模式
不要过早优化:阶段 1 能撑很久。
四、容错设计
单点故障
问题: 只有一个 PostgreSQL 实例,挂了全站不可用
解决:
┌──────────┐ WAL 复制 ┌──────────┐
│ 主库 │────────────────►│ 从库 │
│ (读写) │ │ (只读) │
└──────────┘ └──────────┘
↑ ↑
│ 主库挂了 │ 提升为新主库
✕ ─────────── failover ──────►│
│
Patroni / pg_auto_failover
自动检测 + 自动切换
重试与幂等
客户端请求超时了 → 重试 → 服务端可能已经处理了第一次
幂等设计:
客户端每个请求带唯一 request_id
服务端: 先查 request_id 有没有处理过
处理过 → 直接返回之前的结果
没处理过 → 处理 + 保存 request_id + 返回结果
CREATE TABLE idempotency_keys (
request_id UUID PRIMARY KEY,
response JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
限流
防止恶意请求或突发流量打垮服务:
令牌桶 (Token Bucket):
桶里有 N 个令牌,每秒补充 R 个
每个请求消耗一个令牌
令牌用完 → 返回 429 Too Many Requests
Redis 实现:
key: rate_limit:{player_id}
INCR + EXPIRE
每秒最多 10 次请求
tower 中间件:
tower::limit::RateLimitLayer
熔断
下游服务(比如交易服务)出问题了:
正常 → 每个请求都调交易服务 → 都超时 → 线程全卡住 → 自己也挂了
熔断器 (Circuit Breaker):
┌──────┐ 失败率 > 50% ┌──────┐ 超时后尝试一个请求 ┌───────────┐
│ 关闭 │─────────────────►│ 打开 │──────────────────────►│ 半开 │
│(正常) │ │(拒绝) │ │(试探) │
└──────┘◄──────────────────└──────┘◄──────────────────────└───────────┘
成功率恢复 快速失败 成功 → 关闭
不调下游 失败 → 打开
优雅降级
Redis 挂了:
方案 A: 直接报错 → 全站不可用 ✗
方案 B: 跳过缓存,直查数据库 → 慢但可用 ✓(临时降级)
匹配服务过载:
方案 A: 拒绝所有匹配请求 ✗
方案 B: 延长匹配间隔,显示"匹配队列繁忙" ✓
战斗服务满载:
方案 A: 新对局排队等待 ✓
方案 B: 缩小对局人数上限 ✓
五、通信模式
同步通信(请求-响应)
服务 A ──请求──► 服务 B ──响应──► 服务 A
A 阻塞等待 B 的响应
适合: 需要立即知道结果的操作(登录、查询)
风险: B 挂了 A 也卡住(级联故障)
异步通信(消息队列)
服务 A ──消息──► 消息队列 ──消息──► 服务 B
A 发完就走,不等 B 处理
┌──────────────┐
交易服务 ─────►│ 消息队列 │────► 成就系统(检查成就)
"交易完成" │ (Redis/NATS) │────► 排行榜(更新排名)
│ │────► 日志系统(记录交易)
└──────────────┘
适合: 不需要立即结果、多个消费者、削峰
六、三者的关系
┌──────────────────────────────────────────────────────────┐
│ System Design │
│ 系统整体架构:服务划分、通信、扩展、容错 │
│ │
│ ┌────────────────────┐ ┌────────────────────┐ │
│ │ 服务 A │ │ 服务 B │ │
│ │ │ │ │ │
│ │ Clean Architecture│ │ Clean Architecture│ │
│ │ 内部代码分层 │ │ 内部代码分层 │ │
│ │ │ │ │ │
│ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │
│ │ │ DDD │ │ │ │ DDD │ │ │
│ │ │ 领域模型 │ │ │ │ 领域模型 │ │ │
│ │ │ 业务逻辑 │ │ │ │ 业务逻辑 │ │ │
│ │ └──────────────┘ │ │ └──────────────┘ │ │
│ └────────────────────┘ └────────────────────┘ │
│ │ │ │
│ └──── gRPC / 消息队列 ────┘ │
└──────────────────────────────────────────────────────────┘
DDD: 一个服务内,业务逻辑怎么建模
Clean Architecture: 一个服务内,代码怎么分层
System Design: 多个服务间,系统怎么组织
三者不冲突,是不同层面的设计决策。小项目只需要 Clean Architecture 的分层思路就够了;业务复杂了引入 DDD 建模;用户量上来了再考虑 System Design 的分布式方案。