【架构实战】蓝绿部署:零停机发布的工程实践

【架构实战】蓝绿部署:零停机发布的工程实践

一、背景:每一次重启都是一场冒险

2019年,我们支付系统有次发版,运维凌晨3点手动操作:

  1. 通知Nginx摘除一台机器
  2. 等5分钟,确认没有未完成的请求
  3. 停服务 → 替换Jar包 → 重启 → 健康检查 → 挂回Nginx
  4. 重复以上操作4台机器

总共花了40分钟,期间系统处理能力下降到25%。那晚差点超时触发银行的对账异常。

更离谱的是,有一次运维在第三步"替换Jar包"时复制错了文件,把测试环境的包放到了线上。用户支付成功后余额没扣——白送了一天。

每一次发布都是一次赌博:赌服务重启不超时、赌新版本没Bug、赌操作不出错。

蓝绿部署的核心思想很简单:准备两套完整环境,切换流量而非更新服务。


二、蓝绿部署的核心原理

2.1 什么是蓝绿部署

┌─────────────────────────────────────────────────────────┐ │ 蓝绿部署 │ │ │ │ 当前线上环境(Blue) 备用环境(Green) │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ 订单服务 V1 │ │ 订单服务 V2 │ │ │ │ 库存服务 V1 │ │ 库存服务 V2 │ │ │ │ 支付服务 V1 │ │ 支付服务 V2 │ │ │ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ ┌──────▼───────────────────────▼───────┐ │ │ │ 负载均衡器 │ │ │ │ (Nginx/Haproxy/Kong) │ │ │ └──────┬───────────────────────────────┘ │ │ │ │ │ 用户流量 │ └─────────────────────────────────────────────────────────┘ 发布流程: 1. 当前 Blue 环境承载 100% 流量 2. 在 Green 环境部署新版本,充分验证 3. 负载均衡器将流量从 Blue 切到 Green 4. Green 成为新 Blue,旧 Blue 保留作为回滚环境

2.2 蓝绿部署 vs 其他发布策略

维度蓝绿部署金丝雀发布滚动更新
切换速度秒级分钟-小时分钟-小时
回滚速度秒级秒级分钟级
资源成本2倍1.1-1.2倍正常
适用场景重大版本升级日常发布无状态服务
环境隔离完全隔离部分隔离无隔离
数据库兼容需要注意需要注意需要注意

三、蓝绿部署的关键技术实现

3.1 负载均衡层切换

方案一:Nginx upstream 切换

# nginx.conf upstream backend_blue { server 10.0.1.1:8080 weight=100; server 10.0.1.2:8080 weight=100; server 10.0.1.3:8080 weight=100; } upstream backend_green { server 10.0.2.1:8080 weight=0; # 初始权重为0 server 10.0.2.2:8080 weight=0; server 10.0.2.3:8080 weight=0; } server { listen 80; location / { # 默认走Blue环境 proxy_pass http://backend_blue; } }

切换脚本

#!/bin/bash# blue-green-switch.shecho"=== 蓝绿部署流量切换 ==="# 1. Green环境预热(先发少量流量验证)echo"1/4 Green环境预热..."sed-i's/weight=0/weight=5/g'/etc/nginx/conf.d/upstream.conf nginx-sreloadsleep30# 2. 检查Green环境健康状态echo"2/4 检查Green环境..."foriin{1..3};doSTATUS=$(curl-s-o/dev/null-w"%{http_code}"http://10.0.2.${i}:8080/actuator/health)if["$STATUS"!="200"];thenecho"Green环境节点${i}不健康,终止切换"exit1fidone# 3. 全量切换流量echo"3/4 全量切换到Green环境..."# 将Blue的weight设为0sed-i'/backend_blue/,/}/{s/weight=100/weight=0/g}'/etc/nginx/conf.d/upstream.conf# 将Green的weight设为100sed-i'/backend_green/,/}/{s/weight=5/weight=100/g}'/etc/nginx/conf.d/upstream.conf nginx-sreload# 4. 确认切换完成echo"4/4 确认流量切换..."sleep10BLUE_COUNT=$(tail-1000/var/log/nginx/access.log|grep"backend_blue"|wc-l)GREEN_COUNT=$(tail-1000/var/log/nginx/access.log|grep"backend_green"|wc-l)echo"Blue请求:${BLUE_COUNT}, Green请求:${GREEN_COUNT}"echo"=== 切换完成 ==="

