Spring Cloud实现灰度发布-基于Spring Cloud Gateway和Spring Cloud Loadbalancer

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

参考:

https://juejin.cn/post/7088147514733363237

Spring Cloud Gateway 扩展支持多版本控制及灰度发布-腾讯云开发者社区-腾讯云

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值