一、一次全量发布差点搞垮公司
2020年,我们上线了一个新的支付模块,直接全量发布。结果10分钟后开始出现"双重扣款"的bug。
回滚花了7分钟,那7分钟又产生了几十笔问题订单。
从那以后,"灰度发布"成了铁律。
二、灰度发布策略
2.1 策略类型
┌─────────────────────────────────────────────────────────────────┐
│ 灰度发布策略 │
│ │
│ 1. 按比例灰度 │
│ - 1% → 5% → 10% → 50% → 100% │
│ - 适合一般业务发布 │
│ │
│ 2. 按用户灰度 │
│ - 内部用户 → 白名单用户 → 全量 │
│ - 适合有明确用户分组的业务 │
│ │
│ 3. 按地域灰度 │
│ - 杭州 → 上海 → 北京 → 全国 │
│ - 适合地域性强的业务 │
│ │
│ 4. 按功能开关 │
│ - 新功能默认关闭,逐步开启 │
│ - 适合新功能上线 │
│ │
│ 5. A/B测试 │
│ - 两套方案同时运行,对比效果 │
│ - 适合产品实验 │
│ │
└──────────────────────────────────────────────────────────────────┘
三、Kubernetes灰度发布
3.1 基于Weight的灰度
yaml
# 稳定版本
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
version: stable
ports:
- port: 8080
---
# 灰度版本
apiVersion: v1
kind: Service
metadata:
name: order-service-canary
spec:
selector:
app: order-service
version: canary
ports:
- port: 8080
---
# Istio VirtualService - 10%流量到灰度
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- route:
- destination:
host: order-service
subset: stable
weight: 90
- destination:
host: order-service
subset: canary
weight: 10
3.2 灰度发布脚本
java
/**
* 灰度发布服务
*/
@Service
@Slf4j
public class CanaryDeployService {
@Autowired
private KubernetesClient k8sClient;
@Autowired
private MonitoringService monitoringService;
/**
* 执行灰度发布
*/
public void canaryDeploy(String service, String newImage,
CanaryStrategy strategy) {
log.info("开始灰度发布: service={}, newImage={}", service, newImage);
// 1. 部署灰度版本
deployCanary(service, newImage, strategy.getInitialWeight());
// 2. 等待灰度版本就绪
waitForReady(service, "canary");
// 3. 观察指标
observeMetrics(service, strategy);
log.info("灰度发布完成: service={}", service);
}
/**
* 观察灰度指标
*/
private void observeMetrics(String service, CanaryStrategy strategy) {
int currentWeight = strategy.getInitialWeight();
while (currentWeight < 100) {
// 等待观察期
sleep(strategy.getObserveDuration());
// 检查灰度版本指标
Metrics metrics = monitoringService.getServiceMetrics(service, "canary");
if (metrics.getErrorRate() > strategy.getMaxErrorRate()) {
log.error("灰度版本错误率过高: {}%", metrics.getErrorRate());
rollback(service);
throw new RuntimeException("灰度发布失败,已回滚");
}
if (metrics.getP99Latency() > strategy.getMaxP99Ms()) {
log.error("灰度版本延迟过高: {}ms", metrics.getP99Latency());
rollback(service);
throw new RuntimeException("灰度发布失败,已回滚");
}
// 指标正常,增加灰度比例
currentWeight = Math.min(100, currentWeight * 2);
adjustWeight(service, currentWeight);
log.info("灰度比例调整: {}%", currentWeight);
}
}
/**
* 回滚
*/
public void rollback(String service) {
// 将所有流量切回稳定版本
adjustWeight(service, 0);
// 删除灰度版本
deleteCanary(service);
log.warn("灰度发布已回滚: service={}", service);
}
}
四、踩坑实录
坑1:灰度比例跳跃太大
直接从5%跳到50%,出了问题影响面太大。
解决:灰度比例倍增(5→10→20→40→80→100),每步观察。
坑2:灰度期间没有监控
灰度版本出了问题但没发现,全量后才知道。
解决:灰度期间对比新旧版本的关键指标。
坑3:灰度版本和数据库不兼容
新版本的数据库Schema和旧版本不兼容。
解决:数据库变更要向前兼容,先加字段后删字段。
坑4:灰度版本影响缓存
新旧版本写入的缓存格式不同,互相覆盖。
解决:缓存Key加版本号,或保证向后兼容。
坑5:回滚不彻底
回滚了应用但没回滚配置,导致功能异常。
解决:回滚检查清单,应用+配置+数据一起回滚。
五、总结
灰度发布最佳实践:
| 原则 | 说明 |
|---|---|
| 小步快跑 | 1%→5%→10%→50%→100% |
| 对比监控 | 新旧版本指标对比 |
| 自动回滚 | 错误率超阈值自动回滚 |
| 向前兼容 | 数据库变更要兼容 |
| 完整回滚 | 应用+配置+数据一起回滚 |
血的教训:
每一次全量发布都是在赌命。灰度发布不是浪费时间,是买保险。
思考题: 你的团队是怎么做灰度发布的?
个人观点,仅供参考