54-灰度发布详解

6 阅读10分钟

灰度发布详解

一、知识概述

灰度发布(Canary Release)是一种降低发布风险的发布策略,通过逐步放量的方式,先让小部分用户使用新版本,观察无问题后再逐步扩大范围,最终完成全量发布。

核心目标:

  • 降低发布风险
  • 快速发现问题
  • 平滑过渡
  • 支持快速回滚

对比传统发布:

发布方式风险回滚速度用户影响
直接发布全量
蓝绿发布瞬间切换
灰度发布渐进式

二、知识点详细讲解

2.1 灰度发布策略

2.1.1 基于权重的灰度
┌─────────────────────────────────────────────────────┐
│              基于权重的流量切分                      │
├─────────────────────────────────────────────────────┤
│                                                     │
│                 ┌─────────┐                        │
│                 │  用户   │                        │
│                 └────┬────┘                        │
│                      │                             │
│              ┌───────┴───────┐                     │
│              │   负载均衡    │                     │
│              └───────┬───────┘                     │
│                      │                             │
│         ┌────────────┼────────────┐               │
│         ↓            ↓            ↓               │
│    ┌─────────┐  ┌─────────┐  ┌─────────┐        │
│    │ 旧版本  │  │ 灰度版本│  │ 旧版本  │        │
│    │   90%   │  │   10%   │  │         │        │
│    └─────────┘  └─────────┘  └─────────┘        │
│                                                     │
│    灰度步骤:10% → 30% → 50% → 100%              │
└─────────────────────────────────────────────────────┘
/**
 * 权重路由服务
 */
@Service
public class WeightRoutingService {
    
    // 灰度配置
    private volatile GrayConfig grayConfig = GrayConfig.builder()
        .enabled(true)
        .grayWeight(10)  // 10%流量
        .grayVersion("v2.0.0")
        .stableVersion("v1.0.0")
        .build();
    
    /**
     * 根据权重决定路由
     */
    public String route(Long userId) {
        if (!grayConfig.isEnabled()) {
            return grayConfig.getStableVersion();
        }
        
        // 基于用户ID哈希 + 权重判断
        int hash = Math.abs(userId.hashCode() % 100);
        
        if (hash < grayConfig.getGrayWeight()) {
            return grayConfig.getGrayVersion();
        }
        
        return grayConfig.getStableVersion();
    }
    
    /**
     * 更新灰度权重
     */
    public void updateWeight(int newWeight) {
        if (newWeight < 0 || newWeight > 100) {
            throw new IllegalArgumentException("权重必须在0-100之间");
        }
        
        grayConfig = GrayConfig.builder()
            .enabled(grayConfig.isEnabled())
            .grayWeight(newWeight)
            .grayVersion(grayConfig.getGrayVersion())
            .stableVersion(grayConfig.getStableVersion())
            .build();
        
        log.info("灰度权重更新: {}%", newWeight);
    }
}
2.1.2 基于规则的灰度
┌─────────────────────────────────────────────────────┐
│              基于规则的流量切分                      │
├─────────────────────────────────────────────────────┤
│                                                     │
│   规则类型:                                        │
│   ┌─────────────────────────────────────────────┐ │
│   │ 1. 用户ID白名单                             │ │
│   │    - 内部员工                               │ │
│   │    - 种子用户                               │ │
│   │                                             │ │
│   │ 2. 用户标签                                 │ │
│   │    - VIP用户                                │ │
│   │    - 新注册用户                             │ │
│   │                                             │ │
│   │ 3. 地理位置                                 │ │
│   │    - 特定城市                               │ │
│   │    - 海外用户                               │ │
│   │                                             │ │
│   │ 4. 设备类型                                 │ │
│   │    - iOS / Android                          │ │
│   │    - 新版App                                │ │
│   └─────────────────────────────────────────────┘ │
│                                                     │
└─────────────────────────────────────────────────────┘
/**
 * 规则路由服务
 */
@Service
public class RuleRoutingService {
    
    @Autowired
    private UserService userService;
    
