蓝绿发布

蓝绿发布

蓝绿发布是一种零停机部署策略,通过维护两套完全相同的生产环境(蓝环境和绿环境),实现快速切换和回滚。

什么是蓝绿发布

核心概念

┌─────────────────────────────────────┐
│           负载均衡器                 │
│         (Service/Ingress)           │
└──────────────┬──────────────────────┘
               │
               │ 流量切换
               │
        ┌──────┴──────┐
        │             │
    蓝环境 v1      绿环境 v2
    (运行中)       (待上线)
        │             │
   ┌────▼────┐   ┌───▼─────┐
   │ 3 Pods  │   │ 3 Pods  │
   │ v1.0    │   │ v2.0    │
   └─────────┘   └─────────┘

蓝环境(Blue):当前正在运行的生产环境
绿环境(Green):新版本部署的环境

蓝绿发布流程

  1. 部署绿环境:在生产集群部署新版本(v2.0)
  2. 测试验证:对绿环境进行完整测试
  3. 流量切换:将负载均衡器指向绿环境
  4. 监控观察:观察新版本运行状态
  5. 保留蓝环境:保留旧版本一段时间用于快速回滚
  6. 清理资源:确认无问题后删除蓝环境

优势与劣势

✅ 优势

  • 零停机部署:切换瞬间完成
  • 快速回滚:秒级回滚到旧版本
  • 完整测试:可在生产环境测试新版本
  • 风险可控:出问题立即切回
  • 用户无感知:平滑过渡

❌ 劣势

  • 资源消耗:需要双倍资源
  • 数据库迁移:需要兼容两个版本
  • 成本较高:临时需要 2 倍实例
  • 状态管理:有状态应用需特殊处理

实现方式

方式 1:基于 Service 标签选择器

这是最简单直接的实现方式。

1. 部署蓝环境(当前版本)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-blue
  labels:
    app: myapp
    version: blue
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
      version: blue
  template:
    metadata:
      labels:
        app: myapp
        version: blue
    spec:
      containers:
      - name: myapp
        image: myapp:v1.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 200m
            memory: 256Mi
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

2. 创建 Service(指向蓝环境)

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
  labels:
    app: myapp
spec:
  selector:
    app: myapp
    version: blue  # 当前指向蓝色环境
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: ClusterIP

3. 部署绿环境(新版本)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-green
  labels:
    app: myapp
    version: green
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
      version: green
  template:
    metadata:
      labels:
        app: myapp
        version: green
    spec:
      containers:
      - name: myapp
        image: myapp:v2.0  # 新版本
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 200m
            memory: 256Mi
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

4. 测试绿环境

# 创建临时测试 Service
kubectl expose deployment myapp-green \
  --name=myapp-green-test \
  --port=80 \
  --target-port=8080 \
  --type=ClusterIP

# 端口转发测试
kubectl port-forward svc/myapp-green-test 8080:80

# 在另一个终端测试
curl http://localhost:8080/health
curl http://localhost:8080/api/test

# 运行完整测试套件
./scripts/integration-test.sh http://localhost:8080

5. 切换流量到绿环境

# 方法 1:使用 kubectl patch
kubectl patch service myapp-service -p '{"spec":{"selector":{"version":"green"}}}'

# 方法 2:使用 kubectl edit
kubectl edit service myapp-service
# 修改 selector.version: green

# 方法 3:使用 kubectl apply
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp
    version: green
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
EOF

6. 验证切换

# 查看 Service 选择器
kubectl get service myapp-service -o yaml | grep -A2 selector

# 查看 Endpoints
kubectl get endpoints myapp-service

# 测试访问
for i in {1..10}; do
  curl http://myapp-service/version
  sleep 1
done

7. 保留蓝环境观察期

# 监控新版本指标
kubectl top pods -l version=green

# 查看日志
kubectl logs -f -l version=green --tail=100

# 检查错误率
kubectl logs -l version=green | grep ERROR | wc -l

8. 确认无误后清理蓝环境

# 删除蓝环境
kubectl delete deployment myapp-blue

# 清理测试 Service
kubectl delete service myapp-green-test

# 重命名绿环境为蓝环境(为下次发布做准备)
kubectl patch deployment myapp-green -p '{"metadata":{"name":"myapp-blue"}}'
kubectl patch deployment myapp-green -p '{"spec":{"selector":{"matchLabels":{"version":"blue"}},"template":{"metadata":{"labels":{"version":"blue"}}}}}'

方式 2:使用 Ingress 流量切换

更适合对外暴露的服务。

1. 创建两个 Service

