土法炼钢兴趣小组的算法知识备份

【网络工程】负载均衡工程实践:会话保持、灰度发布与容量规划

文章导航

分类入口
network
标签入口
#load-balancing#session-affinity#canary#capacity-planning#high-availability

目录

前面五篇文章从 L4/L7 架构、调度算法、健康检查到全局负载均衡,覆盖了负载均衡的技术原理。但在生产环境中,工程师面对的往往是更具体的问题:用户登录后刷新页面为什么 session 丢了?新版本怎么灰度发布而不影响所有用户?LB 本身的容量够不够、挂了怎么办?

这篇文章专注于负载均衡的工程实践——这些细节决定了一个负载均衡架构能否在生产环境中稳定运行。

一、会话保持

1.1 为什么需要会话保持

在无状态的 REST 服务中,任何一个后端实例都能处理任何请求——不需要会话保持。但现实中很多场景需要同一用户的请求被路由到同一台后端:

场景 原因 示例
服务端 Session Session 存储在本地内存 传统 Java Web 应用
文件上传 分片上传需要发到同一台 大文件分片上传
WebSocket 长连接绑定到特定后端 实时聊天
本地缓存 利用后端的本地缓存 内存缓存命中率
有状态计算 中间状态存在本地 流式处理、分步向导

核心矛盾:会话保持本质上是在负载均衡状态亲和之间做取舍。完美的负载均衡要求请求均匀分布,而会话保持要求特定请求必须去特定后端——这两个目标天然冲突。

1.2 源 IP 哈希

最简单的会话保持方式——根据客户端 IP 的哈希值选择后端:

# Nginx 配置
upstream backend {
    ip_hash;
    server 10.0.1.1:8080;
    server 10.0.1.2:8080;
    server 10.0.1.3:8080;
}
upstream backend
    balance source
    server app1 10.0.1.1:8080 check
    server app2 10.0.1.2:8080 check
    server app3 10.0.1.3:8080 check

原理hash(client_ip) % backend_count → 选择后端

问题

  1. NAT 环境下失效。同一个办公室的 100 个用户共享一个出口 IP,所有请求都被路由到同一台后端——负载极不均匀。
  2. 移动网络 IP 变化。手机从 WiFi 切到 4G,IP 变化,会话丢失。
  3. 后端增减导致大量重映射。新增一台后端时,hash % 3 变成 hash % 4,大部分用户的映射都会改变。
后端增减的影响:
3 台后端: hash=7 → 7%3=1 → backend[1]
4 台后端: hash=7 → 7%4=3 → backend[3]  ← 映射变了

使用一致性哈希可以减少重映射:
Nginx: hash $remote_addr consistent;

更精确的方案——通过 Cookie 标识用户,将相同 Cookie 的请求路由到同一后端:

方式 1:LB 插入 Cookie

# Nginx Plus(商业版)或 HAProxy
# HAProxy 配置
backend app_servers
    balance roundrobin
    cookie SERVERID insert indirect nocache httponly secure
    server app1 10.0.1.1:8080 check cookie s1
    server app2 10.0.1.2:8080 check cookie s2
    server app3 10.0.1.3:8080 check cookie s3

工作流程:

第一次请求:
  Client → LB → 选择 app2(Round Robin)
  LB 在响应中插入: Set-Cookie: SERVERID=s2; Path=/; HttpOnly; Secure
  Client ← LB ← app2

后续请求:
  Client → (Cookie: SERVERID=s2) → LB
  LB 读取 Cookie,路由到 app2
  Client ← LB ← app2

方式 2:应用层 Session Cookie

# Nginx 根据应用设置的 JSESSIONID 做路由
upstream backend {
    hash $cookie_JSESSIONID consistent;
    server 10.0.1.1:8080;
    server 10.0.1.2:8080;
    server 10.0.1.3:8080;
}

方式 3:Envoy 的路由级 Cookie 亲和

# Envoy 配置
clusters:
  - name: app_cluster
    type: STRICT_DNS
    lb_policy: RING_HASH
    ring_hash_lb_config:
      minimum_ring_size: 1024
    load_assignment:
      cluster_name: app_cluster
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 10.0.1.1
                    port_value: 8080
            - endpoint:
                address:
                  socket_address:
                    address: 10.0.1.2
                    port_value: 8080

