Java灰度案例

wen java案例 2

本文目录导读:

Java灰度案例

  1. 灰度发布核心概念
  2. 完整实现案例
  3. 使用方式
  4. 最佳实践建议

我来为您提供一个Java灰度发布(Gray Release) 的完整案例,包含多种常见实现方案。

灰度发布核心概念

灰度发布是指通过控制流量逐渐引入新版本,从少量用户开始测试,观察无误后再扩大范围,最终实现全量发布的过程。

完整实现案例

1 基础灰度路由方案(基于用户ID)

import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.client.RestTemplate;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.time.LocalTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
 * 灰度发布控制器 - 基于用户ID的灰度路由
 */
@RestController
@RequestMapping("/api")
public class GrayReleaseController {
    @Autowired
    private GrayReleaseService grayReleaseService;
    @Autowired
    private V1Service v1Service;
    @Autowired
    private V2Service v2Service;
    /**
     * 灰度接口示例
     */
    @GetMapping("/greet")
    public String greet(@RequestHeader(value = "X-User-ID", required = false) String userId) {
        // 判断是否命中灰度
        if (grayReleaseService.isGrayUser(userId)) {
            return v2Service.greetV2(userId);
        } else {
            return v1Service.greetV1(userId);
        }
    }
    /**
     * 更新灰度配置
     */
    @PostMapping("/gray/config")
    public String updateGrayConfig(@RequestBody GrayConfig config) {
        grayReleaseService.updateConfig(config);
        return "灰度配置更新成功";
    }
}

2 灰度配置管理服务

import org.springframework.stereotype.Service;
import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class GrayReleaseService {
    // 灰度配置存储(生产环境建议用Redis/配置中心)
    private final Map<String, GrayConfig> grayConfigMap = new ConcurrentHashMap<>();
    // 已灰度用户集合
    private final Set<String> grayUsers = ConcurrentHashMap.newKeySet();
    // 灰度流量计数器
    private final AtomicInteger grayRequestCount = new AtomicInteger(0);
    // Redis模板(可选)
    @Autowired(required = false)
    private RedisTemplate<String, Object> redisTemplate;
    /**
     * 初始化默认灰度配置
     */
    @PostConstruct
    public void init() {
        // 默认灰度比例为10%
        GrayConfig defaultConfig = new GrayConfig();
        defaultConfig.setServiceName("user-service");
        defaultConfig.setGrayRatio(10);
        defaultConfig.setEnabled(true);
        defaultConfig.setVersion("v2");
        defaultConfig.setDescription("默认灰度配置");
        grayConfigMap.put(defaultConfig.getServiceName(), defaultConfig);
        // 预置一些灰度用户
        grayUsers.addAll(Arrays.asList("1001", "1002", "1003", "1004", "1005"));
    }
    /**
     * 判断用户是否为灰度用户
     */
    public boolean isGrayUser(String userId) {
        if (userId == null || userId.isEmpty()) {
            return false;
        }
        // 方式1:基于用户ID白名单
        if (grayUsers.contains(userId)) {
            return true;
        }
        // 方式2:基于比例灰度
        GrayConfig config = grayConfigMap.get("user-service");
        if (config == null || !config.isEnabled()) {
            return false;
        }
        // 根据用户ID哈希取模
        int hash = Math.abs(userId.hashCode()) % 100;
        return hash < config.getGrayRatio();
    }
    /**
     * 基于流量比例灰度
     */
    public boolean isGrayByRatio(int ratio) {
        return grayRequestCount.incrementAndGet() % 100 < ratio;
    }
    /**
     * 基于时间窗口灰度(例如每天9:00-18:00灰度)
     */
    public boolean isGrayByTime() {
        LocalTime now = LocalTime.now();
        LocalTime start = LocalTime.of(9, 0);
        LocalTime end = LocalTime.of(18, 0);
        return !now.isBefore(start) && !now.isAfter(end);
    }
    /**
     * 更新灰度配置
     */
    public void updateConfig(GrayConfig newConfig) {
        grayConfigMap.put(newConfig.getServiceName(), newConfig);
        // 同步到Redis(可选)
        if (redisTemplate != null) {
            redisTemplate.opsForValue().set("gray:config:" + newConfig.getServiceName(), newConfig);
        }
    }
    /**
     * 添加灰度用户
     */
    public void addGrayUser(String userId) {
        grayUsers.add(userId);
    }
    /**
     * 移除灰度用户
     */
    public void removeGrayUser(String userId) {
        grayUsers.remove(userId);
    }
    /**
     * 获取当前灰度配置
     */
    public GrayConfig getGrayConfig(String serviceName) {
        GrayConfig config = grayConfigMap.get(serviceName);
        if (config == null && redisTemplate != null) {
            config = (GrayConfig) redisTemplate.opsForValue().get("gray:config:" + serviceName);
        }
        return config;
    }
    /**
     * 灰度埋点 - 记录灰度用户操作
     */
    public void trackGrayMetric(String userId, String requestPath) {
        // 记录灰度指标,用于监控
        System.out.println(String.format("[GRAY] User: %s, Path: %s, Time: %s", 
            userId, requestPath, new Date()));
    }
}

