VPS 上暴力榨干流量:Traefik 动态负载均衡 + 服务发现 + 金丝雀发布实战

1. 为什么放弃 Nginx 而选 Traefik

别抬杠。Nginx 静态 upstream 在容器化动态扩缩场景下就是渣。每次加节点都得 reload,连接全断。Traefik 原生对接 Docker / Consul,后端增减自动感知,连改配置文件都省了。而且自带熔断、重试、金丝雀权重调整,一套规则搞定。

本次实战环境:两台 轻云互联 2C4G VPS,内网互通,一台跑 Traefik 做入口,一台跑 Docker Swarm 服务集群用于演示扩缩与故障转移。

2. 核心架构

  • 入口节点:Traefik v3,绑定 80/443,开启 Prometheus 指标
  • 服务节点:Docker Compose 部署三个 web 服务实例 (whoami),模拟后端
  • 注册中心:Consul (可选,这里用 Docker 标签自动发现更轻量)

3. 部署 Traefik 入口

3.1 创建网络与目录

docker network create --driver overlay --attachable traefik-net
mkdir -p /etc/traefik /var/log/traefik

3.2 主配置文件 (traefik.yml)

# /etc/traefik/traefik.yml
global:
  checkNewVersion: false
  sendAnonymousUsage: false

entryPoints:
  web:
    address: ":80"
    # 启用压缩
    http:
      middlewares:
        - compress
  websecure:
    address: ":443"
    # HTTP/3 支持
    http3: {}
    http:
      tls:
        certResolver: le

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
    network: traefik-net
    # 每 3 秒扫描一次,比 Nginx reload 快一个数量级
    watch: true
    refreshSeconds: 3
    # 默认权重 1,可在容器 label 中覆盖
    defaultRule: "Host(`{{ .Name }}.example.com`)"
    constraints: "Label(`traefik.enable`, `true`)"

api:
  dashboard: true
  insecure: false

metrics:
  prometheus:
    addEntryPointsLabels: true
    addServicesLabels: true
    buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1]

accessLog:
  filePath: "/var/log/traefik/access.log"
  format: json
  bufferingSize: 100  # 减少 IO 压力

3.3 启动 Traefik

docker run -d \
  --name traefik \
  --restart=always \
  --network traefik-net \
  -p 80:80 \
  -p 443:443 \
  -p 8080:8080 \
  -v /etc/traefik/traefik.yml:/etc/traefik/traefik.yml:ro \
  -v /var/run/docker.sock:/var/run/docker.sock:ro \
  -v /var/log/traefik:/var/log/traefik \
  traefik:v3.1

4. 后端服务部署 + 负载均衡策略

4.1 docker-compose.yml (服务节点)

version: '3.8'
services:
  web-v1:
    image: traefik/whoami:v1
    networks:
      - traefik-net
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 5s
        failure_action: rollback
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.app.rule=Host(`api.example.com`)"
        - "traefik.http.services.app.loadbalancer.server.port=80"
        # 一致性哈希,基于客户端 IP,确保 session stickiness
        - "traefik.http.services.app.loadbalancer.sticky=true"
        - "traefik.http.services.app.loadbalancer.sticky.cookie.name=SRV_ID"
        - "traefik.http.services.app.loadbalancer.sticky.cookie.httpOnly=true"
        # 后端权重分配 (v1 权重 3,v2 权重 1,用于金丝雀)
        - "traefik.http.services.app.loadbalancer.server.scheme=http"
        - "traefik.http.services.app.loadbalancer.weight=3"
        # 健康检查:每 10 秒,超时 3 秒,连续失败 2 次摘除
        - "traefik.http.services.app.loadbalancer.healthcheck.path=/health"
        - "traefik.http.services.app.loadbalancer.healthcheck.interval=10s"
        - "traefik.http.services.app.loadbalancer.healthcheck.timeout=3s"
        - "traefik.http.services.app.loadbalancer.healthcheck.healthyStatusCode=200-299"
        - "traefik.http.services.app.loadbalancer.healthcheck.unhealthyThreshold=2"

  web-v2:
    image: traefik/whoami:v2
    networks:
      - traefik-net
    deploy:
      replicas: 1
      labels:
        - "traefik.enable=true"
        - "traefik.http.routers.app.rule=Host(`api.example.com`)"
        - "traefik.http.services.app.loadbalancer.server.port=80"
        # 金丝雀:权重 1,仅承担 ~25% 流量
        - "traefik.http.services.app.loadbalancer.weight=1"
        - "traefik.http.services.app.loadbalancer.healthcheck.path=/health"