# 蓝环境 Service
apiVersion: v1
kind: Service
metadata:
  name: myapp-blue-svc
spec:
  selector:
    app: myapp
    version: blue
  ports:
  - port: 80
    targetPort: 8080

---
# 绿环境 Service
apiVersion: v1
kind: Service
metadata:
  name: myapp-green-svc
spec:
  selector:
    app: myapp
    version: green
  ports:
  - port: 80
    targetPort: 8080

2. 配置 Ingress(指向蓝环境)

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: myapp-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp-blue-svc  # 指向蓝环境
            port:
              number: 80

3. 切换到绿环境

# 更新 Ingress
kubectl patch ingress myapp-ingress -p '
{
  "spec": {
    "rules": [{
      "host": "myapp.example.com",
      "http": {
        "paths": [{
          "path": "/",
          "pathType": "Prefix",
          "backend": {
            "service": {
              "name": "myapp-green-svc",
              "port": {
                "number": 80
              }
            }
          }
        }]
      }
    }]
  }
}'

# 验证
curl http://myapp.example.com/version

方式 3:使用 Argo Rollouts 蓝绿发布

最推荐的自动化方式。

1. 安装 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 -LO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
chmod +x kubectl-argo-rollouts-linux-amd64
sudo mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts

2. 定义 Rollout 资源

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: myapp-rollout
spec:
  replicas: 3
  revisionHistoryLimit: 2
  
  selector:
    matchLabels:
      app: myapp
  
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:v1.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 200m
            memory: 256Mi
  
  # 蓝绿发布策略
  strategy:
    blueGreen:
      # 激活的 Service(面向用户)
      activeService: myapp-active
      # 预览 Service(用于测试)
      previewService: myapp-preview
      # 自动升级
      autoPromotionEnabled: false
      # 自动升级延迟
      autoPromotionSeconds: 30
      # 保留旧版本的时间
      scaleDownDelaySeconds: 30
      # 保留旧版本的副本数
      scaleDownDelayRevisionLimit: 1

3. 创建 Services

# Active Service(生产流量)
apiVersion: v1
kind: Service
metadata:
  name: myapp-active
spec:
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 8080

---
# Preview Service(测试流量)
apiVersion: v1
kind: Service
metadata:
  name: myapp-preview
spec:
  selector:
    app: myapp
  ports:
  - port: 80
    targetPort: 8080

4. 触发蓝绿发布

# 更新镜像触发发布
kubectl argo rollouts set image myapp-rollout \
  myapp=myapp:v2.0

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

# 输出示例:
# Name:            myapp-rollout
# Namespace:       default
# Status:          ॥ Paused
# Strategy:        BlueGreen
# Images:          myapp:v1.0 (stable, active)
#                  myapp:v2.0 (preview)
# Replicas:
#   Desired:       3
#   Current:       6
#   Updated:       3
#   Ready:         6
#   Available:     6

5. 测试预览环境

# 通过 preview service 测试
kubectl port-forward svc/myapp-preview 8080:80

# 在另一个终端
curl http://localhost:8080/health
curl http://localhost:8080/api/test

# 运行测试套件
./scripts/smoke-test.sh http://localhost:8080

6. 升级(切换流量)

# 手动升级
kubectl argo rollouts promote myapp-rollout

# 查看升级进度
kubectl argo rollouts get rollout myapp-rollout --watch

# 输出示例:
# Name:            myapp-rollout
# Namespace:       default
# Status:          ✔ Healthy
# Strategy:        BlueGreen
# Images:          myapp:v2.0 (stable, active)
# Replicas:
#   Desired:       3
#   Current:       3
#   Updated:       3
#   Ready:         3
#   Available:     3

7. 回滚

# 中止发布(回滚到旧版本)
kubectl argo rollouts abort myapp-rollout

# 撤销到上一个版本
kubectl argo rollouts undo myapp-rollout

# 撤销到指定版本
kubectl argo rollouts undo myapp-rollout --to-revision=2

数据库兼容性处理

蓝绿发布的关键挑战:数据库必须兼容新旧两个版本

数据库迁移策略

方案 1:向后兼容的迁移

阶段 1:添加新列(旧版本忽略)
  v1.0 代码 + 数据库(有新列)
  ↓
阶段 2:部署新版本
  v2.0 代码开始使用新列
  ↓
阶段 3:删除旧列(下次发布)
  确认旧版本已完全下线后删除

示例:添加新字段

-- ✅ 阶段 1:添加新列(可为空)
ALTER TABLE users 
ADD COLUMN email VARCHAR(255) NULL;

