概述

凌晨 2 点,某出行平台的支付服务做蓝绿切换。运维执行 kubectl rollout 切换 Service selector,流量从蓝色环境切到绿色环境。看起来一切正常——Pod 就绪、健康检查通过、响应延迟正常。

3 分钟后,DBA 群炸了。数据库连接数从 200 飙到 1200,连接池被打满,下游 5 个服务全部超时。原因是绿色环境的应用版本更新了 ORM 框架,连接池默认配置从 20 改成了 100,而蓝绿两套环境同时存在时,总连接数直接翻了一倍。

蓝绿部署和金丝雀发布,概念上很简单——准备两套环境切流量,或者先放一小部分流量验证。但生产环境落地的坑远比"切个流量"复杂得多。数据库 schema 怎么兼容?两套环境并存时资源怎么算?流量切换的粒度怎么控制?回滚怎么自动化?

这篇文章拆解蓝绿与金丝雀发布在生产环境落地的 6 个关键决策,每个决策都来自实际项目中的踩坑或权衡。涉及 K8s 原生 Deployment 的局限性、Argo Rollouts 的渐进式交付架构、Istio 流量治理、数据库迁移兼容策略、自动回滚门禁设计等内容。配置示例全部可复制运行。

决策一:为什么 K8s 原生 Deployment 不够用

滚动更新的三个硬伤

Kubernetes 原生 Deployment 用 RollingUpdate 策略做发布,原理是逐步用新 Pod 替换旧 Pod。对很多场景够用,但生产环境有几个硬伤:

1. 无法控制流量比例。 RollingUpdate 按 Pod 数量做替换,不是按流量比例。假设 10 个副本,maxSurge=1,maxUnavailable=0,那发布过程中新旧 Pod 比例是 1:9、2:8…10:0。但你没法说"只给新版本 5% 的流量"——所有请求随机打到新 Pod,5% 流量意味着 1 个新 Pod 承接 10 个副本中随机的请求。这对支付类业务是致命的:你不知道哪个用户的请求会打到新版本。

2. 就绪探针无法做深度检查。 Readiness Probe 只能做 HTTP/TCP 健康检查,没法查询外部指标。比如"新版本的错误率是否低于 0.1%"、“P99 延迟是否在 200ms 以内”——这些需要从 Prometheus 查询,原生 Deployment 做不到。

3. 无法自动回滚。 RollingUpdate 能暂停发布(kubectl rollout pause),但没法根据指标自动判断"这个版本有问题,自动回滚"。需要人盯着监控手动决策,凌晨发布的场景下,人的反应速度是个大问题。

这三个问题在 50+ 微服务的环境中尤其突出。曾在某新能源物流平台做过 120+ 微服务的 K8s 迁移(相关文章:120 个微服务零故障迁 K8s:3 个月实战迁移中没人讲的 6 个坑),迁移期间最大的教训就是:核心支付链路绝对不能直接用 RollingUpdate 发布,必须上渐进式交付。

原生方案什么时候够用

不是所有服务都需要蓝绿或金丝雀。我推荐的分界线:

服务特征推荐策略理由
无状态、非核心、可快速回滚RollingUpdate成本最低,K8s 原生支持
有状态、有 DB schema 变更蓝绿部署需要 DB 兼容窗口
核心链路、高 QPS、高敏感金丝雀发布爆炸半径最小
新功能验证、A/B 测试金丝雀 + 流量镜像用真实流量验证但不影响用户

别一上来就给所有服务上金丝雀。一个内部管理后台用 RollingUpdate 就够了,给它配 Argo Rollouts + Istio 流量切分纯属浪费。架构决策要看 ROI。

决策二:蓝绿部署的 K8s 实现与三个陷阱

基本架构

蓝绿部署的核心:同时维护两套完整环境,当前生产环境为 Blue,新版本部署到 Green,验证通过后一次性切流量。

在 K8s 中实现蓝绿,有两种方式:

方式 A:Service Selector 切换(原生)

# Blue 环境(当前生产)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-blue
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
      version: blue
  template:
    metadata:
      labels:
        app: myapp
        version: blue
    spec:
      containers:
      - name: myapp
        image: myapp:v1
        ports:
        - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
---
# Green 环境(新版本)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-green
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
      version: green
  template:
    metadata:
      labels:
        app: myapp
        version: green
    spec:
      containers:
      - name: myapp
        image: myapp:v2
        ports:
        - containerPort: 8080
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
---
# Service 指向 Blue
apiVersion: v1
kind: Service
metadata:
  name: myapp-svc