route_config:
  virtual_hosts:
    - name: app
      domains: ["*"]
      routes:
        - match:
            prefix: "/"
          route:
            cluster: app_cluster
            hash_policy:
              - cookie:
                  name: "session_affinity"
                  ttl: 3600s    # Cookie 有效期
                  path: "/"

1.4 会话保持的故障处理

会话保持的最大挑战是:绑定的后端不可用时怎么办?

场景:
  用户绑定 → app2
  app2 宕机
  下一次请求到来 → LB 发现 app2 不可用

选项:
  A. 返回 503 错误(不推荐,用户体验差)
  B. 路由到其他后端,用户丢失 Session(最常见)
  C. 路由到其他后端,但 Session 从共享存储恢复(推荐)

推荐架构:会话外部化

                ┌─────────────┐
                │ Redis Cluster│
                │ (Session Store)│
                └──────┬──────┘
                       │
        ┌──────────────┼──────────────┐
        │              │              │
   ┌────┴─────┐  ┌─────┴────┐  ┌─────┴────┐
   │  app1    │  │  app2    │  │  app3    │
   │(读写Redis)│  │(读写Redis)│  │(读写Redis)│
   └──────────┘  └──────────┘  └──────────┘
# Flask + Redis Session 示例
from flask import Flask, session
from flask_session import Session
import redis