方案二:Kubernetes Service切换

# Blue环境 ServiceapiVersion:v1kind:Servicemetadata:name:order-service-bluelabels:app:orderversion:bluespec:selector:app:orderversion:v1# 老版本ports:-port:8080---# Green环境 ServiceapiVersion:v1kind:Servicemetadata:name:order-service-greenlabels:app:orderversion:greenspec:selector:app:orderversion:v2# 新版本ports:-port:8080---# 主 Service(通过修改selector切换)apiVersion:v1kind:Servicemetadata:name:order-service# 对外暴露的Service名不变spec:selector:app:orderversion:v1# 切换时仅改这个字段ports:-port:8080

切换命令:

# 从Blue切换到Greenkubectl patchserviceorder-service-p'{"spec":{"selector":{"version":"v2"}}}'# 回滚(从Green切回Blue)kubectl patchserviceorder-service-p'{"spec":{"selector":{"version":"v1"}}}'

3.2 数据库兼容性处理

蓝绿部署最大的坑不是服务,是数据库。

新老版本可能同时操作同一张表,Schema不兼容就会出问题。

错误示例: V1: ALTER TABLE orders ADD COLUMN status VARCHAR(20); -- 新增字段 V2: ALTER TABLE orders DROP COLUMN status; -- 删除字段 结果:V1和V2同时在跑,V1依赖status字段 → 报错 正确做法:数据库变更独立于应用发布,分两步走 第一步(兼容阶段): V1 + V2 同时支持 status和new_status 两个字段 第二步(清理阶段): 确认不需要回滚到V1后,再删除 status 字段

数据库变更黄金法则

  1. 只新增,不修改,不删除
  2. 新增字段必须有默认值
  3. 字段重命名 = 新增 + 双写 + 老字段延迟删除
  4. 数据库变更和应用发布分开执行
-- 正确示例:新增字段ALTERTABLEordersADDCOLUMNcoupon_typeVARCHAR(20)DEFAULT'NORMAL';-- 错误示例:直接修改字段类型ALTERTABLEordersMODIFYCOLUMNamountDECIMAL(14,4);-- V1代码期望DECIMAL(12,2),V2期望DECIMAL(14,4),同时运行会出错

3.3 缓存兼容性处理

蓝绿部署中,新老版本可能操作同一个Redis Key,数据结构不兼容会导致序列化异常。

// 解决方案:版本化缓存KeypublicclassCacheKeyBuilder{privatestaticfinalStringCACHE_VERSION="v2";// 应用版本publicstaticStringbuildKey(Stringentity,Stringid){returnString.format("%s:%s:%s",entity,CACHE_VERSION,id);}}// V1缓存Key: user:v1:123// V2缓存Key: user:v2:123// 新老版本互不影响,避免序列化冲突

四、蓝绿部署实战脚本

4.1 完整的蓝绿部署流程