spec:
  selector:
    app: myapp
    version: blue   # 切换到 green 即完成蓝绿切换
  ports:
  - port: 80
    targetPort: 8080

切换操作就是改 Service 的 selector label:kubectl edit svc myapp-svc 把 version: blue 改成 version: green。回滚就是改回来。

简单。但这个方案有三个陷阱。

陷阱一:双倍资源占用

蓝绿部署期间两套环境同时运行,资源消耗翻倍。3 个副本的服务,发布期间变成 6 个副本。50 个服务的集群,同时发布 10 个就是 30 个额外 Pod。

实际踩过这个坑:某次大促前做全量蓝绿发布,10 个核心服务同时切换,集群 CPU 配额直接超限,Pod 进入 Pending 状态。最后只能分批发布,一次最多 3 个服务做蓝绿。

资源规划建议:蓝绿环境按集群总容量的 30% 预留。比如 100 核 CPU 的集群,蓝绿发布占用不能超过 30 核。超过就得用金丝雀——金丝雀的增量资源可以精确控制到 1% 流量对应的 Pod 数。

陷阱二:数据库连接池翻倍

就是开头那个场景。蓝绿两套环境同时运行时,如果应用各自维护数据库连接池,总连接数 = 蓝色连接数 + 绿色连接数。

这不是"把连接池配小一点"能解决的。连接池太小会导致请求排队,太大会打满数据库。正确的做法是:

  1. 数据库侧设硬上限:在 PostgreSQL 用 max_connections,在 MySQL 用 max_connections,设一个比应用侧连接池总和略大的值。比如单服务连接池 20,最大同时蓝绿 3 个服务,那 max_connections 至少设 200(留 buffer 给其他客户端)
  2. 应用侧连接池按副本共享:用 PgBouncer 或 ProxySQL 做连接池中间件,应用侧连中间件而不是直连数据库。这样蓝绿切换时连接数不会翻倍,中间件统一管理
  3. 发布前检查连接池配置:CI 流水线里加一步,对比新旧版本的连接池配置是否变化。如果变了,触发告警
# CI 流水线中的连接池配置检查脚本(简化版)
import yaml
import sys

def check_pool_config(old_yaml, new_yaml):
    old = yaml.safe_load(old_yaml)
    new = yaml.safe_load(new_yaml)
    old_pool = old.get('spring', {}).get('datasource', {}).get('hikari', {}).get('maximum-pool-size', 'N/A')
    new_pool = new.get('spring', {}).get('datasource', {}).get('hikari', {}).get('maximum-pool-size', 'N/A')
    if old_pool != new_pool:
        print(f"WARNING: DB pool size changed from {old_pool} to {new_pool}")
        print(f"Blue-green deployment may cause connection overflow")
        sys.exit(1)
    print("OK: DB pool size unchanged")

陷阱三:Session 和缓存不一致

蓝绿切换后,绿色环境的缓存是冷的。如果用本地缓存(如 Caffeine、Guava Cache),新环境需要重新加载所有热点数据,可能导致缓存击穿。

解决方案:

  • 用 Redis 做分布式缓存:蓝绿环境共享 Redis,切换时缓存不丢失。代价是增加一次网络 RTT,但比缓存击穿划算
  • 预热:绿色环境启动后、切流量前,跑一轮预热脚本把热点 key 加载到本地缓存
  • Session 外置:用 Spring Session + Redis 或 JWT,不依赖本地 Session。蓝绿切换时用户不需要重新登录

决策三:金丝雀发布与流量控制的三个层次

金丝雀的本质

金丝雀发布的核心不是"放一小部分流量",而是"用真实流量做验证"。放 1% 流量到新版本,观察 5 分钟,没问题就放到 5%,再观察,直到 100%。

和蓝绿的关键区别:蓝绿是 0→100% 一次性切换,金丝雀是渐进式放量。蓝绿的爆炸半径是"要么没事要么全炸",金丝雀的爆炸半径是"只有 1% 的用户受影响"。

三个层次的流量控制

层次一:Pod 副本比例(无流量治理)

最简单的金丝雀:新版本 1 个 Pod,旧版本 9 个 Pod。流量随机分布,约 10% 打到新版本。

