4. 负载均衡策略
负载均衡这东西,说白了就是「把流量分给多台机器干活」。我见过不少团队,上来就怼一套复杂的负载均衡方案,结果业务量还没起来,运维成本先把自己压垮了。其实选哪种策略,得看你的场景和预算。
今天咱们把四种主流策略掰开揉碎聊一聊。从最轻量的 DNS 轮询,到企业级的 LVS,再到分布式缓存里必用的一致性哈希。每种我都踩过坑,咱们边聊边避雷。
4.1 DNS 轮询:最轻量的负载均衡
DNS 轮询是最简单粗暴的方式。你配置一个域名,对应多个 A 记录,DNS 服务器每次解析时按顺序返回不同的 IP。
# 示例:DNS 轮询配置
# 域名 api.example.com 对应 3 台服务器
api.example.com. IN A 192.168.1.1
api.example.com. IN A 192.168.1.2
api.example.com. IN A 192.168.1.3
客户端拿到 IP 后,直接连过去。DNS 服务器不关心后端是否活着,也不管哪台机器负载高。
核心特点:
- 实现成本极低,改个 DNS 记录就行
- 流量分发粒度粗,按 DNS 解析请求来分
- 无法感知后端健康状态
- DNS 缓存会导致流量倾斜
我在项目中遇到过一个问题:某次大促前,我们给 CDN 域名配了 5 个源站 IP,结果流量全打到了其中两台。查了半天,发现是运营商 Local DNS 把解析结果缓存了,用户都连到了最先返回的两个 IP 上。嗯,这就是 DNS 轮询的硬伤——你控制不了客户端的缓存行为。
避坑指南:我曾经因为 DNS 轮询吃过亏。某次后端一台机器挂了,DNS 依然把流量解析过去,导致大量请求超时。后来我养成了习惯:DNS 轮询必须配合短 TTL(60-120秒),并且前端要有重试机制。
4.2 Nginx 反向代理:七层负载的瑞士军刀
Nginx 做反向代理,是我个人最常用的方案。它工作在七层,能看懂 HTTP 协议,可以根据 URL、Cookie、Header 做精细化路由。
# Nginx 反向代理 + 负载均衡配置示例
upstream backend {
# 默认是轮询
server 192.168.1.1:8080 weight=3;
server 192.168.1.2:8080 weight=2;
server 192.168.1.3:8080 backup; # 备用节点
# 健康检查
check interval=3000 rise=2 fall=5 timeout=1000;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Nginx 支持多种调度算法:
| 算法 | 说明 | 适用场景 |
|---|---|---|
| 轮询(默认) | 按顺序轮流分配 | 后端配置均匀 |
| 加权轮询 | 按权重分配,权重越高流量越多 | 后端性能不均 |
| IP Hash | 对客户端 IP 做哈希,固定分配到同一台 | 需要 Session 保持 |
| 最少连接 | 分配给当前活跃连接最少的节点 | 长连接场景 |
你想想看,Nginx 最爽的地方是什么?是它能做七层精细化控制。比如我可以根据 User-Agent 把移动端流量引到专门的集群,或者根据 Cookie 里的 session_id 做 sticky session。
我的经验:Nginx 作为反向代理时,要注意 keepalive 的配置。我曾经遇到一个坑:Nginx 和后端之间没有开启 keepalive,每次请求都新建 TCP 连接,导致后端机器连接数暴涨。加上 proxy_http_version 1.1; 和 proxy_set_header Connection ""; 后,问题解决。
4.3 LVS 四层负载:性能怪兽
LVS(Linux Virtual Server)工作在四层,它只处理 TCP/UDP 包的转发,不关心上层协议。性能极高,单机可以支撑百万级并发。
LVS 有三种工作模式:
| 模式 | 工作原理 | 优缺点 |
|---|---|---|
| NAT 模式 | 修改目标 IP 和端口,转发给后端 | 简单,但响应流量也经过 LVS,容易成为瓶颈 |
| TUN 模式 | IP 隧道封装,后端直接回包给客户端 | 性能好,但需要后端支持隧道协议 |
| DR 模式 | 修改 MAC 地址,后端直接回包 | 性能最高,但 LVS 和后端必须在同一二层网络 |
# LVS DR 模式配置示例
# 在 LVS 节点上执行
ipvsadm -A -t 192.168.1.100:80 -s rr
ipvsadm -a -t 192.168.1.100:80 -r 192.168.1.1:80 -g
ipvsadm -a -t 192.168.1.100:80 -r 192.168.1.2:80 -g
ipvsadm -a -t 192.168.1.100:80 -r 192.168.1.3:80 -g
我个人习惯用 DR 模式。为什么呢?因为响应流量不经过 LVS,后端直接回包给客户端,LVS 只处理入站流量,压力小很多。
注意:LVS DR 模式有个坑——后端机器必须在同一个 VLAN 里,而且后端机器需要配置 VIP 到 lo 接口,并关闭 ARP 响应。我曾经因为忘记配置 arp_ignore 和 arp_announce,导致整个网络 ARP 风暴,差点被运维同事拉黑。
4.4 一致性哈希:分布式缓存的救星
一致性哈希不是用来做流量分发的,它解决的是「分布式缓存/存储中,节点增减时数据迁移最少」的问题。
传统的取模哈希有个致命问题:节点数一变,几乎所有 key 的映射关系都变了,导致大量缓存失效,数据库瞬间被打爆。我见过不止一次,就因为加了一台缓存节点,整个系统雪崩了。
一致性哈希怎么解决?它把哈希值空间组织成一个环,每个节点落在环上。数据 key 哈希后,顺时针找到第一个节点。节点增减时,只影响相邻节点的数据。
实际工程中,光有基础的一致性哈希还不够。节点太少时,哈希环上的数据分布会不均匀。怎么办?引入虚拟节点。每个物理节点对应多个虚拟节点,分布在环上,让数据分布更均匀。
// 一致性哈希 + 虚拟节点实现片段(伪代码)
class ConsistentHash {
private final TreeMap<Integer, String> ring = new TreeMap<>();
private final int virtualNodes = 150; // 每个物理节点 150 个虚拟节点
public void addNode(String node) {
for (int i = 0; i < virtualNodes; i++) {
int hash = hash(node + "#" + i);
ring.put(hash, node);
}
}
public String getNode(String key) {
int hash = hash(key);
// 找到第一个大于等于 hash 的节点
Map.Entry<Integer, String> entry = ring.ceilingEntry(hash);
if (entry == null) {
entry = ring.firstEntry(); // 环的末尾,回到开头
}
return entry.getValue();
}
}
我的建议:虚拟节点数量不是越多越好。150-200 个虚拟节点基本能保证数据均匀分布。太多虚拟节点会导致内存占用大,而且节点变更时 rehash 的计算量也大。我在 Redis Cluster 和自研缓存组件里都实践过这个参数,效果不错。
4.5 四种策略怎么选?
最后给个总结,方便你快速决策:
| 策略 | 性能 | 灵活性 | 运维成本 | 最佳场景 |
|---|---|---|---|---|
| DNS 轮询 | 高(无中间节点) | 低 | 极低 | 多机房入口、CDN 源站 |
| Nginx 反向代理 | 中(七层解析有开销) | 高 | 低 | Web 服务、API 网关 |
| LVS 四层负载 | 极高 | 中 | 中 | TCP/UDP 入口、数据库读写分离 |
| 一致性哈希 | 高 | 中 | 中 | 分布式缓存、KV 存储 |
我个人习惯的组合是:入口用 LVS DR 模式扛流量,七层用 Nginx 做精细化路由,缓存层用一致性哈希保证数据分布。DNS 轮询只用在多机房容灾场景,作为最外层的流量调度。
记住一点:没有银弹。每种策略都有它的适用边界。你想想看,如果业务量只有几百 QPS,用 DNS 轮询就够了,何必上 LVS?反过来,如果每天几亿请求,还在用 DNS 轮询,那就是在玩火。