app = Flask(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.Redis(
    host='redis-cluster.internal',
    port=6379,
    db=0,
    socket_timeout=1,        # 超时要短,避免阻塞请求
    socket_connect_timeout=1,
    retry_on_timeout=True
)
app.config['SESSION_KEY_PREFIX'] = 'session:'
app.config['PERMANENT_SESSION_LIFETIME'] = 3600  # 1小时
Session(app)

@app.route('/login', methods=['POST'])
def login():
    session['user_id'] = request.form['user_id']
    session['login_time'] = time.time()
    return jsonify({"status": "ok"})

@app.route('/profile')
def profile():
    user_id = session.get('user_id')
    if not user_id:
        return jsonify({"error": "not logged in"}), 401
    return jsonify({"user_id": user_id})

Session 外部化后,任何后端实例都可以处理任何请求,不再需要会话保持——这是最优解。但迁移旧系统到外部 Session 需要时间,所以会话保持作为过渡方案仍有价值。

1.5 会话保持方案对比

方案 精度 NAT 友好 移动端友好 后端增减影响 配置复杂度
源 IP 哈希
源 IP 一致性哈希
LB 插入 Cookie
应用 Cookie 哈希
Session 外部化 N/A N/A N/A 高(改代码)

决策建议:

二、灰度发布

2.1 灰度发布的调度层次

灰度发布(Canary Release)是将新版本逐步推送给部分用户,验证无问题后再全量发布。负载均衡器是实现灰度路由的天然位置:

                        全部流量
                           │
                     ┌─────┴─────┐
                     │     LB    │ ← 灰度路由决策点
                     └─────┬─────┘
                    ╱      │      ╲
              95%  ╱       │       ╲  5%
                  ╱        │        ╲
          ┌──────┴───┐           ┌───┴──────┐
          │ v1.0 集群 │           │ v1.1 集群 │
          │ (stable)  │           │ (canary)  │
          └──────────┘           └──────────┘

2.2 基于权重的灰度

最简单的灰度——按百分比将流量分配给新旧版本:

# Nginx 权重灰度
upstream stable {
    server 10.0.1.1:8080;
    server 10.0.1.2:8080;
    server 10.0.1.3:8080;
}

upstream canary {
    server 10.0.2.1:8080;
}

# 使用 split_clients 按百分比分流
split_clients "${remote_addr}${uri}" $upstream_variant {
    5%     canary;
    *      stable;
}

server {
    listen 80;
    location / {
        proxy_pass http://$upstream_variant;
        # 添加头标识版本
        proxy_set_header X-Upstream-Variant $upstream_variant;
    }
}
# Envoy 权重路由
route_config:
  virtual_hosts:
    - name: app
      domains: ["*"]
      routes:
        - match:
            prefix: "/"
          route:
            weighted_clusters:
              clusters:
                - name: stable
                  weight: 95
                - name: canary
                  weight: 5
              total_weight: 100
# HAProxy 权重灰度
frontend http_front
    bind *:80
    default_backend stable

    # 5% 流量到 canary
    acl is_canary rand(100) lt 5
    use_backend canary if is_canary

backend stable
    balance roundrobin
    server s1 10.0.1.1:8080 check
    server s2 10.0.1.2:8080 check

backend canary
    balance roundrobin
    server c1 10.0.2.1:8080 check

权重灰度的问题:同一个用户可能一次请求打到 v1.0,下一次打到 v1.1——体验不一致。解决方案是结合 Cookie 保持灰度分组稳定性。

2.3 基于 Header/Cookie 的灰度

更精确的灰度——根据请求特征决定版本:

# Nginx 基于 Header 的灰度路由
map $http_x_canary $upstream_target {
    "true"    canary;
    default   stable;
}

# 基于 Cookie 的灰度路由
map $cookie_canary_group $upstream_target {
    "canary"  canary;
    default   stable;
}

# 基于用户 ID 的灰度(取模分组)
map $cookie_user_id $upstream_target {
    ~^[0-9]*[0-4]$  canary;    # 用户 ID 尾号 0-4 → canary (50%)
    default         stable;
}

server {
    listen 80;
    location / {
        proxy_pass http://$upstream_target;
        add_header X-Served-By $upstream_target;
    }
}
# Envoy 基于 Header 的灰度
route_config:
  virtual_hosts:
    - name: app
      domains: ["*"]
      routes:
        # 内部测试流量
        - match:
            prefix: "/"
            headers:
              - name: "x-canary"
                exact_match: "true"
          route:
            cluster: canary
        # 特定用户
        - match:
            prefix: "/"
            headers:
              - name: "x-user-group"
                exact_match: "beta-tester"
          route:
            cluster: canary
        # 默认走稳定版
        - match:
            prefix: "/"
          route:
            cluster: stable

2.4 灰度发布的完整流程

#!/bin/bash
# canary_deploy.sh — 灰度发布自动化脚本
# 用法: ./canary_deploy.sh <new_version> <canary_weight>

NEW_VERSION=$1
CANARY_WEIGHT=${2:-5}  # 默认 5%

echo "=== 灰度发布 v${NEW_VERSION} (${CANARY_WEIGHT}%) ==="

# Step 1: 部署 Canary 实例
echo "Step 1: 部署 Canary 实例..."
kubectl set image deployment/app-canary \
    app=myapp:${NEW_VERSION} --record

kubectl rollout status deployment/app-canary --timeout=120s
if [ $? -ne 0 ]; then
    echo "Canary 部署失败,中止"
    kubectl rollout undo deployment/app-canary
    exit 1
fi

# Step 2: 设置流量权重
echo "Step 2: 设置 Canary 流量权重: ${CANARY_WEIGHT}%..."
kubectl patch virtualservice app-vs --type=json -p="[{
    \"op\": \"replace\",
    \"path\": \"/spec/http/0/route\",
    \"value\": [
        {\"destination\": {\"host\": \"app-stable\"}, \"weight\": $((100 - CANARY_WEIGHT))},
        {\"destination\": {\"host\": \"app-canary\"}, \"weight\": ${CANARY_WEIGHT}}
    ]
}]"

# Step 3: 监控 Canary 指标
echo "Step 3: 监控 Canary 指标 (300s)..."
MONITOR_DURATION=300
INTERVAL=30
ELAPSED=0

while [ $ELAPSED -lt $MONITOR_DURATION ]; do
    # 查询 Canary 的错误率
    ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query" \
        --data-urlencode "query=rate(http_requests_total{deployment=\"canary\",code=~\"5..\"}[1m]) / rate(http_requests_total{deployment=\"canary\"}[1m]) * 100" \
        | jq -r '.data.result[0].value[1] // "0"')

    # 查询 Canary 的 P99 延迟
    P99_LATENCY=$(curl -s "http://prometheus:9090/api/v1/query" \
        --data-urlencode "query=histogram_quantile(0.99, rate(http_request_duration_seconds_bucket{deployment=\"canary\"}[1m]))" \
        | jq -r '.data.result[0].value[1] // "0"')

    printf "  [%3ds] 错误率: %.2f%%, P99延迟: %.3fs\n" \
        $ELAPSED "$ERROR_RATE" "$P99_LATENCY"

    # 如果错误率 > 1% 或 P99 > 2s,自动回滚
    if (( $(echo "$ERROR_RATE > 1.0" | bc -l) )); then
        echo "错误率超过阈值,自动回滚!"
        kubectl patch virtualservice app-vs --type=json -p="[{
            \"op\": \"replace\",
            \"path\": \"/spec/http/0/route\",
            \"value\": [{\"destination\": {\"host\": \"app-stable\"}, \"weight\": 100}]
        }]"
        kubectl rollout undo deployment/app-canary
        exit 1
    fi

    sleep $INTERVAL
    ELAPSED=$((ELAPSED + INTERVAL))