问题是:你没法精确控制流量比例。10 个 Pod 中 1 个新版本,流量比例取决于负载均衡算法,不一定是精确 10%。而且新旧 Pod 资源配置不同时,流量分布更不可控。

层次二:Ingress 注解(Nginx Ingress Canary)

如果用 ingress-nginx,可以用 Canary 注解做流量切分:

# 稳定版 Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-stable
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp-stable-svc
            port:
              number: 80
---
# 金丝雀 Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-canary
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"   # 10% 流量到金丝雀
spec:
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp-canary-svc
            port:
              number: 80

改 canary-weight 从 10 到 25 到 50 到 100,就完成了渐进式发布。

这个方案的优点是零额外组件,ingress-nginx 原生支持。缺点是:

  • 只能按权重切分,不能按 header/cookie 路由(实际上 ingress-nginx 支持 canary-by-header 但功能有限)
  • 金丝雀版本和稳定版本的 Ingress 共存,管理复杂度上升
  • 没有自动回滚能力,需要人盯着

相关文章:Kubernetes Ingress 控制器选型与配置 中有 Ingress 控制器的选型对比,这里不展开。

层次三:Service Mesh + Argo Rollouts(生产级)

这是我推荐的生产级方案。Argo Rollouts 是专门为渐进式交付设计的 K8s 控制器,支持蓝绿和金丝雀两种策略,并且能和 Istio、Nginx、ALB 等流量治理组件集成,还能从 Prometheus 查询指标做自动判断。

核心架构:

┌──────────────────────────────────────────────────────┐
│                    Argo Rollouts Controller            │
│  (监听 Rollout CRD 变化,协调 ReplicaSet 和流量路由)     │
└──────────────┬───────────────────────┬───────────────┘
               │                       │
               ▼                       ▼
    ┌──────────────┐        ┌──────────────┐
    │  Stable RS   │        │  Canary RS   │
    │  (v1, 90%)   │        │  (v2, 10%)   │
    └──────┬───────┘        └──────┬───────┘
           │                       │
           ▼                       ▼
    ┌──────────────┐        ┌──────────────┐
    │ stable-svc   │        │ canary-svc   │
    └──────┬───────┘        └──────┬───────┘
           │                       │
           ▼                       ▼
    ┌────────────────────────────────────┐
    │       Istio VirtualService          │
    │  (按 weight 分配流量到两个 Service)    │
    └────────────────────────────────────┘

决策四:Argo Rollouts 金丝雀配置实战

安装 Argo Rollouts

# 创建命名空间
kubectl create namespace argo-rollouts

# 安装控制器
kubectl apply -n argo-rollouts -f \
  https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml

# 安装 kubectl 插件
# 方式一:直接下载二进制
curl -sSL -o /usr/local/bin/kubectl-argo-rollouts \
  https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
chmod +x /usr/local/bin/kubectl-argo-rollouts

# 方式二:用 Krew
kubectl krew install argo-rollouts

金丝雀 Rollout 配置(Istio 流量路由)

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: myapp-rollout
spec:
  replicas: 10
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:v2
        ports:
        - name: http
          containerPort: 8080
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 512Mi
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
          failureThreshold: 3
  strategy:
    canary:
      canaryService: myapp-canary      # 金丝雀版本 Service
      stableService: myapp-stable      # 稳定版本 Service
      trafficRouting:
        istio:
          virtualService:
            name: myapp-vsvc            # VirtualService 名称
            routes:
            - primary                   # 路由名称
      steps:
      - setWeight: 5                    # 5% 流量到金丝雀
      - pause: { duration: 5m }        # 等 5 分钟观察
      - analysis:                       # 自动分析指标
          templates:
          - templateName: success-rate  # 引用 AnalysisTemplate
      - setWeight: 20                   # 20% 流量
      - pause: { duration: 5m }
      - setWeight: 50                   # 50% 流量
      - pause: { duration: 10m }
      - analysis:
          templates:
          - templateName: success-rate
      - setWeight: 100                  # 全量切换

配套的 Istio VirtualService:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: myapp-vsvc
spec:
  gateways:
  - myapp-gateway
  http:
  - name: primary          # 必须和 Rollout 中的 routes 名一致
    route:
    - destination:
        host: myapp-stable
        port:
          number: 80
      weight: 100          # 初始 100% 到稳定版
    - destination:
        host: myapp-canary
        port:
          number: 80
      weight: 0             # 初始 0% 到金丝雀

