21、负载均衡:四层负载均衡(LVS)、七层负载均衡(Nginx)、一致性哈希、动态权重分配

负载均衡这东西,说白了就是「把流量分给多台机器干」。

我刚开始做后端那会儿,觉得负载均衡不就是轮询嘛,有啥好研究的。直到有一次线上服务被流量冲垮,我才意识到——分流量这件事,分得好是艺术,分不好是灾难。

今天咱们就把这块掰开揉碎讲清楚。

为什么需要负载均衡?

单机总有扛不住的时候。CPU 打满、内存吃紧、连接数爆表……你想想看,一台 8 核 16G 的机器,能撑住多少并发?几千?几万?再往上就得加机器了。

但加了机器不等于就完事了。你得有个「交通指挥员」,告诉每个请求该去哪台机器。这个指挥员,就是负载均衡器。

核心目标:让每台后端机器「雨露均沾」,别让某台累死、某台闲死。

四层负载均衡(LVS)

四层负载均衡,工作在 OSI 模型的传输层(TCP/UDP)。它不看请求内容,只看 IP 和端口。

我印象最深的是 LVS(Linux Virtual Server)。这玩意儿性能极高,因为它在内核态干活,根本不进用户态。

LVS 的三种工作模式

模式 原理 特点
NAT 模式 修改目标 IP,转发给后端 后端回包必须经过 LVS,容易成为瓶颈
DR 模式 修改 MAC 地址,直接转发 后端直接回包给客户端,性能最好
TUN 模式 IP 隧道封装 适合跨机房部署

我个人习惯用 DR 模式。为什么呢?因为回包不经过 LVS,吞吐量能高出好几倍。我在一个广告竞价系统里用过,单台 LVS 扛了 20 万 QPS,CPU 占用不到 30%。

小提示:DR 模式要求后端机器和 LVS 在同一个二层网络,且后端需要配置 lo 接口的 VIP。

LVS 的调度算法

  • RR(轮询):一人一个,简单粗暴
  • WRR(加权轮询):给性能好的机器多分点
  • LC(最少连接):谁闲找谁
  • WLC(加权最少连接):结合权重和连接数

嗯,这里要注意:WLC 是 LVS 的默认算法,大多数场景下够用了。但如果你后端机器的性能差异很大,建议用 WRR 手动调权重。

七层负载均衡(Nginx)

七层负载均衡工作在应用层(HTTP/HTTPS)。它能看懂请求内容——URL、Cookie、Header,甚至请求体。

说白了,四层只管「把包送到」,七层还能管「送到哪去」。比如同一个域名下的 /api 走一组机器,/static 走另一组,这就是七层才能干的事。

Nginx 的负载均衡配置

upstream backend {
    server 192.168.1.10 weight=3;
    server 192.168.1.11 weight=2;
    server 192.168.1.12 backup;
}

server {
    listen 80;
    location / {
        proxy_pass http://backend;
    }
}

这段配置里,weight=3 表示这台机器分到的流量是另一台的 1.5 倍。backup 表示备用机,平时不干活,只有其他机器全挂了才顶上。

避坑指南:我曾经把 backup 和 weight 混用,结果备用机一直在接流量。后来才发现——backup 不能和 weight 同时生效,这是 Nginx 的设计限制。

Nginx 的 upstream 调度策略

策略 说明 适用场景
轮询(默认) 按顺序分配 机器配置均匀
ip_hash 根据客户端 IP 哈希 需要会话保持
least_conn 找连接数最少的 长连接场景
url_hash 根据 URL 哈希 缓存命中优化

你想想看,如果后端有缓存,用 url_hash 就能让同一个 URL 始终打到同一台机器,缓存命中率能提高不少。我在一个图片服务里试过,命中率从 40% 提到了 75%。

一致性哈希

普通哈希有个大问题:节点增减时,大量 key 会重新映射,导致缓存雪崩。

一致性哈希怎么解决的呢?它把哈希值空间看成一个环(0 到 2^32-1),节点和 key 都哈希到环上。每个 key 找顺时针方向的第一个节点。

Node A Node B Node C Node D Key1 Key2 一致性哈希环:Key 沿顺时针找到最近的节点

