主题
界面
主题
《Nginx 安装与反向代理》讲了反向代理;本节讲它的两项核心能力:把流量分给谁(负载均衡)、能不能不回源(缓存)。
★ 本节的实测会回答两个问题 ✓✓:
① 后端快慢不一时,五种策略差多少? → 4000 ms vs 1335 ms(差 3 倍)
② 加一层代理缓存能提升多少吞吐? → 490 QPS → 13423 QPS(27 倍)先看本节的实测:后端"两快一慢"时,五种策略的墙钟时间 ✓:
策略 分配结果 估算总耗时
轮询 round-robin A=1000 B=1000 C=1000(慢) 4000 ms ✗✗
加权 weight=4:4:1 A=1332 B=1335 C=333 1335 ms ✓
随机 random A=984 B=1025 C=991 3964 ms ✗✗
最少连接 least-conn A=1333 B=1333 C=334 1336 ms ✓✓
IP 哈希 ip-hash A=1005 B=990 C=1005 4020 ms★★ 3000 个请求,墙钟时间差 3 倍 ✓✓ ——差别只在于"流量怎么分" ✓
| 策略 | 假设 | 优点 | 缺点 |
|---|---|---|---|
| 轮询 ★ | 所有后端一样强 | 最简单 ✓ | 慢节点被压垮 ✗✗ |
| 加权轮询 ★★ | 人工知道快慢 | 有效 ✓ | 要人配、且负载变化时跟不上 ⚠️ |
| 随机 ★ | 同轮询 | 无状态、实现简单 | 同样压垮慢节点 ✗✗ |
| 最少连接 ★★★ | 无 | 自动适应快慢 ✓✓ | 需要维护连接计数 ✓ |
| IP 哈希 ★★ | — | 会话粘性 ✓ | 不是为均摊设计的(分布可能很差)⚠️ |
★ 核心结论 ✓✓:
轮询与随机假设"所有后端一样强" ✓
而真实集群里"部分机器更弱/更忙"是常态 ✗ —— 比如某台的 JVM 正在 Full GC、或它连的数据库更远 ✓
★★ "最少连接"为什么能自动适应 ✓✓:
它的分配依据是「当前负载 = 已分配数 × 单次耗时」——
慢节点的 assigned 增长慢 → 后续更少分给它 → 最终总负载被拉平 ✓★ 实测证明了这一点 ✓:最少连接给出 1333 / 1333 / 334 —— 慢节点 C 只拿到 1/3 的量,因为它单次耗时是 4 倍 ✓✓
★ 而"加权"要得到同样效果,必须人工配 4:4:1 ⚠️ —— 配对了效果一样好(1335 ms),但配错就白搭 ✗
★★ 一条实战经验 ✓✓:
"慢节点"很少是硬件差异,多半是"有状态"造成的 ✓
这时最少连接会主动把流量"躲开"它 ✓✓ —— 这是一种天然的、无需人工干预的自愈能力 ✓
实测(10000 个 key,从 3 个节点扩到 4 个)✓:
取模哈希(朴素方案) 7496 个( 75.0%)
一致性哈希(每节点 1 个虚拟节点) 700 个( 7.0%)
一致性哈希(每节点 150 个虚拟节点) 3130 个( 31.3%)
分布均匀度(10000 个 key 落到 3 个节点):
取模哈希 node-A=3333 node-B=3333 node-C=3334 ← 完美均匀 ✓
一致性哈希(1 个虚拟节点) node-A=8610 node-B=690 node-C=700 ← 极不均 ✗✗
一致性哈希(150 个虚拟节点) node-A=5110 node-B=3370 node-C=1520 ← 明显改善 ✓★★★ 这张表里藏着一个反直觉的结论 ✓✓:
"搬家少"不等于"更好" ✗✗
1 个虚拟节点时只搬了 7%(看起来最漂亮),但代价是分布极不均 —— A 拿了 86% 的 key ✗✗
那个"搬得少"是因为 A 太胖了,新节点只能从 A 身上切一小块 ✓
★ 所以评价一致性哈希要看两个指标 ✓✓:
| 指标 | 含义 | 目标 |
|---|---|---|
| 重分布比例 ★★ | 加/减节点时搬多少 key | 越接近 1/N 越好(加第 4 台应为 25%)✓ |
| 分布均匀度 ★★★ | 各节点分到的 key 是否均衡 | 越接近 1/N 越好 ✓✓ |
★ 虚拟节点就是用来"提升均匀度"的 ✓✓:
只有 1 个虚拟节点时,节点在环上的位置是随机的 → 大概率分得很不均 ✗
150 个虚拟节点 = 每个物理节点在环上占 150 个位置 → 把不均摊平了 ✓✓
而均匀度提升后,重分布比例自然回到理论值附近(31.3% ≈ 25% + 波动) ✓
★ 这也解释了 Redis Cluster 为什么用 16384 个槽 ✓✓:
每个节点分到的槽数是由集群协商的,而槽的粒度足够细,所以分布天然均匀 ✓
本质上槽就是"虚拟节点"的一种实现 ✓
★ 与本模块已有内容的呼应 ✓✓:
| 用在哪 | 为什么需要 |
|---|---|
| 分库分表 ★★★ | 扩一个库时希望只搬 1/N 的数据 ✓ |
| 缓存集群(Redis Cluster)★★ | 同上 ✓ |
| 带会话粘性的负载均衡 | ip-hash 是一致性哈希的简化版 ✓ |
实测 ✓✓:
2000 个请求、20 个不同 URL(回源一次 2 ms)
① 全部回源: 4078 ms(490 QPS)✗
② 开启代理缓存: 149 ms(13423 QPS),命中 1980 次 / 回源 20 次 ✓✓
→ 吞吐提升约 27 倍(命中率 99%)★ 收益幅度由两件事决定 ✓✓:
| 因素 | 说明 |
|---|---|
| 命中率 ★★★ | URL 越集中(如商品详情页)收益越大;URL 高度分散时几乎没收益 ✗ |
| 回源成本 ★★ | 后端本来就是内存操作时,收益有限 ✗ |
★★ 代理缓存最危险的配置(真实出过越权事故) ✓✓:
Nginx 默认的 cache key 是
$scheme$host$uri,不含 query string 的语义差异 ✗✗如果后端根据 query 返回不同内容(如
?userId=1),而 cache key 没带 query ——会把 A 用户的数据返回给 B 用户 ✗✗
★ 四条必须检查的配置 ✓✓:
proxy_cache_path /data/cache levels=1:2 keys_zone=mycache:100m inactive=60m;
proxy_cache mycache;
proxy_cache_valid 200 10m; # 只缓存 200,时长按业务定
proxy_cache_key "$scheme$host$uri$is_args$args"; # ★ 必须带 query!
proxy_cache_bypass $cookie_session $arg_nocache; # 登录态/显式禁止的请求不走缓存★★ 一条硬原则 ✓✓:
带用户态的内容一律不要用共享缓存 ✓
"缓存越靠前,越要小心缓存了不该共享的东西" ✓✓
(这与 《缓存一致性与更新策略》 的一致性问题同源:缓存的位置越靠前,影响面越大)
upstream backend {
# 加权轮询(默认就是轮询)
server 10.0.0.1:8080 weight=4; # 权重
server 10.0.0.2:8080 weight=4 max_fails=3 fail_timeout=30s; # 失败 3 次摘除 30 秒
server 10.0.0.3:8080 backup; # 备用节点(主节点全挂才用)
keepalive 32; # ★ 与后端保持长连接
least_conn; # 最少连接(也可以写 ip_hash / random two)
}★ 三个实战要点 ✓✓:
| # | 要点 | 说明 |
|---|---|---|
| ① | max_fails 是"被动健康检查" ★★ | 靠真实请求失败发现故障 → 发现前那几个请求会真的失败 ✗ |
| ② | backup + down 是灰度与维护的标准手法 ★★★ | 新节点低权重试流量 → 逐步加 → 出问题立刻 down ✓✓ |
| ③ | ip_hash 与"无状态服务"是矛盾的 ★★★ | 更好的做法是把会话外置(Redis),让服务真正无状态 ✓✓ |
upstream backend {
server 10.0.0.1:8080;
server 10.0.0.2:8080;
}upstream backend {
server 10.0.0.1:8080 weight=9; # 老版本 90%
server 10.0.0.2:8080 weight=1; # 新版本 10%
}
# 观察没问题 → 逐步调成 5:5 → 0:10 → 下线老节点 ✓upstream backend {
ip_hash; # ✗ 它的目标是"同一 IP 固定后端",不是"平均分配"
}★ 拿它当均摊方案,遇到"少数公司出口 IP 贡献大部分流量"时会严重倾斜 ✗✗
代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/proxy/LoadBalanceLesson.java
运行:
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.proxy.LoadBalanceLesson实测输出(关键部分):
===== 1. 五种负载均衡策略 =====
轮询 round-robin A=1000 B=1000 C=1000 4000 ms 慢节点会被压垮 ✗
加权 weight=4:4:1 A=1332 B=1335 C=333 1335 ms 需人工配权重 ⚠️
随机 random A=984 B=1025 C=991 3964 ms 慢节点会被压垮 ✗
最少连接 least-conn A=1333 B=1333 C=334 1336 ms 自动适应快慢 ✓✓
===== 2. 一致性哈希 =====
取模哈希 7496 个(75.0%)
一致性哈希(1 个虚拟节点) 700 个( 7.0%)
一致性哈希(150 个虚拟节点) 3130 个(31.3%)
取模哈希 node-A=3333 node-B=3333 node-C=3334
一致性哈希(1 个虚拟节点) node-A=8610 node-B=690 node-C=700 ← 极不均 ✗✗
一致性哈希(150 个虚拟节点)node-A=5110 node-B=3370 node-C=1520
===== 3. 代理缓存 =====
① 全部回源: 4078 ms(490 QPS)✗
② 开启代理缓存: 149 ms(13423 QPS),命中 1980 次 / 回源 20 次 ✓✓★ 三条可落地的结论 ✓:
| # | 结论 | 依据 |
|---|---|---|
| ① | 后端快慢不一时,用最少连接(或配对好的加权) | 4000 ms → 1335 ms ✓ |
| ② | 一致性哈希要配足够的虚拟节点,否则分布极不均 | A 拿 86% ✗✗ |
| ③ | 代理缓存收益巨大,但 cache key 必须带 query | 27 倍提升 ✓ |
max_fails 设为 0:观察故障节点一直被重试 ✗;backup:验证主节点全挂时流量切过去 ✓。| # | 错误 | 说明 |
|---|---|---|
| 1 | 以为轮询总是好的 ✗✗ | 慢节点会被压垮 ✓ |
| 2 | 用 ip_hash 做均摊 ✗✗ | 它是为粘性设计的 ✓ |
| 3 | 一致性哈希不配虚拟节点 ✗✗ | 分布极不均 ✓✓ |
| 4 | 用"搬家少"评价一致性哈希 ✗ | 还要看分布均匀度 ✓ |
| 5 | 代理缓存 key 不带 query ✗✗ | 可能返回别人的数据 ✓✓ |
| 6 | 给带登录态的响应配共享缓存 ✗✗ | 越权 ✓ |
| 7 | 以为 max_fails 能提前发现故障 ✗ | 它是被动的,先失败几个请求 ✓ |
目标:亲手验证"策略选择"与"缓存收益"。
步骤:
LoadBalanceLesson,记录五种策略的墙钟时间 ✓;验收方式:能说出"你的系统该用哪种策略、为什么"。
扩展任务:设计一个"两级缓存"方案:
upstream backend { ip_hash; }
proxy_cache_key "$host$uri";这两行分别有什么问题? 5. (设计)给"秒杀"设计网关层方案:要挡住超量请求,又要让真实用户能抢到。
ip_hash 只适合会话粘性,不适合均摊(NAT 后大量用户共用 IP 会倾斜)✗; cache key 不含 query → 不同参数会命中同一份缓存,可能越权 ✗✗backup / down 怎么用于灰度与维护 ★★。《Netty 与网络编程概览》:BIO 与 NIO 的真实差距,以及 Netty 替你封装了什么。
暂无