两个 Service:

apiVersion: v1
kind: Service
metadata:
  name: myapp-stable
spec:
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: myapp-canary
spec:
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 8080

Argo Rollouts 控制器会自动更新这两个 Service 的 selector 指向对应版本的 ReplicaSet,同时修改 VirtualService 的 weight 实现精确流量切分。

AnalysisTemplate:自动指标分析

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  args:
  - name: service-name
  metrics:
  - name: success-rate
    interval: 30s
    count: 5                    # 采样 5 次
    successCondition: result[0] >= 0.99
    failureLimit: 2             # 允许失败 2 次,超过则中止
    provider:
      prometheus:
        address: http://prometheus-server:9090
        query: |
          sum(rate(http_requests_total{service="{{args.service-name}}",code!~"5.."}[2m]))
          /
          sum(rate(http_requests_total{service="{{args.service-name}}"}[2m]))          
  - name: p99-latency
    interval: 30s
    count: 5
    successCondition: result[0] < 200
    failureLimit: 2
    provider:
      prometheus:
        address: http://prometheus-server:9090
        query: |
          histogram_quantile(0.99,
            sum(rate(http_request_duration_ms_bucket{service="{{args.service-name}}"}[2m])) by (le))          

这个 AnalysisTemplate 做两件事:

  1. 查询 Prometheus 的 HTTP 请求成功率,要求 ≥ 99%
  2. 查询 P99 延迟,要求 < 200ms

每 30 秒采样一次,共采样 5 次(2.5 分钟)。如果失败次数超过 2 次,Argo Rollouts 自动中止发布并回滚。

这里有个踩坑细节:count: 5 和 failureLimit: 2 的组合要慎重。如果 count 太小,偶发抖动就会触发回滚。实测推荐 count: 10, failureLimit: 3——给 30% 的容错率,大多数抖动能被过滤掉。

发布操作

# 触发新版本发布(修改 image)
kubectl argo rollouts set image myapp-rollout myapp=myapp:v2

# 实时查看发布状态
kubectl argo rollouts get rollout myapp-rollout --watch

# 手动 promote(跳过当前 pause)
kubectl argo rollouts promote myapp-rollout

# 手动中止并回滚
kubectl argo rollouts abort myapp-rollout

# 查看发布历史
kubectl argo rollouts history myapp-rollout

性能对比数据

在某出行项目的核心 API 服务上做了实测对比:

发布策略发布耗时回滚耗时爆炸半径资源增量适用场景
RollingUpdate3-5min3-5min不可控(10%-100%)10%非核心服务
蓝绿(Service切换)1-2min<30s0或100%100%低风险版本
金丝雀(Nginx Ingress)15-25min1-2min可控(1%-100%)按比例中等核心服务
金丝雀(Argo+Istio)20-35min<30s精确(1%-100%)按比例核心链路

金丝雀 + Argo Rollouts 的发布耗时最长(20-35 分钟),但这正是它安全的来源——每一步都留了观察窗口。回滚耗时不到 30 秒,因为只需要把 VirtualService 的 weight 改回去,不需要重建 Pod。

决策五:数据库迁移的兼容策略

这是蓝绿和金丝雀发布中最难的部分,也是大多数文章一笔带过的。两套环境同时运行时,数据库 schema 怎么处理?

问题本质

蓝绿部署期间,蓝色 v1 和绿色 v2 同时运行。如果 v2 需要修改数据库 schema(加列、改列类型、加索引),怎么办?

  • v2 的代码读 v1 的 schema:可能报错(新代码期望新列不存在)
  • v1 的代码读 v2 的 schema:可能报错(旧代码不认识新列)
  • v2 写入的数据 v1 读不到:业务逻辑可能出错

四个兼容策略

策略一:扩展不收缩(Expand and Contract)

最安全的策略。分三步:

  1. 扩展阶段:先做向后兼容的 schema 变更——加列(允许 NULL 或有默认值)、加索引、加表。v1 和 v2 都能读写
  2. 切换阶段:蓝绿或金丝雀切换到 v2,v2 使用新 schema 的功能
  3. 收缩阶段:确认 v2 稳定后,删除旧列、旧索引(不可逆操作,要等一个完整回滚窗口后执行)
