跳转至

系统设计

概述

系统设计是将业务需求转化为可扩展、高可用技术架构的过程。本文介绍系统设计方法论、核心组件和经典案例。


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

评论 #