系统设计
概述
系统设计是将业务需求转化为可扩展、高可用技术架构的过程。本文介绍系统设计方法论、核心组件和经典案例。
1. 系统设计方法论
1.1 四步法
Step 1: 需求澄清 (5 min)
- 功能需求:核心功能是什么?
- 非功能需求:QPS、延迟、可用性、一致性
- 约束:用户规模、数据量、预算
Step 2: 估算 (5 min)
- QPS、存储、带宽
- 峰值 vs 平均
Step 3: 高层设计 (15 min)
- 核心组件和数据流
- API 设计
- 数据模型
Step 4: 深入设计 (20 min)
- 选择 1-2 个核心组件深入
- 权衡取舍
- 扩展性和容错
1.2 负载估算
常用基准数字:
| 指标 | 数量级 |
|---|---|
| DAU 1M | QPS ≈ 12(均值),峰值 ≈ 24-60 |
| 一条推文 | ~250 bytes(文本) |
| 一张图片 | ~200 KB(压缩后) |
| 一段视频 | ~5 MB/分钟(压缩后) |
| SSD 读 | ~100 μs |
| 内存读 | ~100 ns |
| 网络往返(同区域) | ~0.5 ms |
| 网络往返(跨洲) | ~150 ms |
2. 核心组件
2.1 负载均衡(Load Balancer)
graph LR
C[客户端] --> LB[负载均衡器]
LB --> S1[Server 1]
LB --> S2[Server 2]
LB --> S3[Server 3]
L4 vs L7 负载均衡:
| 层次 | 工作层 | 决策依据 | 性能 | 代表 |
|---|---|---|---|---|
| L4 | 传输层 | IP + 端口 | 高 | LVS, AWS NLB |
| L7 | 应用层 | URL, Header, Cookie | 较高 | Nginx, HAProxy, AWS ALB |
负载均衡策略:
| 策略 | 说明 | 适用 |
|---|---|---|
| 轮询(Round Robin) | 依次分配 | 服务器性能相近 |
| 加权轮询 | 按权重分配 | 服务器性能不同 |
| 最少连接 | 分给连接最少的 | 长连接场景 |
| IP 哈希 | 同 IP 分到同一台 | 会话保持 |
| 一致性哈希 | 哈希环 | 缓存场景 |
2.2 反向代理
客户端 → [反向代理 Nginx] → 后端服务器集群
功能:
- 负载均衡
- SSL 终止
- 静态文件缓存
- 压缩(gzip/brotli)
- 限流
- 安全防护(WAF)
2.3 缓存
缓存层次:
客户端缓存(浏览器)
↓ miss
CDN 缓存(边缘节点)
↓ miss
反向代理缓存(Nginx)
↓ miss
应用层缓存(Redis/Memcached)
↓ miss
数据库
Redis 常用场景:
import redis
r = redis.Redis(host='localhost', port=6379)
# 缓存策略:Cache-Aside
def get_user(user_id):
# 1. 先查缓存
cached = r.get(f"user:{user_id}")
if cached:
return json.loads(cached)
# 2. 缓存未命中,查数据库
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
# 3. 写入缓存(TTL 5 分钟)
r.setex(f"user:{user_id}", 300, json.dumps(user))
return user
缓存问题:
| 问题 | 描述 | 解决方案 |
|---|---|---|
| 缓存穿透 | 查询不存在的 key | 布隆过滤器 / 缓存空值 |
| 缓存击穿 | 热 key 过期瞬间大量请求 | 互斥锁 / 永不过期 |
| 缓存雪崩 | 大量 key 同时过期 | 随机 TTL / 多级缓存 |
CDN(Content Delivery Network):
- 将静态资源缓存到全球边缘节点
- 用户访问最近的节点
- 减少延迟和源站压力
- 代表:Cloudflare、AWS CloudFront、Akamai
2.4 消息队列
异步解耦和削峰填谷:
同步调用(耦合):
用户 → 订单服务 → 库存服务 → 支付服务 → 通知服务
异步消息(解耦):
用户 → 订单服务 → [消息队列]
├→ 库存服务
├→ 支付服务
└→ 通知服务
2.5 数据库分片
垂直分片:按业务拆分表到不同数据库
用户库:users, profiles
订单库:orders, order_items
商品库:products, categories
水平分片:按数据范围拆分同一张表
users 表按 user_id 分片:
Shard 0: user_id 0 - 999,999
Shard 1: user_id 1,000,000 - 1,999,999
Shard 2: user_id 2,000,000 - 2,999,999
分片策略:
| 策略 | 方法 | 优势 | 劣势 |
|---|---|---|---|
| 范围分片 | 按 ID 范围 | 简单,范围查询友好 | 热点问题 |
| 哈希分片 | hash(key) % N | 分布均匀 | 范围查询困难 |
| 一致性哈希 | 哈希环 | 扩缩容影响小 | 实现复杂 |
2.6 数据库复制
写请求 → [主库 Master]
│ 同步/异步复制
┌────┼────┐
▼ ▼ ▼
[从库1] [从库2] [从库3] ← 读请求
3. 一致性哈希
解决分布式系统中节点增减导致的大规模数据迁移问题。
哈希环 (0 ~ 2^32-1):
Node A
/
----●----------
/ key1 \
/ \
| key2 Node B|
\ ● ● /
\ /
----●----------
Node C
key3
规则:key 顺时针找到的第一个 Node 就是其所在节点
添加 Node D:只需迁移 Node D 与其逆时针前一个节点之间的 key
删除 Node:只需将该节点的 key 迁移到顺时针下一个节点
虚拟节点:每个物理节点映射多个虚拟节点到哈希环上,使数据分布更均匀。
4. 限流(Rate Limiting)
4.1 算法
| 算法 | 原理 | 特点 |
|---|---|---|
| 令牌桶 | 固定速率产生令牌,请求消耗令牌 | 允许突发,平均限速 |
| 漏桶 | 请求进入桶,固定速率流出 | 严格限速,平滑输出 |
| 固定窗口 | 固定时间窗口内计数 | 简单,边界突发问题 |
| 滑动窗口 | 滑动时间窗口计数 | 精确,内存开销较大 |
4.2 实现示例
# Redis 滑动窗口限流
import time
def is_rate_limited(redis_client, user_id, limit=100, window=60):
key = f"rate_limit:{user_id}"
now = time.time()
pipe = redis_client.pipeline()
pipe.zremrangebyscore(key, 0, now - window) # 清除过期记录
pipe.zadd(key, {str(now): now}) # 添加当前请求
pipe.zcard(key) # 计数
pipe.expire(key, window) # 设置过期
results = pipe.execute()
count = results[2]
return count > limit
5. 案例分析
5.1 URL 短链服务
需求:将长 URL 映射为短 URL,支持重定向。
写入:POST /api/shorten {url: "https://very-long-url.com/path"}
→ {short_url: "https://short.ly/abc123"}
读取:GET /abc123
→ 301 Redirect to https://very-long-url.com/path
估算:
- 写 QPS: 100/s
- 读 QPS: 10,000/s(读写比 100:1)
- 存储: 100 * 86400 * 365 * 5年 * 500B ≈ 800GB
graph LR
C[客户端] --> LB[负载均衡]
LB --> API[API 服务]
API --> Cache[(Redis 缓存)]
API --> DB[(数据库)]
subgraph 短码生成
API --> ID[ID 生成器]
ID --> B62[Base62 编码]
end
核心设计:
- 短码生成:分布式 ID 生成器(Snowflake)→ Base62 编码
- 读优化:Redis 缓存热门 URL(LRU)
- 301 vs 302:301 永久重定向(浏览器缓存)vs 302 临时重定向(可追踪点击)
5.2 聊天系统
需求:支持一对一和群聊,消息送达通知。
graph TB
C1[客户端 A] -->|WebSocket| GW[WebSocket Gateway]
C2[客户端 B] -->|WebSocket| GW
GW --> MS[消息服务]
MS --> MQ[消息队列 Kafka]
MQ --> PS[推送服务]
MQ --> Store[存储服务]
PS --> GW
Store --> DB[(消息数据库)]
MS --> Session[(会话管理 Redis)]
核心设计:
- 长连接:WebSocket 维持实时通信
- 消息存储:写扩散(群聊写入每个成员收件箱)vs 读扩散
- 离线消息:用户上线后拉取未读消息
- 消息顺序:每个会话内单调递增的 sequence_id
5.3 新闻 Feed
需求:用户发布内容,关注者的 Feed 中显示。
推模型 vs 拉模型:
| 模型 | 写入时 | 读取时 | 适用 |
|---|---|---|---|
| 推模型 (Fan-out) | 写入所有粉丝的 Feed | 直接读取 | 粉丝少 |
| 拉模型 (Fan-in) | 只写入自己的 Timeline | 合并所有关注者的帖子 | 粉丝多(大 V) |
| 混合模型 | 普通用户推,大 V 拉 | 合并推送 + 实时拉取 | 实际生产 |
6. 系统架构总览
graph TB
Client[客户端] --> CDN[CDN]
Client --> DNS[DNS]
CDN --> LB[负载均衡 L7]
LB --> API1[API Server 1]
LB --> API2[API Server 2]
LB --> API3[API Server 3]
API1 --> Cache[(Redis Cluster)]
API1 --> MQ[Kafka]
API1 --> DB_Master[(DB Master)]
DB_Master --> DB_Slave1[(DB Slave 1)]
DB_Master --> DB_Slave2[(DB Slave 2)]
MQ --> Worker1[Worker 1]
MQ --> Worker2[Worker 2]
Worker1 --> DB_Master
Worker1 --> Search[(Elasticsearch)]
Worker1 --> Object[(Object Storage S3)]
7. 设计原则总结
| 原则 | 说明 |
|---|---|
| 无状态服务 | 状态外置到缓存/数据库,便于水平扩展 |
| 读写分离 | 主库写,从库读 |
| 缓存优先 | 热数据放缓存,减少 DB 压力 |
| 异步处理 | 非关键路径用消息队列异步化 |
| 水平扩展 | 加机器而非升级机器 |
| 优雅降级 | 非核心功能不可用时,核心功能继续工作 |
| 防御性设计 | 超时、重试、熔断、限流 |
| 可观测性 | Logging + Metrics + Tracing |
与其他主题的关系
- 参见 分布式系统,理解一致性、消息与容错如何影响高层架构
- 参见 数据库系统,理解缓存、索引、读写分离与数据模型选择
- 参见 云服务,理解负载均衡、对象存储与弹性资源如何承载系统设计
- 参见 全栈开发,理解产品需求如何最终落到 API、前端交互与数据流
参考文献
- "Designing Data-Intensive Applications" - Martin Kleppmann
- "System Design Interview" - Alex Xu
- "Building Microservices" - Sam Newman
- The System Design Primer: https://github.com/donnemartin/system-design-primer