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 哈希后,顺时针找到第一个节点。节点增减时,只影响相邻节点的数据。

一致性哈希环结构图 A B C D key1 key2 key3 key4 key 顺时针找到第一个节点:key1→A, key2→B, key3→C, key4→D

实际工程中,光有基础的一致性哈希还不够。节点太少时,哈希环上的数据分布会不均匀。怎么办?引入虚拟节点。每个物理节点对应多个虚拟节点,分布在环上,让数据分布更均匀。

// 一致性哈希 + 虚拟节点实现片段(伪代码)
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 轮询,那就是在玩火。