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%。
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 表示备用机,平时不干活,只有其他机器全挂了才顶上。
Nginx 的 upstream 调度策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 轮询(默认) | 按顺序分配 | 机器配置均匀 |
| ip_hash | 根据客户端 IP 哈希 | 需要会话保持 |
| least_conn | 找连接数最少的 | 长连接场景 |
| url_hash | 根据 URL 哈希 | 缓存命中优化 |
你想想看,如果后端有缓存,用 url_hash 就能让同一个 URL 始终打到同一台机器,缓存命中率能提高不少。我在一个图片服务里试过,命中率从 40% 提到了 75%。
一致性哈希
普通哈希有个大问题:节点增减时,大量 key 会重新映射,导致缓存雪崩。
一致性哈希怎么解决的呢?它把哈希值空间看成一个环(0 到 2^32-1),节点和 key 都哈希到环上。每个 key 找顺时针方向的第一个节点。
这样做的好处是:增加或删除一个节点,只影响环上相邻的一小部分 key,其他 key 不受影响。
一致性哈希的 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%。
动态权重的伪代码
// 每 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 是请求处理的核心瓶颈,内存一般够用就行。当然,具体比例得根据你的业务调。
四层 vs 七层:怎么选?
| 维度 | 四层(LVS) | 七层(Nginx) |
|---|---|---|
| 性能 | 极高(内核态) | 较高(用户态) |
| 灵活性 | 低(只看 IP+端口) | 高(能解析 HTTP 内容) |
| SSL 卸载 | 不支持 | 支持 |
| 会话保持 | 依赖源 IP | 支持 Cookie、Header |
| 典型场景 | TCP/UDP 流量入口 | HTTP/HTTPS 应用层分发 |
我的建议是:入口用 LVS 扛流量,应用层用 Nginx 做精细化分发。两层配合,既保证了性能,又保留了灵活性。
总结一下
负载均衡不是简单的「把请求分出去」。四层和七层各有各的战场,一致性哈希解决了节点变化的抖动问题,动态权重让系统能自适应负载变化。
你想想看,一个高并发系统,从 LVS 到 Nginx 再到后端服务,每一层都在做「分流」这件事。分得好,系统稳如泰山;分不好,加再多机器也是白搭。
我个人习惯在项目初期就用上动态权重,哪怕机器不多。因为一旦流量上来,再想改就来不及了——线上改权重,稍有不慎就是事故。