    // 灰度规则配置
    private volatile List<GrayRule> grayRules = Arrays.asList(
        GrayRule.builder()
            .type(RuleType.USER_ID)
            .values(Arrays.asList("10001", "10002", "10003"))  // 白名单
            .version("v2.0.0")
            .build(),
        GrayRule.builder()
            .type(RuleType.USER_TAG)
            .values(Arrays.asList("VIP", "INTERNAL"))
            .version("v2.0.0")
            .build(),
        GrayRule.builder()
            .type(RuleType.CITY)
            .values(Arrays.asList("北京", "上海"))
            .version("v2.0.0")
            .build()
    );
    
    /**
     * 根据规则决定路由
     */
    public String route(Long userId, String city, String deviceType) {
        User user = userService.getById(userId);
        
        // 遍历规则,按优先级匹配
        for (GrayRule rule : grayRules) {
            if (matchRule(rule, user, city, deviceType)) {
                log.info("用户命中灰度规则: userId={}, rule={}", userId, rule);
                return rule.getVersion();
            }
        }
        
        // 默认走稳定版本
        return "v1.0.0";
    }
    
    /**
     * 匹配规则
     */
    private boolean matchRule(GrayRule rule, User user, String city, String deviceType) {
        switch (rule.getType()) {
            case USER_ID:
                return rule.getValues().contains(user.getId().toString());
                
            case USER_TAG:
                return user.getTags().stream()
                    .anyMatch(rule.getValues()::contains);
                
            case CITY:
                return rule.getValues().contains(city);
                
            case DEVICE_TYPE:
                return rule.getValues().contains(deviceType);
                
            default:
                return false;
        }
    }
}

2.2 灰度发布流程

2.2.1 完整发布流程
┌────────────────────────────────────────────────────────┐
│                  灰度发布流程                          │
├────────────────────────────────────────────────────────┤
│                                                        │
│   1. 准备阶段                                         │
│      ├── 代码审查                                     │
│      ├── 自动化测试                                   │
│      ├── 性能测试                                     │
│      └── 制定回滚计划                                 │
│                                                        │
│   2. 部署阶段                                         │
│      ├── 部署灰度环境                                 │
│      ├── 配置灰度规则                                 │
│      └── 开始灰度(1%)                               │
│                                                        │
│   3. 观察阶段                                         │
│      ├── 监控错误率                                   │
│      ├── 监控性能指标                                 │
│      ├── 收集用户反馈                                 │
│      └── 逐步扩大(1%→5%→10%→30%→50%→100%)         │
│                                                        │
│   4. 完成/回滚                                        │
│      ├── 无异常 → 全量发布                           │
│      └── 有问题 → 快速回滚                           │
│                                                        │
└────────────────────────────────────────────────────────┘
2.2.2 灰度控制器
/**
 * 灰度发布控制器
 */
@RestController
@RequestMapping("/gray")
public class GrayReleaseController {
    
    @Autowired
    private GrayReleaseService grayReleaseService;
    
    @Autowired
    private MonitoringService monitoringService;
    
    /**
     * 创建灰度发布任务
     */
    @PostMapping("/create")
    public Result<GrayTask> createTask(@RequestBody GrayTaskRequest request) {
        GrayTask task = grayReleaseService.createTask(request);
        return Result.success(task);
    }
    
    /**
     * 开始灰度
     */
    @PostMapping("/start/{taskId}")
    public Result<Void> startGray(@PathVariable Long taskId) {
        grayReleaseService.startGray(taskId);
        return Result.success();
    }
    
    /**
     * 调整灰度比例
     */
    @PostMapping("/adjust/{taskId}")
    public Result<Void> adjustWeight(
            @PathVariable Long taskId,
            @RequestParam int weight) {
        grayReleaseService.adjustWeight(taskId, weight);
        return Result.success();
    }
    
    /**
     * 全量发布
     */
    @PostMapping("/complete/{taskId}")
    public Result<Void> complete(@PathVariable Long taskId) {
        grayReleaseService.completeGray(taskId);
        return Result.success();
    }
    
    /**
     * 回滚
     */
    @PostMapping("/rollback/{taskId}")
    public Result<Void> rollback(@PathVariable Long taskId) {
        grayReleaseService.rollback(taskId);
        return Result.success();
    }
    
    /**
     * 灰度状态
     */
    @GetMapping("/status/{taskId}")
    public Result<GrayStatus> getStatus(@PathVariable Long taskId) {
        GrayStatus status = grayReleaseService.getStatus(taskId);
        return Result.success(status);
    }
}

