← 返回博客
2026-07-22 16:25:20

多环境 CI/CD 流水线设计:灰度发布与金丝雀策略的工程实践

从手动部署到灰度发布:搭一条生产级 CI/CD 流水线

几个月前帮一个团队搭 CI/CD。他们的发布流程是:本地 mvn package,SCP 把 jar 传上服务器,登上去 systemctl restart。三个环境各来一遍。有一次周五下午发布,staging 上漏了一个环境变量,线上挂了 40 分钟才发现。

这篇文章还原当时搭建整条流水线的过程——从代码提交到灰度发布,逐步往上加东西,每一步解决一个实际问题。

先跑通最简流水线:提交即部署到 Dev

先从最需要的开始:代码 push 到 GitLab,自动构建镜像、推到 registry、部署到 dev 环境的 K8s 集群。

# .gitlab-ci.yml
stages:
  - build
  - deploy-dev

variables:
  IMAGE_TAG: $CI_COMMIT_SHORT_SHA

build:
  stage: build
  image: docker:latest
  script:
    - docker build -t registry.example.com/app:$IMAGE_TAG .
    - docker push registry.example.com/app:$IMAGE_TAG

deploy-dev:
  stage: deploy-dev
  image: bitnami/kubectl:latest
  script:
    - kubectl set image deployment/app app=registry.example.com/app:$IMAGE_TAG -n dev

这一步没什么花活,但它解决了一个核心问题:消除"我这能跑啊"。从此 dev 环境永远反映最新代码,开发联调不再需要互相喊"你部署了没"。

给每个 MR 一个独立环境

问题接着来了:三个人同时提 MR,都往 dev 环境部署,互相踩。A 改的连接池配置刚部署上去,B 的部署就把镜像覆盖了,A 的测试白跑。

解决方案:每个 MR 创建一个临时 K8s Namespace,部署独立的应用实例。MR 合并后自动销毁。

deploy-ephemeral:
  stage: deploy-ephemeral
  image: bitnami/kubectl:latest
  script:
    - |
      # 为每个 MR 创建独立 namespace,命名带上分支名避免冲突
      NAMESPACE="review-${CI_COMMIT_REF_SLUG}"
      kubectl create namespace ${NAMESPACE} --dry-run=client -o yaml | kubectl apply -f -
      helm upgrade --install app-review ./charts/app \
        -n ${NAMESPACE} \
        --set image.tag=$IMAGE_TAG \
        --set ingress.host="${CI_COMMIT_REF_SLUG}.review.example.com"
  environment:
    name: review/${CI_COMMIT_REF_SLUG}
    url: https://${CI_COMMIT_REF_SLUG}.review.example.com
    on_stop: delete-ephemeral
  only:
    - merge_requests

几个容易忽略的细节:

从 Dev 到 Staging:把变量交给环境

Dev 验证通过后,下一步是 staging。和 dev 不同,staging 应该尽可能接近生产——用同样的数据库规格、同样的副本数、只是不上生产流量。

关键区别在于配置管理。dev 和 staging 的差异(数据库地址、资源配额、日志级别)不应该写在 CI 文件里,而是用 Helm values 文件分开管理:

deploy-staging:
  stage: deploy-staging
  image: bitnami/kubectl:latest
  script:
    - |
      helm upgrade --install app ./charts/app \
        -n staging \
        -f charts/app/values.yaml \
        -f charts/app/values-staging.yaml \
        --set image.tag=$IMAGE_TAG
  environment:
    name: staging
  only:
    - main

-f charts/app/values.yaml 是公共默认值,-f charts/app/values-staging.yaml 覆盖 staging 特有的配置。这样 CI 文件不掺和环境差异,变量归属清晰。

生产金丝雀:先放 5% 流量,观察,再全量

生产部署最大的风险不是代码有 bug——bug 在 dev 和 staging 应该已经暴露了——而是生产特有的问题:真实数据量级下索引失效、用户并发下连接池耗尽、下游依赖在生产环境有不同的超时配置。

金丝雀策略的思路很简单:新版本先只接 5% 流量,观察关键指标(错误率、延迟),没问题再逐步放量。

用 Argo Rollouts 替代原生 Deployment,声明金丝雀步骤:

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: app
spec:
  replicas: 10
  strategy:
    canary:
      steps:
      - setWeight: 5
      - pause: {duration: 120}
      - setWeight: 25
      - pause: {duration: 120}
      - setWeight: 100

这个配置的含义:

步长怎么定?5% 这个数字来源于:如果新版本有严重的启动崩溃,5% 的流量只影响 5% 的用户,已经在报警阈值内但不会完全瘫痪服务。如果你的服务日活百万级,5% 就是五万人,足够验证但不会造成大事故。

流量怎么分?Istio 做流量控制

单纯靠 K8s Pod 数量来分流量是不准的——Pod 数量比例和实际请求数不完全对应。需要 Service Mesh 在请求级别做流量拆分。

Argo Rollouts 通过 trafficRouting 字段告诉 Istio 调整 VirtualService 的权重:

# Rollout 中增加 trafficRouting 配置
trafficRouting:
  istio:
    virtualService:
      name: app
      routes:
      - primary   # stable 版本的 route 名

不需要手动改 VirtualService——Argo Rollouts 的 controller 会根据 canary steps 里的 setWeight 自动调整 Istio 的路由权重。你只管声明"5% → 25% → 100%",controller 负责操作 Istio API。

自动回滚:不是"出问题人来看",是"指标异常机器来停"

金丝雀如果没有自动回滚,等于让人盯着 Grafana 看 4 分钟——2 分钟 5% + 2 分钟 25%。凌晨三点部署怎么办?

Argo Rollouts 支持基于 Prometheus 指标的自动分析:

# Rollout 中增加 analysis 配置
strategy:
  canary:
    steps:
    - setWeight: 5
    - pause: {duration: 120}
    - setWeight: 25
    - pause: {duration: 120}
    - setWeight: 100
    analysis:
      templates:
      - templateName: canary-metrics
      startingStep: 1        # 从第一步(5% 流量)开始监控
      args:
      - name: service-name
        value: app
---
# AnalysisTemplate 定义监控指标
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: canary-metrics
spec:
  metrics:
  - name: error-rate
    interval: 30s
    failureLimit: 2          # 连续两次超标即判定失败
    provider:
      prometheus:
        address: http://prometheus.monitoring:9090
        query: |
          sum(rate(http_requests_total{service="{{args.service-name}}",status=~"5.."}[1m]))
          /
          sum(rate(http_requests_total{service="{{args.service-name}}"}[1m]))
    successCondition: result < 0.01   # 错误率低于 1%

这段配置的效果是:每 30 秒跑一次 PromQL,如果连续两次错误率超过 1%,Argo Rollouts 自动回滚到旧版本。人不参与判断。

failureLimit: 2 而不是 1 的理由:避免单次毛刺触发回滚。Prometheus 的一次瞬时抖动可能是网络波动,连续两次才说明是真的有问题。

完整流水线

把上面的步骤串起来,完整的交付路径是:

git push (MR)
  → 单元测试 + 镜像构建
  → 临时环境部署(MR 专属 namespace)
  → Code Review + 合并到 main
  → 自动部署 Staging
  → 手动触发生产部署
  → 金丝雀 5% → 25% → 100%(含自动回滚)
  → 清理临时环境

每一步都有明确的触发条件和失败处理方式,不是一段 YAML 扔在那里猜。

实践建议

本文关键词:CI/CD、金丝雀发布、Argo Rollouts、Istio、Kubernetes、DevOps