BreezAm

V1

2022/10/16阅读:73主题:橙心

gateway集成nacos、loadbalancer实现自定义负载均衡器,带源码解析(cloud版本:2021.0.3|boot版本:2.7.4)[场景1:灰度发布(金丝雀发布)]

🎨领域:Java后端开发


在这里插入图片描述


🔥收录专栏: 系统设计与实战 🐒个人主页:BreezAm 💖Gitee:https://gitee.com/BreezAm ✨个人标签:【后端】【大数据】【前端】【运维】

前言

在很多时候,我们需要根据自己的业务实现自定义的负载均衡,例如在灰度发布场景中(金丝雀发布),需要通过灰度策略实现负载均衡,这时候默认的负载均衡器就无法满足需求。下文主要介绍如何实现自定义负载均衡器以及相关源码解析,需要说明一下,不同的版本配置略有区别,以下是本文案例介绍的版本要求。

springboot springloud
2.7.4 2021.0.3

二、配置步骤

2.1 自定义负载均衡器

  1. 通过查看源码可知要实现自己的负载均衡器,需要实现ReactorServiceInstanceLoadBalancer接口,下面的代码中,是从自带的负载均衡器RoundRobinLoadBalancer中拷贝的,因为大部分都是一样的,我们只需要关注choose(Request request)这个方法,在这里可以通过ServiceInstanceListSupplier从注册中心拿到当前访问服务的所有实例,方法要求返回的是一个服务实例,因此就可以按照自己指定的规则返回符合要求的一个实例。
/**
 * 灰度发布负载均衡器
 */

public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private static final Log log = LogFactory.getLog(RoundRobinLoadBalancer.class);

    final AtomicInteger position;

    final String serviceId;

    ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;


    public GrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
                                  String serviceId)
 
{
        this(serviceInstanceListSupplierProvider, serviceId, new Random().nextInt(1000));
    }


    public GrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,
                                  String serviceId, int seedPosition)
 
{
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
        this.position = new AtomicInteger(seedPosition);
    }

    @SuppressWarnings("rawtypes")
    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
                .getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next()
                .map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
    }

    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
                                                              List<ServiceInstance> serviceInstances)
 
{
        Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances);
        if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
            ((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
        }
        return serviceInstanceResponse;
    }

    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + serviceId);
            }
            return new EmptyResponse();
        }
        //此处编写自己的负载均衡策略
       
        int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
        ServiceInstance instance = instances.get(pos % instances.size());
        return new DefaultResponse(instance);
    }
}
  1. 在这个版本中,我们无需增加其他配置就可以拿到HTTP请求的内容,下面选择实例的方法有个参数Request ,里面有一个上下文,从里面我们可以拿到请求的数据,在这个版本中是封装在ResponseData 实体类中的。需要说明一下,在比较老的loadbalancer版本中,如果没有做其他配置,这个Request 是空的,没有任何请求数据。
@Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
}

通过查看负载均衡客户端过滤器ReactiveLoadBalancerClientFilter源码可知,这个Request实例化的的类是DefaultRequest,里面有一个上下文类RequestDataContext,请求相关数据封装在RequestData里后放入了上下文对象里,供负载均衡器使用,相关代码如下:(注:如果需要放入自定义的数据,可以重写ReactiveLoadBalancerClientFilter类)。

DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(
new RequestDataContext(new RequestData(exchange.getRequest()), getHint(serviceId)));

return choose(lbRequest, serviceId, supportedLifecycleProcessors).doOnNext(response -> {
...

知道了实现原理,我们就可以从自定义负载均衡器里面拿到请求数据了,案例代码如下:

 @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        DefaultRequest req = (DefaultRequest) request;
        RequestDataContext context = (RequestDataContext) req.getContext();
        RequestData requestData = context.getClientRequest();
  ...
}

ResponseData实体类的结构如下所示。

public class ResponseData {

 private final HttpStatus httpStatus;

 private final HttpHeaders headers;

 private final MultiValueMap<String, ResponseCookie> cookies;

 private final RequestData requestData;

 private final Integer rawHttpStatus;
    ...
}

2.2 编写配置类

自定义负载均衡器编写好了以后,我们就需要将其注入到spring容器中,下面是配置代码,这里会有一个坑,就是不能加注解@Configuration,因为服务是懒加载的,如果加上注解就会导致容器启动时拿不到该服务实例,出现问题,从下面可以看出,ServiceInstanceListSupplier(服务实例)是通过服务的名字拿到的。

//@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
public class GrayLoadBalancerConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new GrayLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }
}

2.3 启动类添加注解

最后一部就是在启动类上面添加注解@LoadBalancerClients,并把配置类配置上去。

@SpringBootApplication
@LoadBalancerClients(defaultConfiguration = GrayLoadBalancerConfiguration.class)
public class GatewayApplication 
{

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.classargs);
    }
}

三、扩展

3.1 如何操作nacos中的配置文件

首先我们还是先来看下源码,部分源码如下,配置中心的配置文件是通过配置管理器NacosConfigManager 管理的,从代码中我们可以看到NacosConfigManager 是一个bean,也就是放在spring容器中管理了,因此,我们就可以在自己的业务代码中通过@Autowired将其注入就可以使用了。

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.nacos.config.enabled", matchIfMissing = true)
public class NacosConfigAutoConfiguration {
    ...
    @Bean
 public NacosConfigManager nacosConfigManager(
   NacosConfigProperties nacosConfigProperties)
 
{
  return new NacosConfigManager(nacosConfigProperties);
 }
 ...
}

当我们通过以下代码注入配置管理器以后,就可以拿到nacos配置服务ConfigService了。

@Autowired
public NacosConfigManager nacosConfigManager;

在ConfigService里有对操作nacos配置文件的CRUD方法,部分接口代码如下,感兴趣的读者可以去尝试一下。

public interface ConfigService {
    String getConfig(String var1, String var2, long var3) throws NacosException;

    boolean publishConfig(String var1, String var2, String var3) throws NacosException;

    boolean publishConfig(String var1, String var2, String var3, String var4) throws NacosException;

    boolean publishConfigCas(String var1, String var2, String var3, String var4, String var5) throws NacosException;

    boolean removeConfig(String var1, String var2) throws NacosException;
}

3.2 如何操作nacos中的服务

有时在业务场景中,我们需要从配置中心拿到存活的服务实例。和配置管理一样,需要拿到nacos服务管理器,从以下代码可以看出NacosServiceManager 也是一个bean,操作和3.1中介绍的大同小异。

@Configuration(proxyBeanMethods = false)
@ConditionalOnDiscoveryEnabled
@ConditionalOnNacosDiscoveryEnabled
public class NacosServiceAutoConfiguration {

 @Bean
 public NacosServiceManager nacosServiceManager() {
  return new NacosServiceManager();
 }
}

四、相关依赖

<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
 <groupId>com.alibaba.cloud</groupId>
 <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
 <groupId>com.alibaba.cloud</groupId>
 <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

🔥收录专栏:系统设计与实战 在这里插入图片描述

分类:

后端

标签:

后端

作者介绍

BreezAm
V1