Skip to content

负载均衡与代理 ​

#网络 · #负载均衡 · #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
L4L7
工作层TCP/UDPHTTP/gRPC
基于IP:PortURL, Header, Cookie
性能更高(不看包内容)稍低(需解析应用层协议)
功能简单分发内容路由、缓存、限流、认证
代表LVS, F5Nginx, 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 是"我把请求打包快递给你,你自己回复"。

维度NATDR (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 通告只用真实 IP

3.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
    end
nginx
# 被动检查(开源版可用)
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 MeshEnvoy + IstioSidecar 模式,东西向流量管理

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 × M

8. 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
模式请求路径响应路径要求性能
DRClient → LVS → RSRS → ClientRS 和 LVS 在同一物理网段🟢🟢 最高
NATClient → LVS → RSRS → LVS → ClientLVS 必须能路由到 RS🟡 中等
TUNClient → LVS → RSRS → ClientRS 需支持 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 实现 HA

9.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

参考 ​

批注模式

💬 文章评论

暂无评论,来说点什么吧 👇

编程学习笔记