-- ✅ 阶段 2:新版本开始填充数据
-- v2.0 应用代码写入 email 字段

-- ✅ 阶段 3:设置为非空(下次发布)
ALTER TABLE users 
MODIFY COLUMN email VARCHAR(255) NOT NULL;

方案 2:Feature Flag 控制

// 应用代码中使用 Feature Flag
func (s *UserService) GetUser(id int) (*User, error) {
    user, err := s.db.GetUserByID(id)
    if err != nil {
        return nil, err
    }
    
    // Feature Flag 控制新功能
    if s.featureFlags.IsEnabled("use_new_email_field") {
        user.Email = s.db.GetUserEmail(id)
    }
    
    return user, nil
}

方案 3:双写策略

// 同时写入新旧字段
func (s *UserService) UpdateUser(user *User) error {
    return s.db.Transaction(func(tx *sql.Tx) error {
        // 写入旧字段(兼容 v1.0)
        if err := tx.UpdateUserLegacy(user); err != nil {
            return err
        }
        
        // 写入新字段(v2.0 使用)
        if err := tx.UpdateUserNew(user); err != nil {
            return err
        }
        
        return nil
    })
}

最佳实践

1. 健康检查

确保应用有完善的健康检查端点。

livenessProbe:
  httpGet:
    path: /health/live
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5
  timeoutSeconds: 3
  failureThreshold: 3

健康检查端点实现:

// /health/live - 存活检查
func LivenessHandler(w http.ResponseWriter, r *http.Request) {
    // 检查应用是否还活着
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}

// /health/ready - 就绪检查
func ReadinessHandler(w http.ResponseWriter, r *http.Request) {
    // 检查依赖服务
    if err := checkDatabase(); err != nil {
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }
    
    if err := checkRedis(); err != nil {
        w.WriteHeader(http.StatusServiceUnavailable)
        return
    }
    
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Ready"))
}

2. 资源预留

确保有足够资源运行双环境。

# 设置资源限制
resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "200m"
    memory: "256Mi"

# 使用 PodDisruptionBudget 保证可用性
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: myapp-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: myapp

3. 监控指标

切换前后密切监控关键指标。

# Prometheus 查询示例
# 错误率
rate(http_requests_total{status=~"5.."}[5m])

# 响应时间
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

# QPS
rate(http_requests_total[1m])

4. 自动化脚本

完整的蓝绿部署脚本:

#!/bin/bash
set -e

APP_NAME="myapp"
NEW_VERSION=$1
NAMESPACE="production"

if [ -z "$NEW_VERSION" ]; then
    echo "Usage: $0 <new-version>"
    exit 1
fi

echo "🚀 开始蓝绿发布: $APP_NAME -> $NEW_VERSION"

# 1. 确定当前颜色
CURRENT_COLOR=$(kubectl get service $APP_NAME-service -n $NAMESPACE -o jsonpath='{.spec.selector.version}')
echo "📍 当前颜色: $CURRENT_COLOR"

# 2. 确定新颜色
if [ "$CURRENT_COLOR" == "blue" ]; then
    NEW_COLOR="green"
else
    NEW_COLOR="blue"
fi
echo "🎨 新颜色: $NEW_COLOR"

# 3. 部署新版本
echo "📦 部署 $NEW_COLOR 环境..."
kubectl set image deployment/$APP_NAME-$NEW_COLOR \
    $APP_NAME=myregistry/$APP_NAME:$NEW_VERSION \
    -n $NAMESPACE

# 4. 等待就绪
echo "⏳ 等待 $NEW_COLOR 环境就绪..."
kubectl rollout status deployment/$APP_NAME-$NEW_COLOR -n $NAMESPACE --timeout=5m

# 5. 创建测试 Service
echo "🧪 创建测试 Service..."
kubectl expose deployment $APP_NAME-$NEW_COLOR \
    --name=$APP_NAME-$NEW_COLOR-test \
    --port=80 \
    --target-port=8080 \
    --type=ClusterIP \
    -n $NAMESPACE \
    --dry-run=client -o yaml | kubectl apply -f -

# 6. 运行冒烟测试
echo "🔍 运行冒烟测试..."
if ! ./scripts/smoke-test.sh $APP_NAME-$NEW_COLOR-test.$NAMESPACE.svc.cluster.local; then
    echo "❌ 冒烟测试失败,回滚部署"
    kubectl delete service $APP_NAME-$NEW_COLOR-test -n $NAMESPACE
    exit 1
fi