/**
 * 灰度发布服务
 */
@Service
@Slf4j
public class GrayReleaseService {
    
    @Autowired
    private WeightRoutingService routingService;
    
    @Autowired
    private MonitoringService monitoringService;
    
    @Autowired
    private AlertService alertService;
    
    /**
     * 创建灰度任务
     */
    public GrayTask createTask(GrayTaskRequest request) {
        GrayTask task = GrayTask.builder()
            .id(snowflakeId.nextId())
            .serviceName(request.getServiceName())
            .grayVersion(request.getGrayVersion())
            .stableVersion(request.getStableVersion())
            .currentWeight(0)
            .status(GrayTaskStatus.PENDING)
            .createTime(new Date())
            .build();
        
        taskMapper.insert(task);
        
        log.info("创建灰度任务: {}", task);
        return task;
    }
    
    /**
     * 开始灰度
     */
    public void startGray(Long taskId) {
        GrayTask task = getTask(taskId);
        
        // 更新状态
        task.setStatus(GrayTaskStatus.RUNNING);
        task.setCurrentWeight(1);  // 从1%开始
        taskMapper.updateById(task);
        
        // 配置路由
        routingService.updateWeight(1);
        
        // 启动监控
        startMonitoring(task);
        
        log.info("开始灰度: taskId={}, weight=1%", taskId);
    }
    
    /**
     * 调整灰度比例
     */
    public void adjustWeight(Long taskId, int weight) {
        GrayTask task = getTask(taskId);
        
        // 检查当前指标
        if (!checkMetrics(task)) {
            log.warn("指标异常,拒绝调整: taskId={}", taskId);
            throw new RuntimeException("指标异常,请先处理问题");
        }
        
        // 更新权重
        task.setCurrentWeight(weight);
        taskMapper.updateById(task);
        
        // 更新路由
        routingService.updateWeight(weight);
        
        log.info("调整灰度比例: taskId={}, weight={}%", taskId, weight);
    }
    
    /**
     * 全量发布
     */
    public void completeGray(Long taskId) {
        GrayTask task = getTask(taskId);
        
        // 设置100%流量
        task.setCurrentWeight(100);
        task.setStatus(GrayTaskStatus.COMPLETED);
        task.setCompleteTime(new Date());
        taskMapper.updateById(task);
        
        // 更新路由
        routingService.updateWeight(100);
        
        // 旧版本下线
        shutdownOldVersion(task.getStableVersion());
        
        log.info("灰度完成: taskId={}", taskId);
    }
    
    /**
     * 回滚
     */
    public void rollback(Long taskId) {
        GrayTask task = getTask(taskId);
        
        // 切回旧版本
        task.setCurrentWeight(0);
        task.setStatus(GrayTaskStatus.ROLLBACK);
        task.setRollbackTime(new Date());
        taskMapper.updateById(task);
        
        // 更新路由
        routingService.updateWeight(0);
        
        // 灰度版本下线
        shutdownGrayVersion(task.getGrayVersion());
        
        log.warn("灰度回滚: taskId={}", taskId);
        alertService.sendAlert("灰度发布回滚: " + task.getServiceName());
    }
    
    /**
     * 检查指标
     */
    private boolean checkMetrics(GrayTask task) {
        // 错误率
        double errorRate = monitoringService.getErrorRate(task.getGrayVersion());
        if (errorRate > 0.01) {  // 错误率>1%
            log.warn("错误率过高: {}%", errorRate * 100);
            return false;
        }
        
        // 响应时间
        double p99Latency = monitoringService.getP99Latency(task.getGrayVersion());
        double baseP99Latency = monitoringService.getP99Latency(task.getStableVersion());
        
        if (p99Latency > baseP99Latency * 1.5) {  // P99延迟超过基线50%
            log.warn("性能下降: p99={}ms, baseline={}ms", p99Latency, baseP99Latency);
            return false;
        }
        
        return true;
    }
    
    /**
     * 自动推进灰度
     */
    @Scheduled(fixedDelay = 60000)  // 每分钟检查
    public void autoProgress() {
        List<GrayTask> runningTasks = taskMapper.selectByStatus(GrayTaskStatus.RUNNING);
        
        for (GrayTask task : runningTasks) {
            try {
                if (checkMetrics(task)) {
                    // 指标正常,自动推进
                    int nextWeight = getNextWeight(task.getCurrentWeight());
                    
                    if (nextWeight >= 100) {
                        completeGray(task.getId());
                    } else {
                        adjustWeight(task.getId(), nextWeight);
                    }
                } else {
                    // 指标异常,暂停并告警
                    task.setStatus(GrayTaskStatus.PAUSED);
                    taskMapper.updateById(task);
                    
                    alertService.sendAlert("灰度发布暂停: " + task.getServiceName());
                }
                
            } catch (Exception e) {
                log.error("自动推进失败: taskId={}", task.getId(), e);
            }
        }
    }
    
    /**
     * 计算下一个权重
     */
    private int getNextWeight(int currentWeight) {
        // 权重递进策略: 1% → 5% → 10% → 30% → 50% → 100%
        int[] steps = {1, 5, 10, 30, 50, 100};
        
        for (int step : steps) {
            if (currentWeight < step) {
                return step;
            }
        }
        
        return 100;
    }
}

2.3 蓝绿部署

2.3.1 蓝绿部署架构
┌─────────────────────────────────────────────────────┐
│                 蓝绿部署架构                        │
├─────────────────────────────────────────────────────┤
│                                                     │
│              ┌─────────────┐                       │
│              │   负载均衡  │                       │
│              └──────┬──────┘                       │
│                     │                              │
│         ┌───────────┴───────────┐                 │
│         │                       │                 │
│    ┌────▼────┐            ┌────▼────┐            │
│    │  Blue   │            │  Green  │            │
│    │ (旧版本)│            │ (新版本)│            │
│    │ Active  │            │ Standby │            │
│    └─────────┘            └─────────┘            │
│                                                     │
│    发布流程:                                       │
│    1. Green部署新版本                              │
│    2. Green环境测试                                │
│    3. 切换流量:Blue → Green                       │
│    4. Blue保留作为回滚备份                         │
│                                                     │
└─────────────────────────────────────────────────────┘
/**
 * 蓝绿部署服务
 */
@Service
@Slf4j
public class BlueGreenDeployService {
    
    @Autowired
    private KubernetesClient k8sClient;
    
    @Autowired
    private LoadBalancerService loadBalancer;
    
    /**
     * 蓝绿部署
     */
    public void deploy(BlueGreenDeployRequest request) {
        String serviceName = request.getServiceName();
        String newVersion = request.getNewVersion();
        
        // 1. 确定当前活跃环境
        String activeEnv = getActiveEnvironment(serviceName);
        String standbyEnv = "blue".equals(activeEnv) ? "green" : "blue";
        
        log.info("当前活跃环境: {}, 待部署环境: {}", activeEnv, standbyEnv);
        
        // 2. 在Standby环境部署新版本
        deployToEnvironment(serviceName, standbyEnv, newVersion);
        
        // 3. 等待部署完成
        waitForDeployment(serviceName, standbyEnv);
        
        // 4. 健康检查
        if (!healthCheck(serviceName, standbyEnv)) {
            log.error("健康检查失败,取消部署");
            throw new RuntimeException("健康检查失败");
        }
        
        // 5. 切换流量
        switchTraffic(serviceName, activeEnv, standbyEnv);
        
        // 6. 验证服务
        if (!verifyService(serviceName)) {
            log.error("服务验证失败,执行回滚");
            switchTraffic(serviceName, standbyEnv, activeEnv);
            throw new RuntimeException("服务验证失败,已回滚");
        }
        
        // 7. 更新环境标签
        updateEnvironmentLabel(serviceName, standbyEnv, "active");
        updateEnvironmentLabel(serviceName, activeEnv, "standby");
        
        log.info("蓝绿部署完成: {} -> {}", activeEnv, standbyEnv);
    }
    
    /**
     * 获取当前活跃环境
     */
    private String getActiveEnvironment(String serviceName) {
        Deployment deployment = k8sClient.apps().deployments()
            .withLabel("app", serviceName)
            .withLabel("status", "active")
            .list()
            .getItems()
            .stream()
            .findFirst()
            .orElseThrow(() -> new RuntimeException("找不到活跃部署"));
        
        return deployment.getMetadata().getLabels().get("environment");
    }
    
    /**
     * 切换流量
     */
    private void switchTraffic(String serviceName, String fromEnv, String toEnv) {
        // 更新Service的Selector
        k8sClient.services()
            .withName(serviceName)
            .edit(s -> new ServiceBuilder(s)
                .editSpec()
                .withSelector(Map.of(
                    "app", serviceName,
                    "environment", toEnv
                ))
                .endSpec()
                .build());
        
        // 等待流量切换完成
        waitForTrafficSwitch(serviceName);
        
        log.info("流量切换完成: {} -> {}", fromEnv, toEnv);
    }
}

2.4 金丝雀发布

2.4.1 金丝雀发布特点
┌─────────────────────────────────────────────────────┐
│               金丝雀发布 vs 灰度发布                 │
├─────────────────────────────────────────────────────┤
│                                                     │
│   金丝雀发布:                                      │
│   - 关注指标监控                                   │
│   - 自动化决策                                     │
│   - 快速失败                                       │
│                                                     │
│   ┌───────────────────────────────────────────┐   │
│   │  生产环境                                 │   │
│   │  ┌──────┐  ┌──────┐  ┌──────┐           │   │
│   │  │ v1.0 │  │ v1.0 │  │ v2.0 │           │   │
│   │  │ 95%  │  │ 95%  │  │ 5%   │           │   │
│   │  └──────┘  └──────┘  └──────┘           │   │
│   │       ↑                  ↑               │   │
│   │       │                  │               │   │
│   │   基线指标          金丝雀指标            │   │
│   │   (对比)              (对比)             │   │
│   └───────────────────────────────────────────┘   │
│                                                     │
│   判断标准:                                        │
│   - 错误率对比                                     │
│   - 性能对比                                       │
│   - 业务指标对比                                   │
│                                                     │
└─────────────────────────────────────────────────────┘
/**
 * 金丝雀发布服务
 */
@Service
@Slf4j
public class CanaryDeployService {
    
    @Autowired
    private MonitoringService monitoringService;
    
    @Autowired
    private AlertService alertService;
    
    // 金丝雀配置
    private CanaryConfig config = CanaryConfig.builder()
        .thresholds(CanaryThresholds.builder()
            .maxErrorRateIncrease(0.5)      // 错误率最多增加50%
            .maxLatencyIncrease(0.2)        // 延迟最多增加20%
            .minSuccessRate(0.99)           // 成功率至少99%
            .build())
        .steps(Arrays.asList(1, 5, 10, 25, 50, 100))  // 灰度步骤
        .pauseBetweenSteps(Duration.ofMinutes(5))     // 步骤间隔
        .build();
    
    /**
     * 执行金丝雀发布
     */
    public void canaryDeploy(String serviceName, String newVersion) {
        log.info("开始金丝雀发布: {} -> {}", serviceName, newVersion);
        
        for (int step : config.getSteps()) {
            log.info("金丝雀步骤: {}%", step);
            
            // 设置流量比例
            setTrafficWeight(serviceName, step);
            
            // 等待观察期
            sleep(config.getPauseBetweenSteps());
            
            // 分析指标
            CanaryAnalysisResult result = analyzeCanary(serviceName, newVersion);
            
            if (!result.isPass()) {
                log.error("金丝雀分析失败: {}", result.getReason());
                rollback(serviceName, newVersion);
                
                alertService.sendAlert(String.format(
                    "金丝雀发布失败: %s, 原因: %s", serviceName, result.getReason()));
                
                return;
            }
            
            log.info("金丝雀分析通过: {}", result.getSummary());
        }
        
        log.info("金丝雀发布完成: {}", serviceName);
    }
    
    /**
     * 分析金丝雀指标
     */
    private CanaryAnalysisResult analyzeCanary(String serviceName, String newVersion) {
        CanaryMetrics baseline = getBaselineMetrics(serviceName);
        CanaryMetrics canary = getCanaryMetrics(serviceName, newVersion);
        
        // 检查错误率
        double errorRateIncrease = (canary.getErrorRate() - baseline.getErrorRate()) 
            / baseline.getErrorRate();
        
        if (errorRateIncrease > config.getThresholds().getMaxErrorRateIncrease()) {
            return CanaryAnalysisResult.fail(
                String.format("错误率增加过多: %.2f%%", errorRateIncrease * 100));
        }
        
        // 检查延迟
        double latencyIncrease = (canary.getP99Latency() - baseline.getP99Latency()) 
            / baseline.getP99Latency();
        
        if (latencyIncrease > config.getThresholds().getMaxLatencyIncrease()) {
            return CanaryAnalysisResult.fail(
                String.format("延迟增加过多: %.2f%%", latencyIncrease * 100));
        }
        
        // 检查成功率
        if (canary.getSuccessRate() < config.getThresholds().getMinSuccessRate()) {
            return CanaryAnalysisResult.fail(
                String.format("成功率过低: %.2f%%", canary.getSuccessRate() * 100));
        }
        
        return CanaryAnalysisResult.pass(String.format(
            "错误率: %.2f%%, 延迟: %.0fms, 成功率: %.2f%%",
            canary.getErrorRate() * 100,
            canary.getP99Latency(),
            canary.getSuccessRate() * 100));
    }
}

三、可运行Java代码示例

3.1 灰度网关过滤器

/**
 * 灰度路由网关过滤器
 */
@Component
public class GrayRoutingGatewayFilter implements GlobalFilter, Ordered {
    
    @Autowired
    private WeightRoutingService routingService;
    
    @Autowired
    private RuleRoutingService ruleRoutingService;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        
        // 获取用户信息
        Long userId = extractUserId(request);
        String city = extractCity(request);
        String deviceType = extractDeviceType(request);
        
        // 决定路由版本
        String version = decideVersion(userId, city, deviceType);
        
        // 添加版本头
        ServerHttpRequest newRequest = request.mutate()
            .header("X-Service-Version", version)
            .header("X-User-Id", userId.toString())
            .build();
        
        // 记录灰度日志
        log.info("灰度路由: userId={}, version={}", userId, version);
        
        return chain.filter(exchange.mutate().request(newRequest).build());
    }
    
    /**
     * 决定版本
     */
    private String decideVersion(Long userId, String city, String deviceType) {
        // 优先匹配规则
        String ruleVersion = ruleRoutingService.route(userId, city, deviceType);
        if (ruleVersion != null) {
            return ruleVersion;
        }
        
        // 使用权重路由
        return routingService.route(userId);
    }
    
    @Override
    public int getOrder() {
        return -100;  // 高优先级
    }
}

3.2 灰度监控大盘

/**
 * 灰度监控服务
 */
@Service
public class GrayMonitoringService {
    
    private final MeterRegistry meterRegistry;
    
    // 计数器
    private final Counter grayRequestCounter;
    private final Counter grayErrorCounter;
    private final Counter stableRequestCounter;
    private final Counter stableErrorCounter;
    
    public GrayMonitoringService(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        
        this.grayRequestCounter = Counter.builder("gray.request")
            .tag("version", "gray")
            .register(meterRegistry);
        
        this.grayErrorCounter = Counter.builder("gray.error")
            .tag("version", "gray")
            .register(meterRegistry);
        
        this.stableRequestCounter = Counter.builder("gray.request")
            .tag("version", "stable")
            .register(meterRegistry);
        
        this.stableErrorCounter = Counter.builder("gray.error")
            .tag("version", "stable")
            .register(meterRegistry);
    }
    
    /**
     * 记录请求
     */
    public void recordRequest(String version, boolean success) {
        if ("gray".equals(version)) {
            grayRequestCounter.increment();
            if (!success) {
                grayErrorCounter.increment();
            }
        } else {
            stableRequestCounter.increment();
            if (!success) {
                stableErrorCounter.increment();
            }
        }
    }
    