done

echo "Step 4: Canary 验证通过,逐步扩大流量..."
for weight in 25 50 75 100; do
    echo "  扩大到 ${weight}%..."
    kubectl patch virtualservice app-vs --type=json -p="[{
        \"op\": \"replace\",
        \"path\": \"/spec/http/0/route\",
        \"value\": [
            {\"destination\": {\"host\": \"app-stable\"}, \"weight\": $((100 - weight))},
            {\"destination\": {\"host\": \"app-canary\"}, \"weight\": ${weight}}
        ]
    }]"
    sleep 60
done

echo "=== 灰度发布完成 ==="

2.5 灰度策略对比

策略 粒度 一致性 复杂度 适用场景
权重随机 请求级 无状态 API
Cookie 分组 用户级 Web 应用
Header 路由 请求级 按 Header 内部测试/AB 测试
用户 ID 取模 用户级 需要稳定分组
地域灰度 地域级 全球化服务

三、LB 容量规划

3.1 LB 的性能指标

负载均衡器本身也有性能瓶颈。容量规划需要关注以下指标:

指标 L4 LB L7 LB 说明
新建连接/秒(CPS) 100K-1M 10K-100K L7 需要解析 HTTP
并发连接数 1M-10M 100K-1M L7 每连接内存更大
吞吐量(Gbps) 10-100 1-40 L7 需要应用层处理
每秒请求数(RPS) N/A 50K-500K 短连接场景

3.2 容量估算方法

Step 1: 确定峰值流量
  日均 PV: 1000 万
  峰值倍数: 3x(经验值,对于电商可能 10x-20x)
  峰值 QPS: 10M / 86400 × 3 ≈ 350 QPS

Step 2: 确定连接特征
  平均连接持续时间: 500ms(HTTP/1.1 Keep-Alive)
  峰值并发连接: 350 × 0.5 = 175
  安全系数: 2x
  所需并发连接: 175 × 2 = 350

Step 3: 确定带宽
  平均响应大小: 50KB
  峰值带宽: 350 × 50KB × 8 = 140 Mbps
  安全系数: 1.5x
  所需带宽: 210 Mbps

Step 4: 选择 LB 规格
  350 QPS + 350 并发 + 210 Mbps
  → 单台小规格 LB 即可
  → 但为了高可用,至少需要两台

对于大流量场景:

场景: 大型电商促销
  峰值 QPS: 50,000
  并发连接: 25,000(HTTP/2 连接复用后)
  带宽: 4 Gbps

L4 LB 选型:
  IPVS (DR 模式): 单机可处理 50K+ QPS
  云 NLB: 按需扩展,无需预估

L7 LB 选型:
  Nginx: 单 Worker 约 10K-30K RPS
  需要 2-3 台 Nginx (4 Worker each)
  或 1 台 HAProxy (多线程)

架构:
  DNS → L4 LB (IPVS, 2台主备)
      → L7 LB Pool (Nginx, 4台)
          → App Pool (N 台)

3.3 LB 扩展模式

当单台 LB 容量不足时,有几种扩展方案:

方案 1:DNS 轮询

DNS Round Robin:
  app.example.com → 10.0.1.1 (LB-1)
  app.example.com → 10.0.1.2 (LB-2)
  app.example.com → 10.0.1.3 (LB-3)

优点: 简单,无状态
缺点: 无法感知 LB 健康状态,分配不均

方案 2:ECMP(等价多路径)

路由器:
  目的 IP 10.0.0.100/32
  → ECMP 下一跳:
    10.0.1.1 (LB-1)
    10.0.1.2 (LB-2)
    10.0.1.3 (LB-3)
  哈希: 基于五元组

优点: 对客户端透明,同一连接始终到同一 LB
缺点: 需要路由器支持 ECMP