# 7. 切换流量
echo "🔄 切换流量到 $NEW_COLOR 环境..."
kubectl patch service $APP_NAME-service \
    -n $NAMESPACE \
    -p "{\"spec\":{\"selector\":{\"version\":\"$NEW_COLOR\"}}}"

echo "✅ 流量已切换到 $NEW_COLOR 环境"

# 8. 监控
echo "📊 监控新版本 30 秒..."
sleep 30

# 9. 检查健康状态
echo "🏥 检查健康状态..."
READY_PODS=$(kubectl get pods -l app=$APP_NAME,version=$NEW_COLOR -n $NAMESPACE -o jsonpath='{.items[?(@.status.phase=="Running")].metadata.name}' | wc -w)
TOTAL_PODS=$(kubectl get deployment $APP_NAME-$NEW_COLOR -n $NAMESPACE -o jsonpath='{.spec.replicas}')

if [ "$READY_PODS" -ne "$TOTAL_PODS" ]; then
    echo "⚠️  警告: 只有 $READY_PODS/$TOTAL_PODS 个 Pod 就绪"
    read -p "是否继续?(y/n) " -n 1 -r
    echo
    if [[ ! $REPLY =~ ^[Yy]$ ]]; then
        echo "🔙 回滚到 $CURRENT_COLOR"
        kubectl patch service $APP_NAME-service \
            -n $NAMESPACE \
            -p "{\"spec\":{\"selector\":{\"version\":\"$CURRENT_COLOR\"}}}"
        exit 1
    fi
fi

# 10. 清理
echo "🧹 清理测试 Service..."
kubectl delete service $APP_NAME-$NEW_COLOR-test -n $NAMESPACE

echo "✅ 蓝绿发布完成!"
echo "📝 旧版本 ($CURRENT_COLOR) 仍在运行,建议保留 1-7 天"
echo "   手动删除命令: kubectl delete deployment $APP_NAME-$CURRENT_COLOR -n $NAMESPACE"

5. 回滚检查清单

□ 监控错误率是否上升
□ 检查响应时间是否正常
□ 查看应用日志是否有异常
□ 验证数据库连接是否正常
□ 测试关键业务功能
□ 检查依赖服务状态
□ 确认用户反馈

故障处理

问题 1:切换后服务不可用

排查步骤:

# 1. 检查 Service Endpoints
kubectl get endpoints myapp-service

# 2. 检查 Pod 状态
kubectl get pods -l version=green

# 3. 检查 Pod 日志
kubectl logs -l version=green --tail=100

# 4. 检查健康检查
kubectl describe pod <pod-name> | grep -A10 Readiness

快速回滚:

kubectl patch service myapp-service \
  -p '{"spec":{"selector":{"version":"blue"}}}'

问题 2:数据库不兼容

解决方案:

# 1. 立即回滚应用
kubectl patch service myapp-service \
  -p '{"spec":{"selector":{"version":"blue"}}}'

# 2. 回滚数据库迁移
mysql -u root -p < rollback.sql

# 3. 检查数据一致性
./scripts/verify-data-consistency.sh

问题 3:资源不足

临时扩容:

# 手动扩容节点
kubectl scale deployment myapp-green --replicas=2

# 或使用 Cluster Autoscaler 自动扩容
kubectl annotate deployment myapp-green \
  cluster-autoscaler.kubernetes.io/safe-to-evict="false"

对比:蓝绿 vs 金丝雀

特性 蓝绿发布 金丝雀发布
切换方式 一次性切换所有流量 渐进式增加流量
资源消耗 需要双倍资源 资源消耗较小
回滚速度 秒级回滚 需要逐步回滚
风险控制 风险集中在切换时刻 风险分散,影响范围小
测试验证 可充分测试完整环境 生产环境逐步验证
适用场景 重要系统、需要快速回滚 大规模应用、风险高的更新
复杂度 相对简单 需要流量控制机制

选择建议:

  • 小型应用、资源有限:金丝雀发布
  • 关键业务、要求零停机:蓝绿发布
  • 数据库变更较大:金丝雀发布(逐步验证)
  • 有充足资源、需要快速回滚:蓝绿发布

小结

蓝绿发布是一种简单高效的零停机部署策略:

核心要点:

  • 维护蓝绿两套环境
  • 流量一次性切换
  • 快速回滚能力
  • 需要双倍资源

实现方式:

  1. Service 标签选择器:最简单
  2. Ingress 流量切换:适合对外服务
  3. Argo Rollouts:自动化管理

关键挑战:

  • 数据库兼容性
  • 资源成本
  • 状态管理

下一章我们将学习更多云原生发布策略和工具。