06 - 读写分离与负载均衡
单机榨干之后,就该走第二个方向:横向扩展(加机器)。这一章讲两个最基础的水平扩展手段——应用层的负载均衡,和数据库层的读写分离。
一句话先记住:一台不够就加几台,然后想办法把请求「均匀地分给它们」。
6.1 负载均衡:把请求分给多台应用
单台 FastAPI 扛不住了,就跑多个实例,前面放一个「分发器」(Nginx / 网关):
用户请求
│
▼
┌──────────────┐
│ 负载均衡器 │ Nginx / LVS / 云 LB
│ (分发器) │
└───┬───┬───┬──┘
│ │ │
┌────┘ │ └────┐
▼ ▼ ▼
┌───────┐┌───────┐┌───────┐
│FastAPI││FastAPI││FastAPI│ 多个实例
│ 实例1 ││ 实例2 ││ 实例3 │
└───────┘└───────┘└───────┘
常见分发策略
| 策略 | 怎么分 | 适合 |
|---|---|---|
| 轮询(Round Robin) | 一台一个轮流发 | 各实例性能一样时 |
| 加权轮询 | 性能强的多分点 | 实例配置不一样 |
| 最少连接 | 谁当前连接最少发给谁 | 请求耗时差异大 |
| IP 哈希 / 一致性哈希 | 同一用户固定打到同一台 | 需要会话粘性时 |
关键前提:应用要「无状态」
要能随便分发,应用实例就不能把用户数据(如登录状态)存在自己进程里:
❌ 有状态:登录信息存在实例1的内存
→ 下次请求被分到实例2 → 找不到登录信息 → 掉登录
✅ 无状态:登录信息存在共享的 Redis
→ 无论分到哪台,都能从 Redis 拿到 → 随便扩展
水平扩展的黄金前提:应用无状态,共享状态(Session、缓存)统一放到 Redis。
6.2 读写分离:给数据库减负
数据库往往是最难扩展、最容易成为瓶颈的一层。而大多数系统读远多于写(比如浏览商品 vs 下单,读可能是写的几十倍)。
思路:写主库,读从库。
应用
│
┌────────┴────────┐
写 │ │ 读
▼ ▼
┌─────────┐ ┌──────────┐
│ 主库 │─复制─▶│ 从库1 │
│ (Master) │─复制─▶│ 从库2 │ 读请求分摊到多个从库
└─────────┘ └──────────┘
- 写操作(INSERT/UPDATE/DELETE)→ 只发给主库
- 读操作(SELECT)→ 分发给多个从库
- 主库通过复制(Replication) 把数据同步给从库
PostgreSQL 原生支持流复制(Streaming Replication),可以搭建一主多从。
6.3 读写分离的最大坑:主从延迟
主库的数据同步到从库,需要一点时间(通常毫秒级,但高负载时可能更久)。这带来一个经典问题:
用户操作:
1. 提交一条评论(写 → 主库)
2. 立刻刷新看自己的评论(读 → 从库)
3. 但数据还没同步到从库!
4. 用户:"我的评论怎么没了??"
对策:
- 写后读主库:刚写完的数据,一小段时间内强制读主库
- 关键读走主库:对实时性要求高的读,直接读主库
- 容忍延迟:对实时性不敏感的读(如浏览历史文章),读从库无所谓
重要认知:读写分离不是免费的,它用「可能读到稍旧的数据」换「读能力的扩展」。要区分哪些读能容忍延迟。
6.4 什么时候上读写分离
不要一上来就搞读写分离,它增加了架构复杂度。判断信号:
✅ 该考虑读写分离:
· 读请求量远大于写(读写比 5:1 以上)
· 数据库 CPU / IO 主要被读查询占用
· 已经加过缓存,但读压力依然大
❌ 先别急着上:
· 加个缓存就能解决的(大部分情况)
· 写才是瓶颈(读写分离救不了写,那要看分库分表)
本章小结
| 你要记住 | 要点 |
|---|---|
| 负载均衡 | 多台应用实例,分发器按策略分流 |
| 无状态前提 | 共享状态放 Redis,应用才能随意扩展 |
| 读写分离 | 写主库、读从库,用复制同步数据 |
| 主从延迟 | 最大的坑,关键读走主库 |
| 何时用 | 读远多于写、缓存也扛不住时 |
下一章预告:扩展了机器还不够——面对恶意流量和突发洪峰,系统需要「自我保护」。限流、降级、熔断三板斧登场。
上一章 ← 05 - 池化技术 | 下一章 → 07 - 限流、降级、熔断