3 灰度配置模型

import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.io.Serializable;
import java.util.Date;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GrayConfig implements Serializable {
    // 服务名称
    private String serviceName;
    // 版本号
    private String version;
    // 灰度比例(0-100)
    private int grayRatio;
    // 是否启用
    private boolean enabled;
    // 灰度规则描述
    private String description;
    // 创建时间
    private Date createTime;
    // 更新时间
    private Date updateTime;
}

4 灰度过滤器(基于网关层)

import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
 * 灰度发布过滤器 - 基于HTTP请求头
 */
@Component
public class GrayFilter implements Filter {
    // 灰度版本的服务地址
    private static final Map<String, String> VERSION_URL_MAP = new ConcurrentHashMap<>();
    static {
        // 配置不同版本的服务地址
        VERSION_URL_MAP.put("v1", "http://localhost:8081");
        VERSION_URL_MAP.put("v2", "http://localhost:8082");
    }
    // 白名单用户
    private static final Set<String> WHITE_LIST = ConcurrentHashMap.newKeySet();
    static {
        WHITE_LIST.add("1001");
        WHITE_LIST.add("1002");
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        // 获取用户ID
        String userId = httpRequest.getHeader("X-User-ID");
        // 判断是否为灰度请求
        String version = "v1"; // 默认版本
        if (userId != null && (WHITE_LIST.contains(userId) || isGrayByRatio(userId))) {
            version = "v2"; // 灰度版本
            httpResponse.setHeader("X-Gray-Version", "v2");
        }
        // 可以通过Header传递给下游服务
        httpRequest.setAttribute("grayVersion", version);
        // 继续链式调用
        chain.doFilter(request, response);
    }
}

5 基于Spring Cloud Gateway的灰度路由

import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import reactor.core.publisher.Mono;
/**
 * 基于Spring Cloud Gateway的灰度路由
 */
@Configuration
public class GrayRouteConfiguration {
    /**
     * 配置灰度路由规则
     */
    @Bean
    public RouteLocator grayRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
            .route("user-service-v1", r -> r
                .path("/api/user/**")
                .and()
                .header("X-Version", "v1")
                .uri("http://user-v1-service:8081"))
            .route("user-service-v2", r -> r
                .path("/api/user/**")
                .and()
                .header("X-Version", "v2")
                .uri("http://user-v2-service:8082"))
            .build();
    }
    /**
     * 自定义灰度全局过滤器
     */
    @Bean
    public GlobalFilter grayGlobalFilter() {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            // 获取用户信息
            String userId = request.getHeaders().getFirst("X-User-ID");
            // 判断是否灰度用户
            if (userId != null && GrayRuleService.isGrayUser(userId)) {
                ServerHttpRequest newRequest = request.mutate()
                    .header("X-Version", "v2")
                    .build();
                return chain.filter(exchange.mutate().request(newRequest).build());
            }
            // 默认走v1版本
            ServerHttpRequest newRequest = request.mutate()
                .header("X-Version", "v1")
                .build();
            return chain.filter(exchange.mutate().request(newRequest).build());
        };
    }
}

6 灰度监控与统计

import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.concurrent.atomic.AtomicLong;
import java.time.LocalDate;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 * 灰度监控统计服务
 */