这样做的好处是:增加或删除一个节点,只影响环上相邻的一小部分 key,其他 key 不受影响。

虚拟节点:为了解决节点分布不均的问题,每个物理节点在环上放多个虚拟节点。我一般每个物理节点放 150 个虚拟节点,效果不错。

一致性哈希的 C++ 实现片段

class ConsistentHash {
private:
    std::map<uint32_t, std::string> ring_;
    int virtual_nodes_;

public:
    void AddNode(const std::string& node) {
        for (int i = 0; i < virtual_nodes_; ++i) {
            uint32_t hash = Hash(node + "#" + std::to_string(i));
            ring_[hash] = node;
        }
    }

    std::string GetNode(const std::string& key) {
        if (ring_.empty()) return "";
        uint32_t hash = Hash(key);
        auto it = ring_.lower_bound(hash);
        if (it == ring_.end()) it = ring_.begin();
        return it->second;
    }
};

这段代码看着简单,但有个坑——lower_bound 返回的是第一个不小于 hash 的迭代器。如果没找到,就回到环的起点。嗯,这个逻辑一定要写对,不然 key 会找不到节点。

动态权重分配

静态权重有个问题:你设了 weight=3,但机器可能因为 GC、磁盘 IO 等原因变慢了,这时候还按 3 分流量,反而会拖垮它。

动态权重就是让负载均衡器「感知」后端的状态,实时调整权重。

常见的动态权重策略

  • 基于响应时间:响应慢的机器,权重自动降低
  • 基于连接数:连接数多的机器,少分流量
  • 基于 CPU/内存:通过 agent 上报负载指标

我在一个实时推荐系统里用过动态权重。后端机器每 5 秒上报一次 CPU 和内存使用率,Nginx 通过 Lua 脚本动态调整 upstream 的权重。效果很明显——之前高峰期总有几台机器被打满,调整后整体吞吐量提升了 20%。

实现思路:可以用 Nginx + Lua(OpenResty)或者 Envoy 的主动健康检查 + 权重调整。我个人倾向 Envoy,因为它原生支持动态权重,配置更灵活。

动态权重的伪代码

// 每 5 秒执行一次
void UpdateWeights() {
    for (auto& node : nodes) {
        float cpu = GetCpuUsage(node.ip);
        float mem = GetMemUsage(node.ip);
        float score = 1.0 / (cpu * 0.6 + mem * 0.4 + 0.1);
        node.weight = std::max(1, (int)(score * 10));
    }
    ReloadUpstream();
}

这里 cpu * 0.6 + mem * 0.4 是我常用的加权公式。为什么 CPU 权重大?因为 CPU 是请求处理的核心瓶颈,内存一般够用就行。当然,具体比例得根据你的业务调。

避坑指南:我曾经把权重更新频率设成 1 秒一次,结果 Nginx reload 太频繁,导致少量请求丢失。后来改成 5 秒一次,配合 graceful reload,问题就解决了。

四层 vs 七层:怎么选?

维度 四层(LVS) 七层(Nginx)
性能 极高(内核态) 较高(用户态)
灵活性 低(只看 IP+端口) 高(能解析 HTTP 内容)
SSL 卸载 不支持 支持
会话保持 依赖源 IP 支持 Cookie、Header
典型场景 TCP/UDP 流量入口 HTTP/HTTPS 应用层分发

我的建议是:入口用 LVS 扛流量,应用层用 Nginx 做精细化分发。两层配合,既保证了性能,又保留了灵活性。

总结一下

负载均衡不是简单的「把请求分出去」。四层和七层各有各的战场,一致性哈希解决了节点变化的抖动问题,动态权重让系统能自适应负载变化。

你想想看,一个高并发系统,从 LVS 到 Nginx 再到后端服务,每一层都在做「分流」这件事。分得好,系统稳如泰山;分不好,加再多机器也是白搭。

我个人习惯在项目初期就用上动态权重,哪怕机器不多。因为一旦流量上来,再想改就来不及了——线上改权重,稍有不慎就是事故。


公众号:蓝海资料掘金营,微信deep3321