方案 3:L4→L7 分层

              用户流量
                 │
          ┌──────┴──────┐
          │  L4 LB Pool │  ← IPVS / NLB (高吞吐)
          │  (2-4 台)    │
          └──────┬──────┘
                 │
          ┌──────┴──────┐
          │  L7 LB Pool │  ← Nginx / Envoy (HTTP 路由)
          │  (4-8 台)    │
          └──────┬──────┘
                 │
          ┌──────┴──────┐
          │  App Pool   │
          │  (N 台)     │
          └─────────────┘

L4 LB: 负责连接分发,不解析 HTTP,高吞吐
L7 LB: 负责 HTTP 路由、SSL 终止、灰度,可水平扩展

3.4 云环境的 LB 容量

云 LB 的容量通常有配额限制,需要提前了解:

云厂商 L4 产品 默认并发连接 默认新建连接/秒 默认带宽
AWS NLB 无硬限制 无硬限制 按实例类型
阿里云 CLB 50万 5万 5 Gbps
腾讯云 CLB 10万 5万 按规格
GCP Network LB 无硬限制 无硬限制 按网络层级
# 检查当前 LB 连接使用情况
# AWS NLB
aws cloudwatch get-metric-statistics \
    --namespace AWS/NetworkELB \
    --metric-name ActiveFlowCount \
    --dimensions Name=LoadBalancer,Value=net/my-nlb/1234567890 \
    --start-time "$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%S)" \
    --end-time "$(date -u +%Y-%m-%dT%H:%M:%S)" \
    --period 60 \
    --statistics Maximum

# 阿里云 CLB
aliyun slb DescribeLoadBalancerAttribute \
    --LoadBalancerId lb-xxxxx \
    --RegionId cn-beijing

四、高可用 LB 部署

4.1 单点故障分析

LB 本身是单点故障——如果 LB 挂了,整个服务不可用。消除 LB 单点故障有以下模式:

4.2 主备模式(Active-Passive)

                用户流量
                   │
                   ▼
             VIP: 10.0.0.100
                   │
        ┌──────────┴──────────┐
        │                     │
   ┌────┴─────┐         ┌────┴─────┐
   │  LB-1    │  VRRP   │  LB-2    │
   │ (Master) │ ◄─────► │ (Backup) │
   └────┬─────┘         └──────────┘
        │
   ┌────┴────────────────────┐
   │     Backend Pool        │
   └─────────────────────────┘
# Keepalived 配置 — LB-1 (Master)
# /etc/keepalived/keepalived.conf
global_defs {
    router_id LB_1
    vrrp_skip_check_adv_addr
    vrrp_garp_interval 0
    vrrp_gna_interval 0
}

vrrp_script check_nginx {
    script "/etc/keepalived/check_nginx.sh"
    interval 2
    weight -20        # 检查失败时降低优先级 20
    fall 3            # 连续失败 3 次判定不健康
    rise 2            # 连续成功 2 次判定恢复
}

vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100                  # Master 优先级 100
    advert_int 1                  # VRRP 通告间隔 1 秒
    authentication {
        auth_type PASS
        auth_pass secret123
    }
    virtual_ipaddress {
        10.0.0.100/24 dev eth0    # VIP
    }
    track_script {
        check_nginx
    }
    notify_master "/etc/keepalived/notify.sh MASTER"
    notify_backup "/etc/keepalived/notify.sh BACKUP"
    notify_fault  "/etc/keepalived/notify.sh FAULT"
}
#!/bin/bash
# /etc/keepalived/check_nginx.sh
# 检查 Nginx 是否正常运行

# 检查进程
if ! pgrep -x nginx > /dev/null; then
    exit 1
fi

# 检查端口
if ! ss -tlnp | grep -q ':80 '; then
    exit 1
fi

# 检查 HTTP 响应
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
    --connect-timeout 2 --max-time 5 http://127.0.0.1/health)
if [ "$HTTP_CODE" != "200" ]; then
    exit 1
fi

exit 0
#!/bin/bash
# /etc/keepalived/notify.sh
# VRRP 状态变化通知

STATE=$1
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