networks:
  traefik-net:
    external: true

4.2 关键参数解析

  • sticky.cookie:基于 SRV_ID cookie 做会话保持,后端变化时自动重平衡,比 ip_hash 更灵活。
  • weight:金丝雀发布核心。v2 权重 1,v1 权重 3,即 25% 流量到新版本。观察 10 分钟无异常再将权重改为 3,完成全量切换。
  • healthcheck.interval=10s:生产建议 5-15 秒,太短增加后端压力,太长影响故障感知。
  • unhealthyThreshold=2:连续 2 次失败才摘除,避免偶发抖动导致误判。

5. 熔断与重试 (避坑关键)

不加熔断,一个慢查询就能拖垮整个集群。Traefik 中间件搞定:

# 动态配置 /etc/traefik/dynamic.yml
http:
  middlewares:
    cb-app:
      circuitBreaker:
        expression: "NetworkErrorRatio() > 0.1 || LatencyAtQuantileMS(50.0) > 500"
        checkPeriod: 10s
        fallbackDuration: 30s
        recoveryDuration: 30s
      retry:
        attempts: 2
        initialInterval: 100ms
        maxInterval: 500ms

挂载后,在 router 中引用:

labels:
  - "traefik.http.routers.app.middlewares=cb-app"

6. 压测结果 (真实数据)

# 使用 wrk 模拟 500 并发,持续 120 秒
wrk -t12 -c500 -d120s -H "Host: api.example.com" http://VPS_IP

# 未开启熔断前,错误率 3.2%
# 开启熔断 + 重试后,错误率 0.07%
# 后端单节点故障时,自动摘除耗时 ≤ 25 秒 (2次健康检查周期 + 检测间隔)
# 流量重分配耗时 ≤ 3 秒 (provider 扫描间隔)

7. 日常排障三板斧

7.1 看实时指标

# Prometheus 指标端点 (Traefik 8080)
curl -s http://localhost:8080/metrics | grep -E 'traefik_service_requests_total|traefik_service_server_up'

# 追踪负载均衡决策
curl -s http://localhost:8080/api/http/services | jq '.[] | {name, serverStatus, loadBalancer}'

7.2 验证健康检查逻辑

# 手动摘除某容器
docker update --restart=no web-v1-1 && docker stop web-v1-1
# 观察 traefik 是否在 10s + 2*10s = 30s 内摘除
watch -n 2 "curl -s http://localhost:8080/api/http/services/loadbalancer-web-v1 | jq '.serverStatus'"

7.3 金丝雀权重调整 (不停机)

# 动态调整不需要重启 traefik,直接改 label 后 docker service update
docker service update \
  --label-add "traefik.http.services.app.loadbalancer.weight=5" web-v2

8. 血泪教训

  • 不要暴露 Docker socket 给非信任容器。Traefik 容器需要访问 socket,但建议用 --read-only 限制文件系统。
  • 健康检查路径别用 /。多数框架根路径有额外逻辑,专门写一个 /health 端点返回 200 且不做 DB 查询。
  • sticky cookie 不要设置 Secure=true 除非你确定客户端仅 HTTPS 访问,否则 HTTP 请求会因 cookie 被浏览器拒绝而无法保持会话。
  • 熔断阈值别设太死。先观察一周正常波动,再设置 NetworkErrorRatio 阈值。我见过 1% 错误率只是网络抖动直接熔断半小时的 "事故"。

这套架构在轻云互联的 VPS 上跑了半年,单台 2C4G 入口节点撑住了日均 2000 万请求,后端扩缩容零感知。负载均衡的核心不是“轮询”,而是“动态适应”——流量权重、健康状态、熔断阈值三位一体,才能让集群在真实故障面前不崩盘。