-- 步骤1:扩展(发布前执行,v1 v2 共存期间安全)
ALTER TABLE orders ADD COLUMN payment_method_v2 VARCHAR(32) DEFAULT 'unknown';
CREATE INDEX CONCURRENTLY idx_orders_status_v2 ON orders(status, payment_method_v2);

-- 步骤2:v2 代码读写新列,v1 代码读写旧列,两者兼容
-- 应用层双写:v2 同时写旧列和新列

-- 步骤3:收缩(v1 下线后执行,通常在下一个发布周期)
ALTER TABLE orders DROP COLUMN payment_method;
DROP INDEX idx_orders_status;

关键原则:扩展和收缩不能在同一个发布周期完成。扩展在 v2 发布前做,收缩在 v2 稳定运行至少一个发布周期后做。这样即使 v2 有问题回滚到 v1,旧 schema 仍然兼容。

策略二:视图代理(View Proxy)

如果 schema 变更涉及列重命名或类型修改,用数据库视图做适配层:

-- 原表结构
CREATE TABLE orders (
    id BIGINT PRIMARY KEY,
    amount_cents INT,        -- 旧字段:金额以分为单位
    status VARCHAR(16)
);

-- 变更目标:amount_cents 改成 amount_yuan DECIMAL(10,2)
-- 步骤1:加新列
ALTER TABLE orders ADD COLUMN amount_yuan DECIMAL(10,2);
UPDATE orders SET amount_yuan = amount_cents / 100.0;

-- 步骤2:创建视图,旧列名映射到新列
CREATE OR REPLACE VIEW orders_legacy AS
SELECT id, amount_yuan * 100 AS amount_cents, status FROM orders;

-- v1 应用读 orders_legacy 视图,v2 应用读 orders 表
-- 两套应用共存期间,数据一致性由视图保证

策略三:双写过渡

对于数据结构变化较大的场景,应用层做双写:

// v2 代码:同时写新旧字段
func (r *OrderRepo) Create(ctx context.Context, order *Order) error {
    // 写新字段
    order.AmountYuan = order.AmountCents / 100.0
    return r.db.WithContext(ctx).Create(order).Error
}

// v2 代码:读时优先新字段,回退旧字段
func (r *OrderRepo) GetByID(ctx context.Context, id int64) (*Order, error) {
    var order Order
    if err := r.db.WithContext(ctx).First(&order, id).Error; err != nil {
        return nil, err
    }
    // 兼容逻辑:如果新字段为空,用旧字段
    if order.AmountYuan == 0 && order.AmountCents > 0 {
        order.AmountYuan = float64(order.AmountCents) / 100.0
    }
    return &order, nil
}

策略四:功能开关(Feature Flag)

新代码部署但不激活,用配置中心控制行为:

# feature_flags.yaml
use_new_payment_flow: false   # v2 部署后先关闭
if config.GetBool("use_new_payment_flow") {
    // 新逻辑
} else {
    // 旧逻辑
}

蓝绿切换后先不打开开关,等绿色环境验证通过再打开。出问题就关开关,不需要回滚代码。这是成本最低的方案,但要求应用架构支持功能开关。

策略选择参考表

场景推荐策略风险等级回滚成本
加列/加索引扩展不收缩低低(旧列还在)
列重命名视图代理中中(删视图即可)
列类型修改双写过渡高高(需数据回填)
大表结构变更功能开关 + 扩展中低(关开关即可)
不可逆操作(删列)必须等回滚窗口后极高无法回滚

我的推荐:绝大多数场景用"扩展不收缩"。这是成本最低、风险最小的策略。只有在列类型必须修改时才考虑双写过渡,且务必预留至少一个完整发布周期做数据迁移。

决策六:自动回滚门禁与监控集成

为什么要自动回滚

人工盯监控做发布决策有三个问题:

  1. 反应速度慢:从指标异常到人看到告警再到决策回滚,平均 8-15 分钟。自动回滚可以在 30 秒内完成
  2. 判断标准不统一:不同人对"什么情况该回滚"的判断不同。运维觉得 P99 到 300ms 该回滚,开发觉得 500ms 还行。用代码定义标准,消除主观判断
  3. 凌晨发布的疲劳因素:凌晨 2 点发布,人盯 30 分钟监控,注意力下降是必然的

曾在某出行项目推动告警体系优化,把故障定位时间从 30 分钟降到 8 分钟(相关文章:回滚了却比不回滚还惨:CI/CD 自动回滚体系的 5 层架构与 4 个生产级踩坑)。但自动回滚不只是"回滚快",关键是"什么时候该回滚"的判断标准要设计好。