case $STATE in
    MASTER)
        echo "$TIMESTAMP: 转为 MASTER,接管 VIP" >> /var/log/keepalived_state.log
        # 发送告警
        curl -X POST "https://alerts.example.com/webhook" \
            -d "{\"msg\": \"LB 主备切换: $(hostname) 成为 MASTER\"}"
        ;;
    BACKUP)
        echo "$TIMESTAMP: 转为 BACKUP,释放 VIP" >> /var/log/keepalived_state.log
        ;;
    FAULT)
        echo "$TIMESTAMP: 进入 FAULT 状态" >> /var/log/keepalived_state.log
        curl -X POST "https://alerts.example.com/webhook" \
            -d "{\"msg\": \"LB 故障: $(hostname) 进入 FAULT 状态\"}"
        ;;
esac

4.3 双活模式(Active-Active)

主备模式的问题:备机在正常时完全空闲,资源浪费。双活模式让两台 LB 同时工作:

                  用户流量
                     │
              ┌──────┴──────┐
              │   DNS/ECMP  │
              └──────┬──────┘
                ╱         ╲
          VIP-1╱           ╲VIP-2
              ╱             ╲
   ┌─────────┴──┐     ┌────┴────────┐
   │    LB-1    │     │    LB-2     │
   │ MASTER(V1) │     │ MASTER(V2)  │
   │ BACKUP(V2) │     │ BACKUP(V1)  │
   └──────┬─────┘     └──────┬──────┘
          │                  │
          └────────┬─────────┘
                   │
            Backend Pool
# LB-1 的 Keepalived 配置(双 VIP)
# VIP-1: LB-1 是 Master
vrrp_instance VI_1 {
    state MASTER
    interface eth0
    virtual_router_id 51
    priority 100
    virtual_ipaddress {
        10.0.0.100/24    # VIP-1
    }
    track_script {
        check_nginx
    }
}

# VIP-2: LB-1 是 Backup
vrrp_instance VI_2 {
    state BACKUP
    interface eth0
    virtual_router_id 52
    priority 90
    virtual_ipaddress {
        10.0.0.101/24    # VIP-2
    }
    track_script {
        check_nginx
    }
}
DNS 配置:
  app.example.com → 10.0.0.100 (VIP-1, LB-1 Master)
  app.example.com → 10.0.0.101 (VIP-2, LB-2 Master)

正常时: 两台 LB 各处理约 50% 流量
故障时: 存活的 LB 接管两个 VIP,处理 100% 流量

4.4 Kubernetes 环境的 LB HA

在 Kubernetes 中,LB 的高可用通常由以下组件协同实现:

# 使用 MetalLB 提供裸金属环境的 LoadBalancer
# metallb-config.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: lb-pool
  namespace: metallb-system
spec:
  addresses:
    - 10.0.0.100-10.0.0.110

---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: lb-l2
  namespace: metallb-system
spec:
  ipAddressPools:
    - lb-pool

---
# Ingress Controller 部署(高可用)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-ingress
  namespace: ingress-nginx
spec:
  replicas: 3    # 多副本
  selector:
    matchLabels:
      app: nginx-ingress
  template:
    metadata:
      labels:
        app: nginx-ingress
    spec:
      # Pod 反亲和:确保分布在不同节点
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values: ["nginx-ingress"]
              topologyKey: kubernetes.io/hostname
      containers:
        - name: nginx-ingress
          image: registry.k8s.io/ingress-nginx/controller:v1.9.0
          resources:
            requests:
              cpu: "500m"
              memory: "256Mi"
            limits:
              cpu: "2000m"
              memory: "1Gi"
          ports:
            - containerPort: 80
            - containerPort: 443
# 验证 Ingress Controller 的分布
kubectl get pods -n ingress-nginx -o wide
# NAME                             READY   NODE
# nginx-ingress-xxx-abc   1/1     node-1
# nginx-ingress-xxx-def   1/1     node-2
# nginx-ingress-xxx-ghi   1/1     node-3

# 检查 Service 的 Endpoints
kubectl get endpoints -n ingress-nginx nginx-ingress
# NAME            ENDPOINTS
# nginx-ingress   10.244.1.5:80,10.244.2.8:80,10.244.3.3:80

五、LB 监控与告警

5.1 核心监控指标

