流量管理基础:服务网格、反向代理、负载均衡器在灰度发布中的角色

聊到灰度发布,大家首先想到的往往是“怎么把流量切过去”。这里涉及三个关键角色:负载均衡器、反向代理和服务网格。它们都能操控流量,但各自的分工、粒度、控制面差异很大。我个人习惯把这三者看作“流量管道的不同阀门”——有的管入口,有的管内部,有的管单个请求的精细化路由。

一、三个组件的基本画像

先给一个快速对比,帮你建立直觉。

组件典型产品工作层级灰度控制粒度配置更新方式
负载均衡器NLB / ELB / F5L4(传输层)IP+端口级DNS 或 API 更新
反向代理Nginx / HAProxy / EnvoyL7(应用层)路径、Header、Cookie 级reload 或动态 API
服务网格Istio / Linkerd / Consul ConnectL7(sidecar 代理)请求标签、权重、匹配规则控制面下发,无需重启

一句话判断用哪种:如果你只需要按地域或者实例分组切流量,负载均衡器就够了;如果你需要按用户 ID、浏览器版本、Cookie 做分流,反向代理是首选;如果你在 Kubernetes 里跑微服务,需要细粒度熔断、重试、灰度,服务网格才是正解。

二、负载均衡器:最朴素的入口分流

我记得 early days 做灰度,最粗暴的方式就是“新版本部署到另一组机器,改 DNS 权重”。说白了,就是靠负载均衡器做 IP+端口级别的流量分发。这种方式现在依然在用,尤其是外网入口层。

灰度发布场景下,负载均衡器能做两件事:

  • 权重路由:给旧版本池分配 90% 权重,新版本池分配 10% 权重。调整权重即可逐步放量。
  • 实例摘除/加入:通过健康检查或 API 动态控制后端池的成员。

但要注意,它看不到 HTTP 请求里面的内容。比如你想“只把 Chrome 用户的请求引到新版本”,负载均衡器做不到。它只能做到“10% 的流量去新池”,至于是哪个用户,它不关心。

踩过一个坑:我曾经在生产环境仅靠负载均衡器做灰度,结果新版本池里有一个实例的端口健康检查延迟很高,但负载均衡器还没来得及摘掉它。10% 的流量中有大概 2% 打到了那个半死不活的实例上,导致部分用户超时。后来我在负载均衡器下游又加了反向代理做二次健康探测才解决。

三、反向代理:HTTP 层面的精细控流

反向代理工作在 L7,意味着它能读懂 HTTP 方法、路径、Header、Cookie,甚至 Body。这就让灰度发布有了“按用户特征分流”的能力。

我常用的 Nginx 灰度配置大致长这样:

upstream backend_v1 {
    server 10.0.1.10:8080 weight=90;
}
upstream backend_v2 {
    server 10.0.1.20:8080 weight=10;
}

server {
    listen 443 ssl;
    location /api/ {
        # 按 Cookie 中的 canary 字段路由
        if ($http_cookie ~* "canary=enable") {
            proxy_pass http://backend_v2;
            break;
        }
        proxy_pass http://backend_v1;
    }
}

你看,这个例子实现了:普通用户走 v1,带有 canary=enable Cookie 的用户走 v2。不需要改 DNS,不需要重启后端服务,只需 reload Nginx 就能生效。

不过 reload 本身有坑:Nginx reload 会 fork 新的 worker 进程,如果旧 worker 还在处理长连接,会有一小段双进程并存的时间。我建议用 nginx -s reload 而不是直接 restart,可以做到连接不中断。

反向代理灰度最适合的场景:

  • 按用户特征(地域、设备、Cookie、Header)分流
  • API 网关处的统一灰度入口
  • 对已有基础设施改动最小——直接在代理层加几行规则就行

四、服务网格:Sidecar 级别的流量魔法

服务网格把反向代理的能力下沉到了每个 Pod 旁边——通过 sidecar 代理(通常是 Envoy)接管所有进出流量。控制面(比如 Istio 的 Pilot)统一下发规则,sidecar 本地执行。这样做的好处是:不需要修改应用代码,不需要配置中心,不需要 reload 代理。

