从单机到集群的演进
每个大型系统都是从单台服务器开始的。理解扩展的过程,才能在面试中给出合理的演进路径。
阶段一:单机部署(初创期,<1000 DAU)
┌─────────────────────────────┐
│ 单台服务器 │
│ ┌────────┐ ┌──────────┐ │
│ │ Web │ │ MySQL │ │
│ │ App │ │ 单实例 │ │
│ └────────┘ └──────────┘ │
└─────────────────────────────┘
优点:简单,开发部署快
问题:单点故障,垂直扩展有上限
阶段二:分离 Web 与 DB(成长期,~10K DAU)
┌──────────────┐ ┌──────────────┐
│ Web Server │ │ DB Server │
│ 4核8GB │───▶│ 16核64GB │
└──────────────┘ └──────────────┘
优点:可以独立扩展 Web 和 DB
问题:Web 还是单点
阶段三:多 Web Server + LB(成熟期,~100K DAU)
┌─────────────────┐
Client ────▶│ Load Balancer │
└────────┬────────┘
┌────┴────┐
┌────▼──┐ ┌───▼───┐
│ Web │ │ Web │
│ S1 │ │ S2 │
└───────┘ └───────┘
└────┬────┘
┌────▼────┐
│ DB │
│Primary │
└─────────┘
问题:Session 在哪里?DB 成为新瓶颈
垂直扩展(Scale Up)
垂直扩展是最直觉的做法:买更好的机器。给服务器增加 CPU、内存、更快的磁盘。
垂直扩展的优点
- 简单,不需要修改应用代码
- 没有分布式一致性问题
- 网络延迟低(同机器内通信)
- 运维成本低,单机管理
垂直扩展的限制
- 硬件有物理上限(256核/数TB内存)
- 成本非线性增长(2倍性能花4倍钱)
- 单点故障风险依然存在
- 升级往往需要停机维护
ℹ 真实案例:Stack Overflow
Stack Overflow 用垂直扩展撑住了惊人的流量——他们的主 SQL Server 是一台 384GB 内存、56 核的超级机器,处理数百万 QPS 的读请求。证明对于某些系统,垂直扩展配合精心的缓存策略,比微服务更经济高效。
水平扩展(Scale Out)
水平扩展是增加更多同等规格的机器,通过负载均衡分发请求。这是互联网大厂应对海量流量的核心策略。
水平扩展架构示意
┌──────────────────────────────┐
│ Load Balancer │
│ (nginx / HAProxy / ALB) │
└──┬─────────┬─────────┬───────┘
│ │ │
┌────▼──┐ ┌───▼───┐ ┌──▼────┐
│ App │ │ App │ │ App │
│ #1 │ │ #2 │ │ #3 │
└───┬───┘ └───┬───┘ └───┬───┘
└──────────┼──────────┘
│
┌────────────▼─────────────┐
│ 共享状态层 │
│ Redis (Session / Cache) │
└────────────┬─────────────┘
│
┌────────────▼─────────────┐
│ 数据库 │
│ Primary ──▶ Replica │
└──────────────────────────┘
关键点:
- App Server 本身必须是「无状态」的
- 状态(Session、缓存)集中到 Redis 等共享存储
- 任何一台 App Server 宕机,LB 自动切走流量
无状态服务设计(Stateless Architecture)
无状态意味着服务器不保存任何请求间的状态信息。每个请求必须携带完成该请求所需的全部信息。
有状态 vs 无状态对比
有状态服务(反模式):
用户张三登录 → 命中 Server #1 → Session 存在 Server #1 内存
下次请求 → 负载均衡 → 必须路由到 Server #1(粘性会话)
Server #1 宕机 → 张三的 Session 丢失 → 被迫重新登录
❌ 问题:
- 负载均衡被迫粘性路由,失去均衡效果
- 单台 Server 宕机影响用户
─────────────────────────────────────────────────
无状态服务(推荐):
用户张三登录 → 任意 Server 处理 → Session 存入 Redis
下次请求 → 携带 Token → 任意 Server 验证 Token → 查 Redis
Server #1 宕机 → 自动切到 Server #2 → 体验无中断
✓ 优点:
- 任意 Server 都能处理任意请求
- 水平扩展无障碍
- 故障恢复透明
Session 管理方案
状态不能消失,只是被移到了不同的地方。水平扩展后,Session 有三种主要存储方案:
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 粘性会话 Sticky Session |
LB 将同一用户永远路由到同一服务器 | 无需外部存储,简单 | 节点故障会话丢失,负载不均 | 遗留系统改造期 |
| 集中式 Session Redis/Memcached |
Session 存入共享缓存,任意服务器可读 | 真正无状态,弹性扩展 | 引入 Redis 依赖,网络开销 | 推荐方案 |
| JWT Token | 状态编码在 Token 中,服务端不存储 | 完全无状态,无外部依赖 | Token 无法立即失效,大小有限制 | 无状态 API、微服务间认证 |
| 会话复制 Session Replication |
各服务器间同步 Session | 无外部依赖 | 广播风暴,内存浪费,延迟高 | 不推荐 |
JWT 无状态认证流程
用户登录流程:
Client App Server Redis / DB
│ │ │
│──POST /login──▶│ │
│ {user, pwd} │──验证用户────────▶│
│ │◀─验证通过─────────│
│◀──200 OK────────│ │
│ {jwt_token} │ │
│ │ │
已登录用户请求流程:
Client App Server Redis / DB
│ │ │
│──GET /profile──▶│ │
│ Authorization: │ │
│ Bearer │ │
│ │──验证签名(本地) │
│ │──无需查 DB! │
│◀──200 OK────────│ │
│ {user_data} │ │
JWT 结构:
Header.Payload.Signature
eyJhbGci... . eyJ1c2VyX... . SflKxwRJ...
数据库扩展瓶颈
扩展了 App Server 之后,数据库很快成为新瓶颈。应对数据库压力有以下递进策略:
策略一:读写分离
读写分离架构:
┌──────────────┐
写请求 ─────────▶│ DB Primary │─────┐
└──────────────┘ │ 主从复制
│ (Replication)
读请求 ─────┬──▶ ┌──────────────┐◀───┘
│ │ DB Replica 1 │
│ └──────────────┘
│ ┌──────────────┐
└──▶ │ DB Replica 2 │
└──────────────┘
适用:读多写少场景(如新闻、博客、商品详情)
读 QPS 翻倍靠增加 Replica
写 QPS 仍受 Primary 限制
策略二:连接池
每次查询建立数据库连接开销巨大(约 10ms)。使用连接池(如 PgBouncer、HikariCP)复用连接,减少握手开销,是不改架构情况下最简单的性能提升手段。
策略三:数据库分片(Sharding)
当单机 DB 写入达到瓶颈,需要水平分片。详见第5章。
| 扩展策略 | 解决什么问题 | 复杂度 | 推荐顺序 |
|---|---|---|---|
| 加索引 + 优化查询 | 慢查询 | 低 | 第1步 |
| 连接池(PgBouncer) | 连接数瓶颈 | 低 | 第2步 |
| 缓存(Redis) | 热数据重复读 | 中 | 第3步 |
| 读写分离 + Replica | 读 QPS 瓶颈 | 中 | 第4步 |
| 垂直分库(按业务) | 单库过大 | 高 | 第5步 |
| 水平分片(Sharding) | 写 QPS 瓶颈 / 数据量过大 | 非常高 | 最后手段 |
▶ 面试要点
被问到「如何扩展这个系统」时,永远从最简单的方案开始,按需逐步演进。说「先加缓存,再读写分离,最后才考虑分片」比直接说「用微服务+分布式数据库」要专业得多。展示你理解每一步的代价和收益。