BreezAm
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 自定义负载均衡器
-
通过查看源码可知要实现自己的负载均衡器,需要实现 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);
}
}
-
在这个版本中,我们无需增加其他配置就可以拿到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.class, args);
}
}
三、扩展
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>
🔥收录专栏:系统设计与实战
作者介绍