负载均衡与代理
#网络 · #负载均衡 · #L4 · #L7 · #一致性Hash · #健康检查 · #代理
当单机无法承载流量时,水平扩展是唯一出路。负载均衡器则是流量的"调度中心"——将请求均匀分配到后端节点,同时处理故障转移和健康检查。
1. 负载均衡的层次
mermaid
flowchart TB
subgraph L4["L4 传输层"]
L4_1["基于 IP:Port<br/>TCP/UDP 连接分发"]
L4_2["代表: LVS, F5, HAProxy (TCP mode)"]
end
subgraph L7["L7 应用层"]
L7_1["基于 HTTP Header/Body<br/>URL/Header/Cookie 路由"]
L7_2["代表: Nginx, HAProxy (HTTP mode), Envoy"]
end
Client --> L4
L4 --> L7
L7 --> Backend| L4 | L7 | |
|---|---|---|
| 工作层 | TCP/UDP | HTTP/gRPC |
| 基于 | IP:Port | URL, Header, Cookie |
| 性能 | 更高(不看包内容) | 稍低(需解析应用层协议) |
| 功能 | 简单分发 | 内容路由、缓存、限流、认证 |
| 代表 | LVS, F5 | Nginx, Envoy, Traefik |
2. 负载均衡算法
2.1 轮询(Round Robin)
go
type RoundRobin struct {
nodes []string
current uint64
}
func (r *RoundRobin) Next() string {
idx := atomic.AddUint64(&r.current, 1)
return r.nodes[idx%uint64(len(r.nodes))]
}2.2 加权轮询(Weighted Round Robin)
Nginx 的平滑加权轮询算法:
go
type SmoothWeighted struct {
nodes []*WeightedNode
}
type WeightedNode struct {
addr string
weight int // 配置权重
currentWeight int // 当前有效权重
}
func (s *SmoothWeighted) Next() string {
totalWeight := 0
var best *WeightedNode
for _, node := range s.nodes {
node.currentWeight += node.weight
totalWeight += node.weight
if best == nil || node.currentWeight > best.currentWeight {
best = node
}
}
best.currentWeight -= totalWeight
return best.addr
}运行示例:A:5, B:1, C:1 → 序列 A A B A C A A,完美平滑分布。
2.3 最少连接
go
// 选择当前活跃连接数最少的节点
// 适合长连接场景(WebSocket, gRPC streaming)
type LeastConn struct {
nodes []*ConnNode
}
type ConnNode struct {
addr string
conns int64 // 原子操作
}2.4 一致性哈希
详见 一致性Hash与Raft。用于有状态服务,需要请求粘性(sticky session)。
2.5 最短响应时间
选择历史响应时间最短的节点。需要健康检查来更新响应时间数据。
3. LVS(Linux Virtual Server)
LVS 是内核级的 L4 负载均衡器,性能碾压所有用户态方案。它的核心能力是将发往 VIP(虚拟 IP)的数据包,按照规则改写目的地址/端口或 MAC 地址,转发到后端 Real Server。三种工作模式本质上是"在 OSI 哪一层改写数据包"的区别:
3.1 三种工作模式对比
mermaid
flowchart TB
subgraph NAT["NAT 模式"]
NAT_C["Client"] -->|"1. 请求 VIP"| NAT_LB["LVS (NAT)"]
NAT_LB -->|"2. DNAT: VIP→RS_IP<br/>修改目标 IP:Port"| NAT_RS["Real Server"]
NAT_RS -->|"3. 回复: RS_IP→Client"| NAT_LB
NAT_LB -->|"4. SNAT: RS_IP→VIP<br/>修改源 IP"| NAT_C
end
subgraph DR["DR 模式 (最常用)"]
DR_C["Client"] -->|"1. 请求 VIP (MAC=LVS)"| DR_LB["LVS (DR)"]
DR_LB -->|"2. 只改 MAC 地址<br/>目标 MAC: LVS→RS"| DR_RS["Real Server"]
DR_RS -->|"3. 直接回复<br/>源 IP=VIP (配置在 lo 上)"| DR_C
end
subgraph TUN["IP Tunnel 模式"]
TUN_C["Client"] -->|"1. 请求 VIP"| TUN_LB["LVS (TUN)"]
TUN_LB -->|"2. IPIP 封装<br/>外层: LVS→RS<br/>内层: Client→VIP"| TUN_RS["Real Server<br/>(跨机房)"]
TUN_RS -->|"3. 解封后直接回复<br/>源 IP=VIP"| TUN_C
end三种模式的本质区别:NAT 是"请求和回复都经过我",DR 是"请求过我但回复你自己来",TUN 是"我把请求打包快递给你,你自己回复"。
| 维度 | NAT | DR (Direct Routing) | TUN (IP Tunneling) |
|---|---|---|---|
| 改写内容 | 目标 IP + 端口 | 目标 MAC 地址 | 外层 IP 头(隧道封装) |
| 回复路径 | 必须经 LVS | 直接回复客户端 | 直接回复客户端 |
| LVS 吞吐瓶颈 | ❌ 高(双向流量) | ✅ 低(单向流量) | ✅ 低(单向流量) |
| RS 要求 | 任意 OS | 必须在同一物理网段 + lo 配 VIP + 禁用 ARP | 必须支持 IPIP 隧道(Linux 原生支持) |
| RS 可跨网段 | ✅ | ❌(同 L2) | ✅(可跨机房!) |
| 端口映射 | ✅ 支持 | ❌ | ❌ |
| 典型场景 | 测试环境 | 生产最常用 | 异地多活 |
DR 模式核心技术细节
DR 模式的核心技巧:Real Server 在 lo(回环接口)上配置 VIP,但必须对 VIP 的 ARP 请求不响应——否则物理交换机学习到的 MAC 地址会指向 RS 而非 LVS,流量绕过 LVS 直接打到 RS:
bash
# Real Server 上的配置(关键!)
ip addr add 10.0.0.100/32 dev lo # lo 上配 VIP
echo 1 > /proc/sys/net/ipv4/conf/all/arp_ignore # 不响应 ARP
echo 2 > /proc/sys/net/ipv4/conf/all/arp_announce # ARP 通告只用真实 IP3.2 Keepalived + LVS 高可用
单台 LVS 挂了整个集群就废了——Keepalived 通过 VRRP 协议解决这个问题:
mermaid
sequenceDiagram
participant Master as "LVS Master<br/>(VIP 持有者)"
participant Backup as "LVS Backup<br/>(待命)"
participant RS as "Real Servers"
Note over Master, Backup: VRRP 心跳 (组播 224.0.0.18)
loop 每 1 秒
Master->>Backup: VRRP Advertisement (优先级 100)
Backup-->>Backup: 收到心跳,保持 BACKUP
end
Master-->>Master: ❌ 宕机!
Note over Backup: 3 秒未收到心跳 → 触发选举
Backup->>Backup: 提升为 MASTER
Backup->>Backup: 接管 VIP + 下发 LVS 规则
Backup-->>RS: 流量自动切换VRRP 协议通过组播心跳和优先级比较实现主备切换,切换时间通常在 3-10 秒。
4. Nginx 反向代理
4.1 核心配置
nginx
upstream backend {
# 加权轮询(默认)
server 10.0.0.1:8080 weight=5;
server 10.0.0.2:8080 weight=3;
server 10.0.0.3:8080 backup; # 备用
# 其他策略:
# least_conn; 最少连接
# ip_hash; 根据客户端 IP 哈希
# hash $request_uri; 根据 URL 哈希
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}4.2 健康检查机制
负载均衡器不是"傻傻地轮询"——它必须知道哪些后端"活着"才能分流请求。Nginx 提供两种检查方式:
mermaid
flowchart LR
subgraph Passive["被动检查(Nginx 开源版)"]
P1["请求转发到 RS"]
P2["RS 无响应"]
P3["标记失败"]
P4["fail_timeout 后重试"]
P1 --> P2 --> P3 --> P4
end
subgraph Active["主动检查(Nginx Plus)"]
A1["定期发送健康检查"]
A2["探测 /health 端点"]
A3["连续失败 n 次"]
A4["标记为 down"]
A5["恢复后自动上线"]
A1 --> A2 --> A3 --> A4 --> A5
endnginx
# 被动检查(开源版可用)
upstream backend {
server 10.0.0.1:8080 max_fails=3 fail_timeout=30s;
# max_fails=3: 30s 内失败 3 次就标记为 down
# fail_timeout=30s: 30 秒后重新尝试
server 10.0.0.2:8080 max_fails=3 fail_timeout=30s;
server 10.0.0.3:8080 backup; # 所有主节点 down 后才启用
}
# 主动检查(Nginx Plus 或 openresty + lua-resty-healthcheck)
# health_check interval=5s fails=3 passes=2 uri=/health;关键参数解读:
max_fails和fail_timeout是一对配合使用的参数。max_fails指的是在fail_timeout时间窗口内的最大失败次数,超过就标记为 down。这个设计避免了因单次网络抖动而误摘节点。
4.3 会话保持(Sticky Session)
nginx
upstream backend {
# ip_hash: 根据客户端 IP 做哈希,同一 IP 始终到同一 RS
ip_hash;
server 10.0.0.1:8080;
server 10.0.0.2:8080;
# ⚠️ 缺点:NAT 网络下同一出口 IP 的所有用户都到一个 RS
}
# Cookie 粘性(需要 Nginx Plus 或 OpenResty)
# sticky cookie srv_id expires=1h domain=.example.com path=/;5. 一致性哈希在负载均衡中的应用
详见 一致性Hash与Raft。一致性哈希的核心价值在于节点增减时只影响局部数据:
普通哈希(取模):
3 节点 → 添加第 4 个 → ~75% 的请求映射变化 → 缓存全失效
一致性哈希(哈希环 + 虚拟节点):
3 节点 → 添加第 4 个 → ~25% 的请求映射变化 → 仅部分缓存失效适用场景判断:
| 场景 | 推荐算法 | 原因 |
|---|---|---|
| 无状态服务(REST API) | 轮询/最少连接 | 无需粘性,均匀分布 |
| 有状态缓存(Redis Cluster) | 一致性哈希 | 节点增减最小影响缓存 |
| WebSocket 长连接 | 最少连接 + ip_hash | 避免长连接分布不均 |
| 按用户分片(灰度发布) | 按 Header/Cookie 哈希 | 指定用户固定到灰度环境 |
6. 代理类型对比
| 类型 | 代表 | 特点 |
|---|---|---|
| 正向代理 | Squid, Shadowsocks | 代理客户端访问外部网络 |
| 反向代理 | Nginx, HAProxy | 代理服务端接收外部请求 |
| 透明代理 | iptables REDIRECT | 客户端无感知 |
| API 网关 | Kong, APISIX | 路由 + 认证 + 限流 + 日志 |
| Service Mesh | Envoy + Istio | Sidecar 模式,东西向流量管理 |
7. 高可用架构模式
7.1 主备模式
Active LB ──→ Backend Pool
Standby LB ──→ (待命)
↓
VRRP 心跳检测7.2 主主模式
LB-1 ──→ Backend Pool
LB-2 ──→ Backend Pool
↓
DNS / Anycast 分流7.3 分层负载均衡
DNS Round Robin
↓
L4 LB (LVS) × 2 (高吞吐)
↓
L7 LB (Nginx) × N (内容路由)
↓
Backend × M8. P2C (Power of Two Choices) 算法详解
P2C 是 gRPC 默认客户端负载均衡算法,也是 Google 内部最广泛使用的算法之一。
8.1 算法原理
text
P2C 算法:
1. 从 N 个后端中随机选 2 个
2. 比较两个后端的当前负载(通常用 inflight 请求数)
3. 选择负载较低的那个
为什么是 "Power of Two"?
- 随机选 2 个的效果已经接近"选最优"
- 选 1 个 (纯随机): 最坏情况下后端负载相差 O(log N / log log N)
- 选 2 个 (P2C): 最坏情况下后端负载相差 O(log log N)
- 选 > 2 个: 回报递减, 开销增加mermaid
flowchart TD
R["新请求到达"] --> S["随机选 2 个后端"]
S --> B1["后端 A: inflight=3"]
S --> B2["后端 B: inflight=1"]
B1 --> C["选择 B (inflight 更小)"]
B2 --> C
C --> D["发送请求到 B"]8.2 EWMA (指数加权移动平均) 权重计算
gRPC P2C 用 EWMA 平滑后端延迟,避免瞬间抖动导致错误判断:
text
EWMA 公式:
score(t) = α × latency(t) + (1-α) × score(t-1)
α = 衰减因子 (gRPC 默认 0.2, 即 80% 权重在历史)
latency(t) = 当前请求的延迟
score(t) = EWMA 分数 (越小越好)
效果:
- 单个慢请求不会立即拉低分数
- 持续变慢才会导致分数上升
- α 越大 → 越敏感, 但越不稳定go
// gRPC P2C + EWMA 简化实现
type SubConn struct {
inflight int64 // 当前正在处理的请求数
ewma float64 // EWMA 延迟分数
lastPick time.Time // 上次被选中时间
}
func pick(subConns []*SubConn) *SubConn {
if len(subConns) == 0 { return nil }
if len(subConns) == 1 { return subConns[0] }
// 1. 随机选 2 个
i := rand.Intn(len(subConns))
j := rand.Intn(len(subConns) - 1)
if j >= i { j++ }
a, b := subConns[i], subConns[j]
// 2. 比较: inflight 优先, 相同时比 EWMA
if a.inflight < b.inflight { return a }
if b.inflight < a.inflight { return b }
if a.ewma < b.ewma { return a }
return b
}8.3 实际 QPS 不均排查
text
现象: 某后端 QPS 明显低于其他后端
排查顺序:
1. 检查健康检查: 该后端是否被标记为不健康
2. 检查权重: 该后端权重是否被设置过低
3. 检查 P2C inflight: 该后端 inflight 是否偏高(处理慢)
4. 检查 EWMA: 该后端历史延迟是否偏高
5. 检查连接池: 该后端连接数是否少(连接未预热)
6. 检查下游依赖: 该后端依赖的 DB/Redis 是否有热点| 算法 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| 轮询 | 简单,绝对公平 | 长连接下不均衡 | 短连接、同质后端 |
| 最少连接 | 适合长连接 | 连接数是滞后指标 | Nginx upstream |
| 权重轮询 | 可以手动调权 | 静态权重不反映实时负载 | 异质后端(不同配置) |
| P2C + EWMA | 自动适应、低开销 | 实现复杂 | gRPC、微服务内部 |
| 一致性哈希 | 缓存命中率高 | 增减节点影响局部 | Redis/Memcached |
9. LVS/DPVS — 内核态四层负载均衡
在 Nginx(用户态七层代理)之下,还有一层更高性能的四层负载均衡——LVS(Linux Virtual Server)和 DPVS(基于 DPDK 的 LVS)。
9.1 LVS 三种工作模式
mermaid
flowchart TB
subgraph DR["DR 模式 (Direct Routing) — 最高性能"]
Client1["Client"] -->|"请求"| LVS1["LVS<br/>修改 MAC 地址"]
LVS1 -->|"同一 MAC 帧<br/>转发到后端"| RS1["Real Server 1"]
RS1 -->|"直接回复 Client<br/>(不经过 LVS)"| Client1
end
subgraph NAT["NAT 模式 — 最灵活"]
Client2["Client"] -->|"请求"| LVS2["LVS<br/>修改目的 IP:Port"]
LVS2 -->|"转发"| RS2["Real Server 2"]
RS2 -->|"回复必须<br/>经过 LVS"| LVS2
LVS2 -->|"修改源 IP"| Client2
end
subgraph TUN["TUN 模式 (IP Tunneling)"]
Client3["Client"] -->|"请求"| LVS3["LVS<br/>IPIP 封装"]
LVS3 -->|"隧道转发"| RS3["Real Server 3<br/>(IPIP 解封)"]
RS3 -->|"直接回复"| Client3
end| 模式 | 请求路径 | 响应路径 | 要求 | 性能 |
|---|---|---|---|---|
| DR | Client → LVS → RS | RS → Client | RS 和 LVS 在同一物理网段 | 🟢🟢 最高 |
| NAT | Client → LVS → RS | RS → LVS → Client | LVS 必须能路由到 RS | 🟡 中等 |
| TUN | Client → LVS → RS | RS → Client | RS 需支持 IPIP | 🟢 高 |
9.2 DPVS — 用户态高性能 LVS
传统 LVS 的问题:
- 基于 Linux 内核 netfilter,包处理路径长
- 中断 + 系统调用开销
- 单核性能天花板 ~1M pps(每秒包数)
DPVS 的改进 (基于 DPDK):
- 绕过内核协议栈,直接在用户态处理网络包
- 轮询模式(PMD)代替中断
- 单核可达 10M+ pps
- 支持 FullNAT、SNAT、FNAT 模式
- 支持一致性哈希调度
性能对比:
传统 LVS (DR 模式): ~3M pps/core (受限于中断)
传统 LVS (NAT 模式): ~0.5M pps/core (CT 连接跟踪开销)
DPVS (FNAT 模式): ~12M pps/core ✅
适用场景:
- 四层网关 / 入口负载均衡
- 需要每秒千万级连接的高性能场景
- 配合 keepalived 实现 HA9.3 Nginx upstream 配置实战
nginx
# Nginx 四层代理 (stream 模块)
stream {
upstream mysql_backend {
# 加权最少连接
least_conn;
server 10.0.1.1:3306 weight=5 max_fails=3 fail_timeout=30s;
server 10.0.1.2:3306 weight=3 max_fails=3 fail_timeout=30s;
server 10.0.1.3:3306 backup; # 备用节点,只在其他节点全挂时启用
# 长连接池
keepalive 32;
keepalive_timeout 60s;
keepalive_requests 100;
}
server {
listen 3306;
proxy_pass mysql_backend;
}
}
# Nginx 七层代理 (http 模块)
http {
upstream app_backend {
# 一致性哈希(按 $request_uri 确保相同 URL 到同一后端)
hash $request_uri consistent;
server 10.0.2.1:8080 weight=2;
server 10.0.2.2:8080 weight=2;
# 健康检查(商业版 Nginx Plus 或开源 nginx-module)
# check interval=3000 rise=2 fall=5 timeout=1000;
}
server {
listen 80;
location / {
proxy_pass http://app_backend;
proxy_next_upstream error timeout http_500 http_502 http_503;
proxy_next_upstream_tries 2; # 最多重试 2 次
}
}
}9.4 gRPC 客户端负载均衡
gRPC 基于 HTTP/2 长连接,不能用传统的 L4 负载均衡——因为所有请求复用同一条 TCP 连接。
go
// gRPC 客户端负载均衡 — 客户端感知后端列表
import (
"google.golang.org/grpc"
"google.golang.org/grpc/resolver"
_ "google.golang.org/grpc/xds" // xDS 服务发现
)
// 方案1: DNS 解析 + 客户端轮询
conn, _ := grpc.Dial(
"dns:///my-service:8080", // DNS SRV 记录
grpc.WithDefaultServiceConfig(`{
"loadBalancingPolicy": "round_robin"
}`),
)
// 方案2: 自定义 Name Resolver
conn, _ := grpc.Dial(
"my-resolver:///my-service",
grpc.WithDefaultServiceConfig(`{
"loadBalancingPolicy": "round_robin",
"healthCheckConfig": {
"serviceName": "my.Service"
}
}`),
)
// 方案3: xDS (Envoy/Istio 控制面发现)
conn, _ := grpc.Dial(
"xds:///my-service",
grpc.WithDefaultServiceConfig(`{
"loadBalancingPolicy": "weighted_round_robin"
}`),
)gRPC 客户端 LB 的两种架构:
Lookaside LB (传统方案):
Client → LB (如 Nginx/Envoy) → Backend
问题: LB 成为瓶颈,多一跳延迟
Client-side LB (gRPC 推荐):
Client ← 服务发现(xDS/DNS/etcd)← 后端注册
Client 直接连接后端池,自己做负载均衡
优势: 无代理瓶颈,零额外跳数9.5 全链路负载均衡总结
mermaid
flowchart TB
DNS["DNS / GTM<br/>全局流量调度"] --> L4["L4 LB: LVS/DPVS<br/>ECMP + BGP<br/>百万级 pps"]
L4 --> L7["L7 LB: Nginx/Envoy<br/>URL 路由、限流、鉴权<br/>万级 rps"]
L7 --> Service["Service: gRPC Client LB<br/>P2C + 自适应<br/>内部服务间"]
Service --> App["App Backend"]
style DNS fill:#FF9800,color:#fff
style L4 fill:#F44336,color:#fff
style L7 fill:#2196F3,color:#fff
style Service fill:#4CAF50,color:#fff
登录后即可发表评论 👇