Spring Cloud实现灰度发布,基于注册中心Eureka,网关Spring Cloud Gateway,负载均衡器Spring Cloud Loadbalancer。
我们要实现灰度发布,实现应用平滑升级,那就要做两件事,第一,在注册中心标识出哪些是灰度应用,哪些是正常的应用,第二,修改负载均衡器,根据灰度规则将需要灰度的请求转发到灰度应用上。下面也是按照这两个步骤介绍,文章最后会给出源码示例。
本文由于在本地测试,因此注册中心只启动了一个Eureka实例,有两个项目backend-server1,backend-server2,两个项目代码完全一样,都有一个接口/home/hello,server1请求返回"hello from server1",server2请求返回"hello from server2",其中backend-server1代表灰度应用。还有一个网关是Spring Cloud Gateway,由于Spring Cloud Gateway原生集成了Spring Cloud Loadbalancer,因此这里也是定制负载均衡器Spring Cloud Loadbalancer实现灰度流量转发。
地址端口:
Eureka: 127.0.0.1:8001
backend-server1: 127.0.0.1:8082
backend-server2: 127.0.0.1:8083
gateway: 127.0.0.1:8080
注册中心配置
配置文件application.properties
spring.application.name=eureka-server
server.port=8081
eureka.client.fetch-registry=false
eureka.client.register-with-eureka=false
eureka.client.service-url.defaultZone=http://127.0.0.1:8081/eureka
启动类
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
backend-server1(灰度应用)配置
配置文件application.properties
spring.application.name=backend-server
server.port=8082
eureka.instance.instance-id=server1
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8081/eureka
eureka.instance.metadata-map.gray=true
这里的配置项eureka.instance.metadata-map.gray=true就是重点,这个会存储到注册中心,负载均衡器从注册中心取实例的时候会带着这个属性,然后就可以区分出哪些是灰度应用,哪些不是灰度应用了。
接口
@RestController
@RequestMapping("/home")
@Slf4j
public class MainPageController {
@GetMapping("/hello")
public String hello(@RequestHeader(name = "gray", required = false, defaultValue = "false") boolean gray) {
log.info("gray? {}", gray);
return "hello from server1";
}
}
backend-server2配置
application.properties
spring.application.name=backend-server
server.port=8083
eureka.instance.instance-id=server2
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8081/eureka
可以看出这里是没有灰度属性的,因此它是正常的应用。
接口
@RestController
@RequestMapping("/home")
@Slf4j
public class MainPageController {
@GetMapping("/hello")
public String hello(@RequestHeader(name = "gray", required = false, defaultValue = "false") boolean gray) {
log.info("gray? {}", gray);
return "hello from server2";
}
}
gateway网关配置
配置文件application.yml
server:
port: 8080
eureka:
instance:
instance-id: gateway
client:
service-url:
defaultZone: http://127.0.0.1:8081/eureka
spring:
main:
web-application-type: reactive
application:
name: gateway
cloud:
gateway:
routes:
- id: backend-server
uri: lb://backend-server
predicates:
- Path=/home/**
过滤器配置
首先我们需要标识出哪些是灰度流量,这个正常情况下是根据配置规则比如手机尾号等来标识的,这里为了简单仅做示例从请求参数获取。知道哪些是灰度流量将标识存储到上下文中,这里因为也要将标识传递给后端应用,因此将标识存储到请求头中。
@Component
public class GrayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
boolean isGrayRequest = isGrayRequest(exchange.getRequest());
// 设置到请求头
ServerHttpRequest tokenRequest = exchange.getRequest().mutate()
.header("gray", String.valueOf(isGrayRequest))
.build();
ServerWebExchange newServerExchange = exchange.mutate().request(tokenRequest).build();
return chain.filter(newServerExchange);
}
private boolean isGrayRequest(ServerHttpRequest serverHttpRequest) {
// 正常情况下根据用户手机号,用户id等灰度
MultiValueMap<String, String> queryParams = serverHttpRequest.getQueryParams();
return queryParams.containsKey("gray") && "true".equals(queryParams.getFirst("gray"));
}
@Override
public int getOrder() {
return 10149;
}
}
定制负载均衡规则
Spring Cloud Loadbalancer实现的负载均衡选择实例在一个过滤器中实现,过滤器名称ReactiveLoadBalancerClientFilter。
ReactiveLoadBalancerClientFilter选择实例的方法
private Mono<Response<ServiceInstance>> choose(Request<RequestDataContext> lbRequest, String serviceId, Set<LoadBalancerLifecycle> supportedLifecycleProcessors) {
ReactorLoadBalancer<ServiceInstance> loadBalancer = (ReactorLoadBalancer)this.clientFactory.getInstance(serviceId, ReactorServiceInstanceLoadBalancer.class);
if (loadBalancer == null) {
throw new NotFoundException("No loadbalancer available for " + serviceId);
} else {
supportedLifecycleProcessors.forEach((lifecycle) -> {
lifecycle.onStart(lbRequest);
});
return loadBalancer.choose(lbRequest);
}
}
可以看到负载均衡器就是ReactorServiceInstanceLoadBalancer,查看它的实现类如下
第一个是我新增的,第二第三是自带的,默认是第三个轮询策略。
默认的配置在类org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration中,下面就是系统默认的配置。
@Bean
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty("loadbalancer.client.name");
return new RoundRobinLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
我们要做的就是新增负载均衡规则,然后覆盖掉默认配置。
因为Spring Cloud Loadbalancer默认采用轮询策略,因此我们定制一个轮询策略,根据有没有灰度标识决定是在正常应用中轮询还是在灰度应用中轮询。名字就叫GrayRoundRobinLoadBalancer。
在负载均衡器方法调用入口将灰度标识传递到内层方法,下面的isGray参数就是从请求头获取的值,请求头的数据是在上面过滤器中设置的。
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
RequestDataContext requestDataContext = (RequestDataContext) request.getContext();
String isGray = requestDataContext.getClientRequest().getHeaders().getFirst("gray");
return supplier.get(request).next().map((serviceInstances) -> {
return this.processInstanceResponse(Boolean.parseBoolean(isGray), supplier, serviceInstances);
});
}
最终调用的内层方法根据是否是灰度流量返回应用实例,如果是灰度流量选择灰度的实例,选择的标准就是注册到注册中心的实例元数据有没有配置gray参数。
// filter
List<ServiceInstance> grayInstances = instances.stream()
.filter(serviceInstance -> StringUtils.equals(serviceInstance.getMetadata().get("gray"), "true")).toList();
List<ServiceInstance> normalInstances = new ArrayList<>(instances);
normalInstances.removeAll(grayInstances);
if (isGray) {
int pos = this.grayPosition.incrementAndGet() & Integer.MAX_VALUE;
ServiceInstance instance = grayInstances.get(pos % grayInstances.size());
return new DefaultResponse(instance);
} else {
int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
ServiceInstance instance = normalInstances.get(pos % normalInstances.size());
return new DefaultResponse(instance);
}
负载均衡规则有了下面就是新增一个自动配置,覆盖掉默认的,让我们的GrayRoundRobinLoadBalancer生效。
public class CustomLoadBalancerAutoConfiguration extends LoadBalancerClientConfiguration {
@Bean
@ConditionalOnMissingBean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty("loadbalancer.client.name");
return new GrayRoundRobinLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
这是继承原来的配置,只覆盖了我们需要的方法。
新增一个配置,来向LoadBalancerClientFactory注入我们的配置类,从而覆盖掉原来的配置。
@Component
public class LoadBalancerClientFactoryPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean.getClass().isAssignableFrom(LoadBalancerClientFactory.class)) {
List<LoadBalancerClientSpecification> allConfigurations = new ArrayList(((LoadBalancerClientFactory) bean).getConfigurations().values());
allConfigurations.add(new LoadBalancerClientSpecification("backend-server", new Class[]{ CustomLoadBalancerAutoConfiguration.class }));
((LoadBalancerClientFactory) bean).setConfigurations(allConfigurations);
}
return bean;
}
}
这样Spring Cloud Gateway就是使用我们自定义的负载均衡规则,根据是否是灰度的流量选择正常实例还是灰度实例。
Postman测试
启动四个应用,使用postman测试请求网关。
如果带灰度请求参数则从server1返回
不带灰度参数则从server2返回
代码已经传到了gitee,地址gray-release: Spring Cloud灰度发布示例Spring Cloud Gateway + Eureka
参考: