前面五篇文章从 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 → 选择后端
问题:
- NAT 环境下失效。同一个办公室的 100 个用户共享一个出口 IP,所有请求都被路由到同一台后端——负载极不均匀。
- 移动网络 IP 变化。手机从 WiFi 切到 4G,IP 变化,会话丢失。
- 后端增减导致大量重映射。新增一台后端时,
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;
1.3 Cookie 会话保持
更精确的方案——通过 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 | 无 | 高(改代码) |
决策建议:
- 新项目:直接用 Session 外部化(Redis/Memcached),不做会话保持
- 遗留系统:LB 插入 Cookie 作为过渡,逐步迁移到外部 Session
- 仅内部服务:一致性哈希 即可,不需要 Cookie
- WebSocket:连接级绑定,LB 层保持长连接不中断
二、灰度发布
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: stable2.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 状态\"}"
;;
esac4.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"
}'七、总结
会话保持是过渡方案,Session 外部化才是终局。新项目从一开始就用 Redis/Memcached 存储 Session,避免依赖 LB 的会话保持。遗留系统使用 Cookie 插入(HAProxy)或 Cookie 哈希(Nginx)作为过渡。
灰度发布的关键是可观测性和自动回滚。5% 灰度本身不难实现,难的是:怎么知道 Canary 有问题?怎么自动回滚?建立错误率和延迟的基线对比,配合自动化回滚脚本。
LB 容量规划要考虑峰值而非平均值。峰值流量可能是平均值的 3-10 倍。L4 LB 的容量瓶颈通常在连接表大小和包转发速率,L7 LB 的瓶颈通常在 CPU(HTTP 解析、TLS 握手)。
LB 自身的高可用比后端更重要。后端挂一台,LB 会自动摘除;LB 挂了,整个服务不可用。至少用 Keepalived + VIP 做主备,大规模场景用 ECMP + LB Pool。
监控要覆盖 LB 和后端两个层面。LB 的连接数、CPU、错误率、VRRP 状态都需要监控和告警。特别注意 VRRP 频繁切换(脑裂风险)和后端健康状态抖动。
参考文献
- HAProxy Documentation: Cookie Persistence (www.haproxy.com/documentation)
- Nginx Documentation: Upstream Module (nginx.org/en/docs/http/ngx_http_upstream_module.html)
- Envoy Documentation: Route Configuration (www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3)
- Keepalived Documentation (keepalived.readthedocs.io)
- Martin Fowler: Canary Release (martinfowler.com/bliki/CanaryRelease.html)
上一篇:全局负载均衡:GSLB、DNS 调度与 Anycast
下一篇:Nginx 架构深度剖析:事件模型、Worker 与 Upstream
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【网络工程】HAProxy 工程:高级配置、ACL 与运维
系统讲解 HAProxy 的工程实践:Frontend/Backend/Listen 配置模型、ACL 规则引擎的高级用法、Runtime API 的动态管理能力、多线程模型与性能调优、SSL 终止与健康检查策略,建立 HAProxy 从配置到生产运维的完整体系。
【网络工程】L4 负载均衡:IPVS、LVS 与连接级调度
系统讲解 L4 负载均衡的内核实现:IPVS 的工作原理与三种转发模式(NAT/DR/TUN)、调度算法选择、LVS 高可用方案(Keepalived + VIP)、云环境中的 L4 LB(NLB/MetalLB),建立传输层负载均衡的工程能力。
【网络工程】L7 负载均衡:HTTP 感知路由与内容交换
深入讲解 L7 负载均衡的工程实现:HTTP 请求解析与路由决策、基于 Host/Path/Header 的高级路由规则、SSL Termination 的架构位置、L4+L7 混合架构设计,以及 L7 LB 的性能优化与故障排查。
【网络工程】负载均衡算法深度解析:从轮询到 P2C
系统讲解负载均衡算法的数学原理与工程实现:Round Robin 及加权变体、Least Connection 及其局限、一致性哈希在 LB 中的应用、P2C(Power of Two Choices)的概率优势,以及真实负载下的算法性能对比与选型。