Argo Rollouts 自动回滚配置

回到决策四的 AnalysisTemplate,关键参数:

spec:
  metrics:
  - name: success-rate
    interval: 30s          # 每 30 秒查一次
    count: 10              # 共查 10 次(5 分钟窗口)
    successCondition: result[0] >= 0.99
    failureLimit: 3        # 连续失败 3 次触发回滚

这里 failureLimit: 3 意味着 10 次采样中允许 3 次失败(30% 容错率)。如果 4 次失败,Argo Rollouts 自动中止发布,把 VirtualService 的 weight 改回 0%,流量全部回到稳定版。

多指标门禁设计

单一指标做判断不够。我推荐至少三个维度:

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: production-gate
spec:
  args:
  - name: service-name
  metrics:
  # 指标1:错误率
  - name: error-rate
    interval: 30s
    count: 10
    successCondition: result[0] < 0.01    # 错误率 < 1%
    failureLimit: 3
    provider:
      prometheus:
        address: http://prometheus:9090
        query: |
          sum(rate(http_requests_total{service="{{args.service-name}}",code=~"5.."}[2m]))
          /
          sum(rate(http_requests_total{service="{{args.service-name}}"}[2m]))          

  # 指标2:P99 延迟
  - name: p99-latency
    interval: 30s
    count: 10
    successCondition: result[0] < 200     # P99 < 200ms
    failureLimit: 3
    provider:
      prometheus:
        address: http://prometheus:9090
        query: |
          histogram_quantile(0.99,
            sum(rate(http_request_duration_ms_bucket{service="{{args.service-name}}"}[2m])) by (le))          

  # 指标3:CPU 使用率(防止资源耗尽)
  - name: cpu-usage
    interval: 30s
    count: 10
    successCondition: result[0] < 0.85    # CPU < 85%
    failureLimit: 3
    provider:
      prometheus:
        address: http://prometheus:9090
        query: |
          sum(rate(container_cpu_usage_seconds_total{pod=~"{{args.service-name}}-.*"}[2m]))
          /
          sum(kube_pod_container_resource_limits{pod=~"{{args.service-name}}-.*",resource="cpu"})          

三个指标任意一个连续失败 3 次,自动回滚。这个设计覆盖了功能正确性(错误率)、性能(延迟)、资源(CPU)三个维度。

流量镜像:零风险验证

金丝雀发布有一个高级特性——流量镜像(Traffic Mirroring)。Istio 支持把生产流量复制一份发给金丝雀版本,但金丝雀的响应被丢弃。

# 在 VirtualService 中配置流量镜像
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: myapp-vsvc
spec:
  http:
  - name: primary
    route:
    - destination:
        host: myapp-stable
        port:
          number: 80
      weight: 100
    mirror:
      host: myapp-canary          # 镜像流量到金丝雀
      port:
        number: 80
    mirrorPercentage:
      value: 100.0                # 镜像 100% 流量

镜像模式下,100% 流量还是打到稳定版,但同时复制一份给金丝雀版。金丝雀版处理真实流量但不返回结果给用户。你可以观察金丝雀版的错误率、延迟、资源消耗——用的是真实流量,但零用户影响。

适用场景:大版本升级、ORM 框架替换、数据库迁移验证。在正式放量前先用镜像验证 30 分钟,确认无异常再开始金丝雀放量。

回滚后的清理

自动回滚后,金丝雀版本的 ReplicaSet 不会立即删除(保留为 revision history)。这是好事——方便排查问题。但要注意设 revisionHistoryLimit,否则历史版本会堆积:

spec:
  revisionHistoryLimit: 3   # 只保留 3 个历史版本

生产环境完整发布流程

把上面 6 个决策组合成一个完整的发布流程:

阶段一:发布前准备

  1. Schema 扩展(T-2天):执行向后兼容的 DDL——加列、加索引,不删不改
  2. CI 检查:连接池配置对比、镜像安全扫描、配置文件 diff
  3. 资源预检:确认集群有足够资源容纳金丝雀增量 Pod
  4. AnalysisTemplate 部署:确保 Prometheus 指标可用,手动跑一次 query 确认数据正常
# 发布前预检脚本
#!/bin/bash
set -e

# 1. 检查 Prometheus 指标可用
SERVICE="myapp"
PROM_URL="http://prometheus:9090"