@Service
public class GrayMonitorService {
    // 统计各版本请求量
    private final Map<String, AtomicLong> requestCount = new ConcurrentHashMap<>();
    // 统计各版本错误量
    private final Map<String, AtomicLong> errorCount = new ConcurrentHashMap<>();
    // 记录灰度用户操作
    private final Map<String, List<GrayLog>> grayLogs = new ConcurrentHashMap<>();
    /**
     * 记录请求
     */
    public void recordRequest(String version, String userId, boolean success) {
        // 增加请求计数
        requestCount.computeIfAbsent(version, k -> new AtomicLong()).incrementAndGet();
        // 记录错误
        if (!success) {
            errorCount.computeIfAbsent(version, k -> new AtomicLong()).incrementAndGet();
        }
        // 记录灰度用户日志
        if ("v2".equals(version)) {
            GrayLog log = new GrayLog(userId, userId, success, "gray-request");
            grayLogs.computeIfAbsent(userId, k -> new ArrayList<>()).add(log);
            // 检查是否存在严重错误
            checkGrayHealth(version);
        }
    }
    /**
     * 检查灰度健康状态
     */
    private void checkGrayHealth(String version) {
        long total = requestCount.getOrDefault(version, new AtomicLong()).get();
        long errors = errorCount.getOrDefault(version, new AtomicLong()).get();
        // 如果错误率超过20%,自动回滚
        if (total >= 100 && errors > 0 && (errors * 100.0 / total) > 20) {
            // 触发自动回滚
            rollback();
        }
    }
    /**
     * 自动回滚
     */
    private void rollback() {
        // 恢复v1版本,停止灰度
        GrayReleaseController controller = new GrayReleaseController();
        // 实际项目中通过配置中心动态切换
        System.err.println("[GRAY] 灰度异常,正在自动回滚...");
    }
    /**
     * 获取灰度统计报表
     */
    public GrayReport getGrayReport() {
        return GrayReport.builder()
            .date(LocalDate.now().toString())
            .v1RequestCount(requestCount.getOrDefault("v1", new AtomicLong()).get())
            .v2RequestCount(requestCount.getOrDefault("v2", new AtomicLong()).get())
            .v1ErrorCount(errorCount.getOrDefault("v1", new AtomicLong()).get())
            .v2ErrorCount(errorCount.getOrDefault("v2", new AtomicLong()).get())
            .build();
    }
}

7 服务版本实现

import org.springframework.stereotype.Service;
/**
 * V1版本服务
 */
@Service
public class V1Service {
    public String greetV1(String userId) {
        return String.format("你好 %s,这是V1版本服务", userId);
    }
    public User getUserV1(Long id) {
        return new User(id, "v1-user-" + id);
    }
}
/**
 * V2版本服务(灰度服务)
 */
@Service
public class V2Service {
    public String greetV2(String userId) {
        return String.format("您好 %s,这是V2版本服务(含新功能)", userId);
    }
    public User getUserV2(Long id) {
        return new User(id, "v2-user-" + id, "VIP用户");
    }
}

8 Kubernetes环境灰度配置示例

# 灰度部署配置
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service-v1
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
      version: v1
  template:
    metadata:
      labels:
        app: user-service
        version: v1
    spec:
      containers:
      - name: user-service
        image: user-service:v1.0.0
        ports:
        - containerPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service-v2
spec:
  replicas: 1  # 先部署1个实例
  selector:
    matchLabels:
      app: user-service
      version: v2
  template:
    metadata:
      labels:
        app: user-service
        version: v2
    spec:
      containers:
      - name: user-service
        image: user-service:v2.0.0
        ports:
        - containerPort: 8080
---
# 服务入口
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
  - port: 80
    targetPort: 8080
---
# Istio虚拟机灰度配置
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: user-service
spec:
  hosts:
  - user-service
  http:
  - match:
    - headers:
        X-Gray:
          exact: "true"
    route:
    - destination:
        host: user-service
        subset: v2
      weight: 100
  - route:
    - destination:
        host: user-service
        subset: v1
      weight: 100

使用方式

1 测试接口

# 正常版本
curl -X GET http://localhost:8080/api/greet -H "X-User-ID: 2000"
# 灰度版本(命中白名单)
curl -X GET http://localhost:8080/api/greet -H "X-User-ID: 1001"
# 更新灰度配置
curl -X POST http://localhost:8080/api/gray/config \
  -H "Content-Type: application/json" \
  -d '{
    "serviceName": "user-service",
    "version": "v2",
    "grayRatio": 30,
    "enabled": true
  }'

2 灰度策略汇总

策略类型 适用场景 实现方式
用户白名单 内测、指定用户 Set集合匹配
比例灰度 逐步放量 哈希取模
基于Header A/B测试 HTTP头匹配
基于时间 业务低峰期 时间窗口
动态配置 实时调整 Redis/配置中心
自动回滚 异常降级 错误率监控

最佳实践建议

  1. 灰度范围控制:建议从1%开始,逐步增加到5%,10%,20%,50%,100%
  2. 监控报警:实时监控灰度版本的错误率、延迟、用户反馈
  3. 自动回滚机制:设置错误率阈值(如5%),超限自动回滚
  4. 灰度日志:记录完整的灰度用户操作日志,便于问题追踪
  5. 数据一致性:做好灰度期间的数据迁移和兼容
  6. 配置分离:灰度开关独立管理,灵活开关

这个案例覆盖了Java灰度发布的核心场景,您可以根据实际需求进行调整和扩展。

抱歉,评论功能暂时关闭!