    /**
     * 获取灰度报告
     */
    public GrayReport getReport() {
        double grayRequests = grayRequestCounter.count();
        double grayErrors = grayErrorCounter.count();
        double stableRequests = stableRequestCounter.count();
        double stableErrors = stableErrorCounter.count();
        
        double grayErrorRate = grayRequests > 0 ? grayErrors / grayRequests : 0;
        double stableErrorRate = stableRequests > 0 ? stableErrors / stableRequests : 0;
        
        return GrayReport.builder()
            .grayRequests((long) grayRequests)
            .grayErrors((long) grayErrors)
            .grayErrorRate(grayErrorRate)
            .stableRequests((long) stableRequests)
            .stableErrors((long) stableErrors)
            .stableErrorRate(stableErrorRate)
            .errorRateDiff(grayErrorRate - stableErrorRate)
            .build();
    }
}

四、实战应用场景

4.1 微服务灰度

场景: 订单服务升级,需要灰度验证

方案:

# Nacos灰度配置
spring:
  cloud:
    nacos:
      discovery:
        metadata:
          version: v2.0.0    # 服务版本
          gray: true         # 灰度标识

# 网关灰度路由
spring:
  cloud:
    gateway:
      routes:
        - id: order-service-gray
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
            - Header=X-User-Id, 10001  # 白名单用户
          metadata:
            version: v2.0.0

4.2 前端灰度

场景: 新版首页上线

方案:

// 前端灰度SDK
class GraySDK {
    constructor() {
        this.config = null;
    }
    
    async init(userId) {
        // 获取灰度配置
        const response = await fetch('/api/gray/config?userId=' + userId);
        this.config = await response.json();
    }
    
    getVersion(feature) {
        if (!this.config) return 'stable';
        
        // 检查是否命中灰度
        if (this.config.features[feature] === 'gray') {
            return 'gray';
        }
        
        return 'stable';
    }
    
    // 加载对应版本
    loadPage(pageName) {
        const version = this.getVersion(pageName);
        
        if (version === 'gray') {
            return import('./pages/' + pageName + '-v2.js');
        } else {
            return import('./pages/' + pageName + '.js');
        }
    }
}

五、总结与最佳实践

5.1 发布策略选择

策略适用场景风险复杂度
直接发布非关键服务
蓝绿部署快速回滚需求
灰度发布大流量服务
金丝雀发布关键服务最低

5.2 灰度发布清单

发布前:

  • ✅ 代码审查完成
  • ✅ 自动化测试通过
  • ✅ 性能测试通过
  • ✅ 回滚方案准备

发布中:

  • ✅ 灰度规则配置
  • ✅ 监控告警配置
  • ✅ 日志采集就绪
  • ✅ 值班人员待命

发布后:

  • ✅ 指标对比正常
  • ✅ 用户反馈收集
  • ✅ 文档更新完成
  • ✅ 经验总结归档

5.3 常见问题

问题原因解决方案
灰度不生效缓存/配置未更新强制刷新+验证
流量不均匀哈希算法问题优化哈希策略
回滚失败数据库变更数据库回滚脚本
监控延迟指标聚合慢实时监控方案

六、思考与练习

思考题

  1. 基础题:灰度发布、蓝绿部署、金丝雀发布有什么区别?它们分别适合什么场景?

  2. 进阶题:在金丝雀发布中,如何确定新版本可以继续推进?应该监控哪些指标?如果发现新版本性能下降但未出错,应该如何处理?

  3. 实战题:某微服务系统包含订单服务、支付服务、库存服务三个核心服务。现在需要对订单服务进行重大重构升级,请设计完整的灰度发布方案,包括灰度规则、监控策略、回滚预案。

编程练习

练习:实现灰度网关路由功能

  • 支持基于用户ID白名单的灰度规则
  • 支持基于权重的流量切分(如10%流量走新版本)
  • 实现灰度配置的动态更新(无需重启)
  • 记录每次请求路由到的版本,便于后续分析
  • 提供灰度状态查询接口

要求:实现一个简单的网关过滤器,支持灰度路由功能。

章节关联

  • 前置章节:限流熔断详解
  • 后续章节:分布式系统设计
  • 扩展阅读:《Continuous Delivery》、Spinnaker官方文档、Istio流量管理

📝 下一章预告

至此,系统设计篇章已全部完成。接下来的内容将进入分布式系统设计领域,探讨分布式事务、分布式锁、分布式ID等核心问题。


本章完


参考资料

  • 《Continuous Delivery》
  • 《Kubernetes Patterns》
  • Spinnaker官方文档
  • Istio流量管理