echo "=== 检查 Prometheus 指标 ==="
ERROR_RATE=$(curl -s "${PROM_URL}/api/v1/query?query=sum(rate(http_requests_total{service=\"${SERVICE}\",code=~\"5..\"}[2m]))/sum(rate(http_requests_total{service=\"${SERVICE}\"}[2m]))" | jq -r '.data.result[0].value[1]')
echo "当前错误率: ${ERROR_RATE}"
if [ "$(echo "${ERROR_RATE} > 0.01" | bc)" = 1 ]; then
  echo "ERROR: 基线错误率已超过 1%,不建议发布"
  exit 1
fi

# 2. 检查集群资源余量
echo "=== 检查集群资源 ==="
ALLOCATABLE=$(kubectl get nodes -o jsonpath='{.items[*].status.allocatable.cpu}' | awk '{sum+=$1} END {print sum}')
REQUESTED=$(kubectl get pods --all-namespaces -o jsonpath='{.items[*].spec.containers[*].resources.requests.cpu}' | awk '{sum+=$1} END {print sum}')
echo "CPU: ${REQUESTED}/${ALLOCATABLE} 已分配"
if [ "$(echo "${REQUESTED} > ${ALLOCATABLE} * 0.7" | bc)" = 1 ]; then
  echo "ERROR: 集群 CPU 分配率已超 70%,无法安全发布"
  exit 1
fi

# 3. 检查数据库连接数
echo "=== 检查数据库连接数 ==="
CURRENT_CONNS=$(kubectl exec -n data postgres-0 -- psql -U admin -c "SELECT count(*) FROM pg_stat_activity;" -t)
MAX_CONNS=$(kubectl exec -n data postgres-0 -- psql -U admin -c "SHOW max_connections;" -t)
echo "数据库连接: ${CURRENT_CONNS}/${MAX_CONNS}"
if [ "$(echo "${CURRENT_CONNS} > ${MAX_CONNS} * 0.7" | bc)" = 1 ]; then
  echo "ERROR: 数据库连接数已超 70%,蓝绿部署可能导致连接耗尽"
  exit 1
fi

echo "=== 预检通过 ==="

阶段二:金丝雀发布

  1. 触发发布:kubectl argo rollouts set image myapp-rollout myapp=myapp:v2
  2. 5% 流量阶段:观察 5 分钟,检查错误率、延迟、日志
  3. 自动分析:AnalysisTemplate 执行 5 分钟,10 次采样
  4. 20% → 50% → 100%:每步都有 pause 和 analysis 门禁
  5. 全量切换后:观察 30 分钟,确认稳定

阶段三:发布后清理

  1. 旧版本 ReplicaSet 保留 3 个 revision
  2. 等一个完整发布周期后执行 Schema 收缩(删旧列)
  3. 清理 Argo Rollouts 历史记录:kubectl argo rollouts terminate myapp-rollout

常见问题与踩坑清单

坑1:ArgoCD selfHeal 覆盖手动操作

如果用 ArgoCD 管理 Rollout 资源且开启了 selfHeal: true,手动 kubectl argo rollouts promote/abort 的操作可能被 ArgoCD 覆盖。因为 ArgoCD 检测到集群状态和 Git 仓库不一致,会自动同步。

解决方案:发布期间临时关闭 selfHeal,或者把发布状态写回 Git 仓库让 ArgoCD 同步。

坑2:金丝雀 Pod 数为 0 时流量切分异常

如果金丝雀版本的 Pod 还没 Ready,VirtualService 已经分配了 5% 权重,这 5% 流量会打到不存在的 Pod 导致 5xx。

Argo Rollouts 会等所有金丝雀 Pod Ready 后才修改 VirtualService weight,但如果 Pod 启动超时,weight 可能停留在一个中间状态。设置合理的 progressDeadlineSeconds:

spec:
  progressDeadlineSeconds: 300   # 5 分钟无进展判定超时

坑3:Prometheus 指标延迟导致误判

Prometheus 的 rate() 函数基于 2 分钟窗口([2m]),意味着指标有 2 分钟滞后。如果金丝雀版本在第 1 分钟就出问题,AnalysisTemplate 可能在第 3 分钟才检测到。

解决方案:缩短查询窗口到 [30s],但注意短窗口的抖动更大。实测推荐 [1m] 窗口 + count: 10 采样,平衡灵敏度和稳定性。

