本文目录导读:

我来为您提供一个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%开始,逐步增加到5%,10%,20%,50%,100%
- 监控报警:实时监控灰度版本的错误率、延迟、用户反馈
- 自动回滚机制:设置错误率阈值(如5%),超限自动回滚
- 灰度日志:记录完整的灰度用户操作日志,便于问题追踪
- 数据一致性:做好灰度期间的数据迁移和兼容
- 配置分离:灰度开关独立管理,灵活开关
这个案例覆盖了Java灰度发布的核心场景,您可以根据实际需求进行调整和扩展。