# Prometheus 告警规则 — LB 监控
groups:
  - name: lb_alerts
    rules:
      # LB 后端全部不可用
      - alert: LBNoHealthyBackends
        expr: |
          nginx_upstream_peers{state="up"} == 0
        for: 30s
        labels:
          severity: critical
        annotations:
          summary: "LB upstream {{ $labels.upstream }} 无健康后端"

      # LB 高错误率
      - alert: LBHighErrorRate
        expr: |
          rate(nginx_http_requests_total{status=~"5.."}[5m])
          / rate(nginx_http_requests_total[5m]) > 0.05
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "LB 5xx 错误率超过 5%"

      # LB 连接数接近上限
      - alert: LBHighConnectionCount
        expr: |
          nginx_connections_active / nginx_connections_active_limit > 0.8
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "LB 活跃连接数超过 80%"

      # LB 响应延迟异常
      - alert: LBHighLatency
        expr: |
          histogram_quantile(0.99,
            rate(nginx_http_request_duration_seconds_bucket[5m])
          ) > 2.0
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "LB P99 延迟超过 2 秒"

      # VRRP 主备切换
      - alert: VRRPFailover
        expr: |
          changes(keepalived_vrrp_state[5m]) > 0
        labels:
          severity: critical
        annotations:
          summary: "Keepalived VRRP 发生主备切换"

5.2 运维巡检脚本

#!/bin/bash
# lb_health_report.sh — LB 健康巡检报告

echo "=========================================="
echo "LB 健康巡检报告 - $(date '+%Y-%m-%d %H:%M:%S')"
echo "=========================================="

# 1. Nginx 进程状态
echo -e "\n--- Nginx 进程 ---"
nginx_workers=$(pgrep -c nginx)
echo "Worker 进程数: $nginx_workers"
nginx -t 2>&1 | tail -1

# 2. 连接状态
echo -e "\n--- 连接统计 ---"
curl -s http://127.0.0.1/nginx_status 2>/dev/null || echo "status 模块未启用"

# 3. Upstream 健康状态
echo -e "\n--- Upstream 状态 ---"
curl -s http://127.0.0.1/upstream_status 2>/dev/null | \
    python3 -c "
import sys, json
try:
    data = json.load(sys.stdin)
    for name, servers in data.items():
        up = sum(1 for s in servers if s.get('status') == 'up')
        total = len(servers)
        status = 'OK' if up == total else 'DEGRADED' if up > 0 else 'DOWN'
        print(f'  {name}: {up}/{total} healthy [{status}]')
except:
    print('  无法解析 upstream 状态')
"

# 4. 系统资源
echo -e "\n--- 系统资源 ---"
echo "CPU: $(top -bn1 | grep 'Cpu(s)' | awk '{print $2}')% 使用"
echo "内存: $(free -m | awk '/Mem:/{printf "%d/%dMB (%.1f%%)", $3, $2, $3/$2*100}')"
echo "网络连接: $(ss -s | grep 'TCP:' | awk '{print $2}')"

# 5. Keepalived 状态
echo -e "\n--- VRRP 状态 ---"
if systemctl is-active keepalived > /dev/null 2>&1; then
    ip addr show | grep "10.0.0.100" > /dev/null && \
        echo "VIP 10.0.0.100: 本机持有 (MASTER)" || \
        echo "VIP 10.0.0.100: 非本机 (BACKUP)"
else
    echo "Keepalived 未运行"
fi

# 6. 连接排队
echo -e "\n--- 连接排队 ---"
echo "SYN Backlog 溢出: $(cat /proc/net/netstat | awk '/TcpExt/{getline; print $22}')"
echo "Accept Queue 溢出: $(ss -tlnp | head -5)"

echo -e "\n=========================================="
echo "巡检完成"

六、常见问题与排查

6.1 会话保持不生效

# 症状:配置了 Cookie 会话保持,但用户仍然被分配到不同后端
# 排查步骤:

# 1. 检查 LB 是否设置了 Cookie
curl -v http://app.example.com/ 2>&1 | grep -i "set-cookie"
# 应该看到: Set-Cookie: SERVERID=s1; Path=/; HttpOnly

# 2. 检查客户端是否发送了 Cookie
curl -v -b "SERVERID=s1" http://app.example.com/ 2>&1 | grep -i cookie
# 应该看到: Cookie: SERVERID=s1

# 3. 检查是否有中间层剥离了 Cookie
# CDN、WAF 可能会修改或删除特定 Cookie
curl -H "Host: app.example.com" http://cdn-ip/debug/headers

# 4. 检查 Cookie 的 Path/Domain 是否正确
# Path=/ 才能在所有路径生效
# Domain 需要匹配请求的域名

6.2 灰度流量比例不准