坑4:Istio sidecar 注入延迟

新 Pod 启动时,Istio sidecar 注入有延迟。如果应用容器先启动并开始接收流量,而 sidecar 还没就绪,请求会失败。

解决:设置 holdApplicationUntilProxyStarts: true(Istio 1.7+):

apiVersion: v1
kind: Pod
metadata:
  annotations:
    proxy.istio.io/config: '{"holdApplicationUntilProxyStarts": true}'

这会确保 Envoy sidecar 就绪后才启动应用容器。

替代方案对比

Argo Rollouts + Istio 不是唯一选择。简要对比:

方案优点缺点适用场景
K8s 原生 Deployment + RollingUpdate零依赖,最简单无法控制流量比例、无自动回滚非核心服务、小团队
Nginx Ingress Canary零额外组件,ingress-nginx 原生功能有限、无指标分析中小规模集群
Argo Rollouts + Nginx支持自动回滚和指标分析Nginx 流量切分精度不如 Istio不想用 Service Mesh 的团队
Argo Rollouts + Istio精确流量控制、镜像流量、指标分析引入 Istio 的运维复杂度核心链路、大规模集群
Flagger + Istio类似 Argo Rollouts,支持更多指标源社区比 Argo 小已用 Flagger 的团队

我的推荐:50 节点以下用 Argo Rollouts + Nginx Ingress,50 节点以上或核心链路用 Argo Rollouts + Istio。Istio 的运维成本不低,小集群不值得。但如果已经上了 Istio 做流量管理,那搭配 Argo Rollouts 是自然选择。

总结

蓝绿部署和金丝雀发布的核心不是"怎么切流量",而是"切流量前后怎么保证不出事"。六个决策回顾:

  1. 选对策略:核心链路用金丝雀,低风险用蓝绿,非核心用 RollingUpdate。别给管理后台上金丝雀
  2. 蓝绿陷阱:双倍资源、数据库连接池翻倍、缓存冷启动——发布前做资源预检和连接池配置对比
  3. 流量控制三层:Pod 副本比例 → Ingress 注解 → Service Mesh + Argo Rollouts。生产级用第三层
  4. Argo Rollouts 配置:Rollout CRD + Istio VirtualService + AnalysisTemplate 三件套。analysis 的 count 和 failureLimit 要调好
  5. 数据库迁移:扩展不收缩策略为主。加列加索引在发布前做,删列在下个周期做。绝对不在同一发布周期内做不可逆操作
  6. 自动回滚门禁:多指标(错误率 + 延迟 + 资源)联合判断,failureLimit 给 30% 容错。流量镜像是零风险验证手段

实际经验:在某出行平台推动可用性从 99.5% 提升到 99.9% 的过程中(相关文章:变更管理:灰度发布与回滚策略),金丝雀发布 + 自动回滚是贡献最大的单项工程——变更类故障减少了 60%,MTTR 从 40 分钟降到 8 分钟。但前提是 AnalysisTemplate 的指标阈值调了 3 轮才稳定——第一轮太灵敏(偶发抖动就回滚),第二轮太迟钝(错误率 10% 了还不回滚),第三轮找到平衡点。

发布策略没有银弹。核心原则就一条:爆炸半径越小越好,回滚速度越快越好。金丝雀 + 自动回滚把这两条做到了极致。

参考资料与致谢

本文在撰写过程中参考了以下资料,感谢原作者的贡献:

  1. Argo Rollouts - Kubernetes Progressive Delivery Controller — Argo Rollouts 官方文档,提供 Blue-Green 和 Canary 策略的完整 CRD 规范和流量路由集成方案
  2. How To Use Blue-Green Deployments to Release Software Safely — DigitalOcean,蓝绿部署的概念定义和传统实现方式
  3. Argo Rollouts Canary Strategy — Argo Rollouts 官方文档,金丝雀策略的 setWeight、pause、analysis 步骤详解
  4. 在Istio 服务网格中使用 Argo Rollouts 实现智能的渐进式发布 — 详细拆解 Argo Rollouts 与 Istio VirtualService 集成的架构设计和 AnalysisTemplate 配置
  5. 3套开源灰度发布方案,从Nginx Ingress到Argo Rollouts我全踩了一遍 — 实战对比 Nginx Ingress Canary、Argo Rollouts + Nginx、Argo Rollouts + Istio 三种方案的踩坑记录