#!/bin/bash# 完整的蓝绿部署脚本set-eAPP_NAME=$1VERSION=$2ENV=${3:-prod}BLUE_NS="${APP_NAME}-blue"GREEN_NS="${APP_NAME}-green"echo"========================================="echo" 蓝绿部署:${APP_NAME}->${VERSION}"echo"========================================="# 1. 确定当前活跃环境CURRENT=$(kubectl get svc ${APP_NAME}-ojsonpath='{.spec.selector.version}')if["$CURRENT"=="blue"];thenACTIVE=$BLUE_NSSTANDBY=$GREEN_NSNEW_COLOR="green"elseACTIVE=$GREEN_NSSTANDBY=$BLUE_NSNEW_COLOR="blue"fiecho"当前活跃:$ACTIVE, 目标环境:$STANDBY"# 2. 部署新版本到备用环境echo"部署${VERSION}${STANDBY}..."kubectlsetimage deployment/${APP_NAME}\${APP_NAME}=registry.company.com/${APP_NAME}:${VERSION}\-n${STANDBY}# 3. 等待就绪echo"等待新版本就绪..."kubectl rollout status deployment/${APP_NAME}-n${STANDBY}--timeout=300s# 4. 健康检查echo"健康检查..."PODS=$(kubectl get pods-n${STANDBY}-lapp=${APP_NAME}-ojsonpath='{.items[*].status.podIP}')forPODin$PODS;doHEALTH=$(curl-s-o/dev/null-w"%{http_code}"http://${POD}:8080/actuator/health)if["$HEALTH"!="200"];thenecho"节点${POD}不健康"exit1fidoneecho"所有节点健康 ✓"# 5. 预热(可选:先发送少量流量)echo"预热备用环境..."# 通过Istio注入1%测试流量...# 6. 切换流量echo"切换流量到${STANDBY}..."kubectl patchservice${APP_NAME}-p"{\"spec\":{\"selector\":{\"version\":\"${NEW_COLOR}\"}}}"# 7. 验证echo"验证切换结果..."sleep10ERROR_RATE=$(curl-s"http://prometheus:9090/api/v1/query?query=rate(http_requests_total{app=\"${APP_NAME}\",status=~\"5..\"}[1m])")echo"当前错误率:${ERROR_RATE}"echo"========================================="echo" 蓝绿部署完成:${APP_NAME}${VERSION}"echo" 旧版本保留在${ACTIVE},可用于回滚"echo"========================================="

4.2 回滚脚本

#!/bin/bash# 一键回滚APP_NAME=$1echo"=== 紧急回滚${APP_NAME}==="# 获取当前版本CURRENT=$(kubectl get svc ${APP_NAME}-ojsonpath='{.spec.selector.version}')# 切回上一个版本if["$CURRENT"=="blue"];thenkubectl patchservice${APP_NAME}-p'{"spec":{"selector":{"version":"green"}}}'elsekubectl patchservice${APP_NAME}-p'{"spec":{"selector":{"version":"blue"}}}'fiecho"已切回上一个版本,请检查监控"

五、蓝绿部署的进阶技巧

5.1 数据库迁移独立于发布

时间线: T0: 执行数据库变更脚本(只新增字段,不删除) T1: 部署Green环境V2版本 T2: 切换流量到Green T3: 观察30分钟,确认无问题 T4: 执行数据库清理脚本(删除旧字段)← 此时已确认不回滚

5.2 蓝绿 + 金丝雀组合

蓝绿部署也可以结合金丝雀策略:

先切10%流量到Green → 观察 → 再全量切换 实现: Step 1: Nginx将10%流量路由到Green upstream Step 2: 观察15分钟 Step 3: 将100%流量切到Green

5.3 基础设施即代码

# Terraform 管理蓝绿环境resource "kubernetes_namespace" "blue"{metadata{name = "${var.app_name}-blue"}}resource "kubernetes_namespace" "green"{metadata{name = "${var.app_name}-green"}}# 每次部署,在备用环境创建完整副本resource "kubernetes_deployment" "standby"{metadata{name = var.app_name namespace = var.target_namespace# blue 或 green}# ... 部署配置}

六、总结

蓝绿部署的核心价值

  1. 零停机:流量切换而非服务重启,用户无感知
  2. 秒级回滚:出问题切回旧环境,比重新部署快100倍
  3. 充分验证:新环境可以充分测试后再切流量
  4. 操作简单:不需要复杂的灰度规则,切换一个路由即可

蓝绿部署的代价

  • 需要两倍硬件资源(或至少1.5倍)
  • 数据库Schema要兼容新老版本
  • 缓存Key要版本化隔离
  • 需要自动化的部署和切换工具

适用场景

  • 重大版本升级(不兼容变更)
  • 需要快速回滚的核心服务
  • 有充足资源预算的团队

不适用场景

  • 资源紧张的小团队
  • 有状态服务(数据库、消息队列)
  • 数据库变更频繁且无法保证兼容性

蓝绿部署不是银弹,但它是保障发布安全的最后一道防线。如果你只能选一种发布策略来保护核心服务,就选蓝绿部署。


个人观点,仅供参考