灰度发布,也称为金丝雀发布,是一种介于全新发布和未发布之间,可以平滑过渡的发布方式。通过先向一小部分用户推出新功能,进行生产环境的灰度测试,若测试无问题,则进一步扩展到所有用户。作为一种优秀的微服务解决方案,Spring Cloud能够通过一系列改造快速实现灰度发布。
前言
在Spring Cloud生态系统中,灰度发布通常涉及Spring Cloud Gateway(简称SCG)与服务发现组件(如Nacos或Eureka)的元数据配置,实现灰度标记从SCG到各业务模块的转发。在多个微服务模块构成的复杂业务场景中,不仅是SCG到业务模块之间,业务模块间的调用也需要灰度发布的支持。这种从SCG到业务模块,以及业务模块间相互调用的全链路灰度发布方式,是我们本文的重点。
结合具体的业务需求,本文将详细介绍如何实现上述灰度发布方案,以Nacos为例进行说明。
实施步骤
1.1 步骤1: 网关判断请求是否需要灰度发布
网关需要根据一定的规则(如客户白名单、客户类型、客户所属地区等)来判断某一请求是否应走灰度路径。这可以通过Spring Gateway的Filter来实现。
为了便于后续扩展更多规则,我们将Filter抽象出一个基类:
代码如下:
public abstract class GrayFilter extends BaseFilter {
public boolean shouldFilter(ServerWebExchange exchange) {
return true;
}
public abstract Mono<Void> doFilter(ServerWebExchange exchange,
GatewayFilterChain chain,
Map<String, String> requestHeader);
public int getOrder() {
return RoutingConstants.GRAY_FILTER_ORDER;
}
}
具体实现一个根据客户白名单进行灰度发布的Filter:
public class CustomerGrayFilter extends GrayFilter {
private XXXClient xxxClient;
public Mono<Void> doFilter(ServerWebExchange exchange,
GatewayFilterChain chain,
Map<String, String> requestHeader) {
// 获取当前请求的客户ID
Long customerId = exchange.getAttribute(RoutingConstants.CUSTOMER_ID);
// 获取配置中需要灰度的客户
boolean grayFlag = bizConfigClient.getValue(RoutingConstants.CUSTOMER_GRAY_KEY, customerId);
...
return null;
}
}
1.2 步骤2: 网关打灰度标记
在确认请求需要通过灰度发布处理后,网关需要在请求头中添加一个明确的灰度标记,以便下游服务能够识别请求应该被路由到灰度实例。
在Spring Cloud Gateway中设置灰度标记
利用Spring Cloud Gateway的GlobalFilter,我们可以轻松地对所有通过网关的请求进行处理,并根据特定的逻辑添加灰度标记。以下是一个添加灰度标记的GlobalFilter实现示例:
public class GrayMarkingFilter implements GlobalFilter, Ordered {
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 根据业务逻辑判断是否为灰度请求
boolean isGrayRequest = shouldMarkAsGray(exchange);
if (isGrayRequest) {
// 在请求头中添加灰度标记
exchange.getRequest().mutate().header("Gray", "true").build();
}
return chain.filter(exchange);
}
private boolean shouldMarkAsGray(ServerWebExchange exchange) {
// 这里实现您的灰度标记逻辑,例如检查请求路径、查询参数或者cookie等
// 示例逻辑:根据查询参数中是否含有gray标记
String gray = exchange.getRequest().getQueryParams().getFirst("Gray");
return "true".equals(gray);
}
int getOrder() {
// 设置Filter执行顺序
return -1;
}
}
在这个GlobalFilter中,shouldMarkAsGray方法用于判断是否应该将请求标记为灰度。这里的逻辑可以根据实际需求进行定制,例如根据请求路径、查询参数或者cookie来决定。如果判断为灰度请求,就在请求头中添加Gray: true标记,然后继续Filter链的执行。
1.3 步骤3: 读取服务列表
为了实现灰度路由,网关需要能够从服务发现组件(如Nacos)中读取服务列表,并区分哪些是灰度实例。
使用Spring Cloud DiscoveryClient获取服务实例
在自定义的路由转发逻辑中,我们首先需要获取目标服务的所有实例列表,然后根据实例的元数据或其他标记来区分正常实例和灰度实例。
@Autowired
private DiscoveryClient discoveryClient;
public List<ServiceInstance> getTargetServiceInstances(String serviceName) {
// 从服务发现组件获取指定服务的所有实例
return discoveryClient.getInstances(serviceName);
}
public List<ServiceInstance> filterGrayInstances(List<ServiceInstance> instances) {
// 筛选出灰度实例
return instances.stream()
.filter(instance -> "true".equals(instance.getMetadata().get("Gray")))
.collect(Collectors.toList());
}
在这个示例中,getTargetServiceInstances方法使用Spring Cloud的DiscoveryClient接口来获取指定服务的所有实例。然后filterGrayInstances方法根据每个实例的元数据中是否包含Gray: true标记来筛选灰度实例。
结合步骤2中设置的灰度标记,这些灰度实例可以在步骤4中被优先选择来处理标记为灰度的请求,从而实现灰度路由的目标。
通过上述步骤,我们确保请求在通过Spring Cloud Gateway时能被正确地标记为灰度,并且网关能够根据这些标记从服务发现组件中正确选择灰度服务实例进行路由。这为灰度发布提供了必要的基础,确保了灰度请求的正确处理。
1.4 步骤4: 灰度路由转发
在Spring Cloud Gateway中,灰度路由转发的实现关键在于选择正确的服务实例进行请求转发。这一步骤需要我们在网关层面根据请求中的灰度标记,动态选择目标服务的灰度实例或正常实例。
自定义LoadBalancerClientFilter
Spring Cloud Gateway使用LoadBalancerClientFilter来实现请求的路由转发。为了支持灰度路由,我们需要继承并重写此Filter,以根据灰度标记选择合适的服务实例:
public class CustomLoadBalancerClientFilter extends LoadBalancerClientFilter {
private final LoadBalancerClient loadBalancer;
private final LoadBalancerProperties properties;
public CustomLoadBalancerClientFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties) {
super(loadBalancer, properties);
this.loadBalancer = loadBalancer;
this.properties = properties;
}
protected ServiceInstance choose(ServerWebExchange exchange) {
String grayFlag = exchange.getRequest().getHeaders().getFirst("Gray");
if ("true".equals(grayFlag)) {
List<ServiceInstance> instances = ((DiscoveryClient) loadBalancer).getInstances("YOUR-SERVICE-ID");
List<ServiceInstance> grayInstances = instances.stream()
.filter(instance -> "true".equals(instance.getMetadata().get("grayFlag")))
.collect(Collectors.toList());
if (!grayInstances.isEmpty()) {
// Randomly choose a gray instance
return grayInstances.get(ThreadLocalRandom.current().nextInt(grayInstances.size()));
}
}
// Fall back to the default behavior if no gray instance is found
return loadBalancer.choose(exchange.getRequest().getURI().getHost());
}
}
在这个重写的Filter中,我们首先检查请求头中是否存在Gray标记。如果存在,并且值为true,则我们从服务发现组件(如Nacos)获取到所有的服务实例,并筛选出标记为灰度的实例。最后,随机选择一个灰度实例进行路由。如果没有找到灰度实例,或者请求没有标记为灰度,则回到默认的负载均衡行为,选择一个正常的服务实例进行路由。
通过这种方式,我们能够实现在Spring Cloud Gateway层面对灰度发布的支持,根据请求的灰度标记动态选择目标服务的实例进行请求转发。这对于灰度发布的测试和渐进式部署提供了强大的支持。
这样,我们就完整地实现了灰度路由转发的关键步骤,确保在全链路灰度发布过程中,请求能够被正确地路由到相应的灰度服务实例。
1.5 步骤5: 业务模块读取灰度标记
业务模块需要识别请求是否携带灰度标记,并据此处理请求。在微服务架构中,服务间调用常通过HTTP头进行灰度信息传递。但在服务调用链中,灰度标记可能会丢失,因此需要一个机制确保标记的连贯性。
实现Feign客户端拦截器
为了在服务间调用时保持灰度标记,我们通过Feign客户端的拦截器实现:
public class GrayRequestInterceptor implements RequestInterceptor {
public void apply(RequestTemplate template) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (null != requestAttributes && null != requestAttributes.getRequest()) {
HttpServletRequest request = requestAttributes.getRequest();
Map<String, String> headers = new LinkedHashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String key = headerNames.nextElement();
String value = request.getHeader(key);
if (key.equalsIgnoreCase(NacosConstants.GRAY_FLAG) && value.equalsIgnoreCase(NacosConstants.GRAY_VALUE_TRUE)) {
headers.put(key, value);
}
}
}
headers.forEach(template::header);
}
}
}
此拦截器捕获进入当前服务的请求中的所有头信息,并在发起Feign客户端调用时,将这些头信息传递给被调用的服务,确保灰度标记的连续性。
1.6 步骤6: 调取灰度服务
当服务需要调用另一个服务时,应根据灰度标记决定调用的目标服务是灰度实例还是正常实例。这通常涉及到负载均衡策略的修改。
自定义Ribbon路由规则
通过自定义Ribbon的路由规则,可以根据灰度标记选择合适的服务实例:
public class GrayRule extends ZoneAvoidanceRule {
public Server choose(Object key) {
List<Server> serverList = this.getLoadBalancer().getReachableServers();
List<Server> grayServerList = new ArrayList<>();
for (Server server : serverList) {
if (server instanceof NacosServer) {
NacosServer nacosServer = (NacosServer) server;
String flag = nacosServer.getMetadata().get(NacosConstants.GRAY_FLAG);
if (NacosConstants.GRAY_VALUE_TRUE.equals(flag)) {
grayServerList.add(server);
}
}
}
if (!grayServerList.isEmpty()) {
// 走灰度服务
return chooseRandomly(grayServerList);
}
return super.choose(key);
}
private Server chooseRandomly(List<Server> serverList) {
return serverList.get(ThreadLocalRandom.current().nextInt(serverList.size()));
}
}
灰度配置自动化
为了简化灰度发布的配置和启用过程,可以通过Spring的配置类将自定义的Ribbon规则和Feign拦截器自动应用到所有服务上:
public class GrayAutoConfig {
public GrayRule grayRule() {
return new GrayRule();
}
public GrayRequestInterceptor grayRequestInterceptor() {
return new GrayRequestInterceptor();
}
}
Clients(defaultConfiguration = GrayAutoConfig.class)
public class Application {
// 应用启动类
}
1.7 步骤7: 添加业务模块配置
最后,需要在业务模块的配置中添加灰度标记,以便Nacos等服务发现组件能够识别并进行相应的处理:
spring:
cloud:
nacos:
discovery:
metadata:
# 灰度标记
grayFlag: 'true'
项目启动后,注册的服务将携带灰度标记,测试完成后,应移除grayFlag: 'true'配置以避免影响正常服务。
结论
通过上述步骤,我们详细介绍了在Spring Cloud环境下实现全链路灰度发布的方法。从网关判断请求是否需要灰度发布,到服务间调用的灰度传递,再到灰度服务的调用,每一环节都已实现,确保灰度发布可以平滑、高效地进行。这套灰度发布方案不仅适用于服务的新功能测试,还能在微服务架构中应对复杂的业务场景,提高服务的稳定性和可靠性。
关于领创集团