# 症状:配置了 5% 灰度,但实际流量远高于 5%
# 常见原因:

# 1. 哈希分布不均
# split_clients 使用的变量基数太小
# 例如只有 10 个不同的 client IP,5% = 0.5 个 IP → 实际是 0% 或 10%

# 2. DNS 缓存
# 如果用 DNS 做灰度,缓存可能导致比例偏差

# 3. 采样误差
# 流量太小时,统计偏差大
# 1000 个请求中 5% = 50 个,实际可能在 30-70 之间波动

# 验证实际流量比例
tail -10000 /var/log/nginx/access.log | \
    awk '{print $NF}' | sort | uniq -c | sort -rn
# 其中 $NF 是日志中标识版本的字段

6.3 LB 性能瓶颈定位

# 判断瓶颈在 LB 还是后端

# 1. 检查 LB CPU
top -bn1 | head -5
# 如果 CPU > 80%,LB 可能是瓶颈

# 2. 检查 LB 连接数
ss -s
# 如果 established 接近系统限制,需要扩容

# 3. 检查 LB 到后端的延迟
# 在 LB 上执行:
curl -o /dev/null -s -w "connect: %{time_connect}s\nttfb: %{time_starttransfer}s\ntotal: %{time_total}s\n" \
    http://10.0.1.1:8080/health

# 4. 对比 LB 入口延迟和后端延迟
# 如果 LB 入口延迟 >> 后端延迟,说明 LB 是瓶颈
# 检查 Nginx upstream_response_time vs request_time
tail -1000 /var/log/nginx/access.log | \
    awk '{
        req_time = $(NF-1)
        ups_time = $NF
        lb_overhead = req_time - ups_time
        if (lb_overhead > 0.1) print "HIGH LB overhead:", lb_overhead, "s"
    }'

七、总结

  1. 会话保持是过渡方案,Session 外部化才是终局。新项目从一开始就用 Redis/Memcached 存储 Session,避免依赖 LB 的会话保持。遗留系统使用 Cookie 插入(HAProxy)或 Cookie 哈希(Nginx)作为过渡。

  2. 灰度发布的关键是可观测性和自动回滚。5% 灰度本身不难实现,难的是:怎么知道 Canary 有问题?怎么自动回滚?建立错误率和延迟的基线对比,配合自动化回滚脚本。

  3. LB 容量规划要考虑峰值而非平均值。峰值流量可能是平均值的 3-10 倍。L4 LB 的容量瓶颈通常在连接表大小和包转发速率,L7 LB 的瓶颈通常在 CPU(HTTP 解析、TLS 握手)。

  4. LB 自身的高可用比后端更重要。后端挂一台,LB 会自动摘除;LB 挂了,整个服务不可用。至少用 Keepalived + VIP 做主备,大规模场景用 ECMP + LB Pool。

  5. 监控要覆盖 LB 和后端两个层面。LB 的连接数、CPU、错误率、VRRP 状态都需要监控和告警。特别注意 VRRP 频繁切换(脑裂风险)和后端健康状态抖动。


参考文献


上一篇:全局负载均衡:GSLB、DNS 调度与 Anycast

下一篇:Nginx 架构深度剖析:事件模型、Worker 与 Upstream

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2025-08-31 · network

【网络工程】HAProxy 工程:高级配置、ACL 与运维

系统讲解 HAProxy 的工程实践:Frontend/Backend/Listen 配置模型、ACL 规则引擎的高级用法、Runtime API 的动态管理能力、多线程模型与性能调优、SSL 终止与健康检查策略,建立 HAProxy 从配置到生产运维的完整体系。

2025-08-18 · network

【网络工程】L4 负载均衡:IPVS、LVS 与连接级调度

系统讲解 L4 负载均衡的内核实现:IPVS 的工作原理与三种转发模式(NAT/DR/TUN)、调度算法选择、LVS 高可用方案(Keepalived + VIP)、云环境中的 L4 LB(NLB/MetalLB),建立传输层负载均衡的工程能力。

2025-08-20 · network

【网络工程】负载均衡算法深度解析:从轮询到 P2C

系统讲解负载均衡算法的数学原理与工程实现:Round Robin 及加权变体、Least Connection 及其局限、一致性哈希在 LB 中的应用、P2C(Power of Two Choices)的概率优势,以及真实负载下的算法性能对比与选型。


By .