以 Istio 为例,一个典型的灰度路由规则:

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: my-svc
spec:
  hosts:
  - my-svc
  http:
  - match:
    - headers:
        canary:
          exact: "true"
    route:
    - destination:
        host: my-svc
        subset: v2
  - route:
    - destination:
        host: my-svc
        subset: v1
      weight: 90
    - destination:
        host: my-svc
        subset: v2
      weight: 10

这样做的好处很直接:

  • 无中断更新:规则推送到 sidecar,Envoy 热加载,不需要 reload。
  • 流量镜像:可以把一部分请求复制一份到新版本,而不影响线上用户——用于“暗部署”。
  • 条件组合:可以基于 Header、Cookie、源服务、URI 等多个维度组合匹配。

但服务网格不是银弹。你想想看,它引入了额外的网络跳转(sidecar 到业务容器再回 sidecar),延迟增加了 1~3ms。另外,控制面本身也是一个新组件,如果 Pilot 挂了,sidecar 会沿用最后有效的规则,但无法更新。我在项目里遇到过控制面内存泄漏导致规则下发延迟,新版本的灰度流量一直上不去——查了半天才发现是 Pilot OOM 了。

我的建议:如果你团队规模较小(10 人以下),或者服务数量不超过 20 个,直接用反向代理做灰度,运维成本低很多。服务网格适合服务数量超过 50、需要频繁灰度发布的团队——它的自动化优势才能体现出来。

五、三者在灰度流程中的协作关系

实际生产中很少只用一种。通常会组合使用:负载均衡器做全局入口分流,反向代理做应用层灰度策略,服务网格做服务间请求的精细路由。下面这张图展示了它们的分层协作。

灰度发布流量分层协作图 外部客户端请求 负载均衡器(L4:按地域/实例权重分发) 反向代理(L7:Header/Cookie 分流) 反向代理(L7:Header/Cookie 分流) 服务网格(Sidecar 代理:条件路由 / 权重分流 / 流量镜像) 服务 v1(稳定版本,权重 90%) 服务 v2(灰度版本,权重 10%) 💡 分层职责:负载均衡器管入口 → 反向代理管应用层策略 → 服务网格管家内精细路由 各层可独立配置,互不依赖;复杂度从下往上递增,灵活性也递增。

这张图展示了典型的“三层灰度架构”。注意看:负载均衡器做第一道筛子,把请求按地域或实例权重分发到不同的入口网关;入口网关(反向代理)根据用户特征做第二道分流;服务网格在集群内部做第三道细粒度路由。每一层都可以独立调整灰度参数,互不干扰。

六、避坑与选型建议

避坑一:我曾经在同一个集群里同时用 Nginx 和 Istio 做灰度策略,结果两个层的规则冲突了——Nginx 把“canary=enable”的流量引到了 v2,但 Istio 的 VirtualService 又把 v2 的 10% 权重分回了 v1。最后线上用户收到了不一致的结果。记住:不要在多个层重复做同维度的灰度路由。要么全靠反向代理,要么全靠服务网格。

避坑二:负载均衡器做灰度时,健康检查超时时间要设得比业务超时短。我之前遇到过健康检查设为 30 秒,但业务的慢请求会等 60 秒,导致负载均衡器认为实例健康,实际上用户已经在超时边缘。把健康检查超时改为 5 秒,阈值 2 次,才解决问题。

选型速查:

  • 2~10 个服务,快速灰度:反向代理(Nginx / HAProxy)就够了
  • 10~50 个服务,多维度分流:反向代理 + 简单的内部流量管理脚本
  • 50+ 个服务,频繁灰度:上服务网格(Istio / Linkerd),用控制面统一管理

流量管理这件事,说白了就是找到“改动最小、影响可控”的切入点。我个人习惯从负载均衡器开始——先确保入口层的灰度能跑通,再考虑要不要加反向代理,最后看是否值得引入服务网格。每一步都问自己一句:“这个复杂度,我是真的需要吗?” 答案清楚了,方案也就清晰了。