title: 自定义spring cloud loadbalancer负载路由规则
date: 2022-12-01 19:51:10
tags:
- loadbalancer
- code analyze
- 微服务
categories: - [spring cloud gateway]
- [spring cloud loadbalancer]
前提
在微服务的架构中,虽然每个服务都是独立开发,但是如果一个服务由多个人协同开发,就会出现请求乱窜的问题。即服务A由甲乙两个人共同开发,nacos
上会生成A1
、A2
两个服务实例,甲本地发起一个请求可能会打到乙启动的服务实例上,但该实例上并没有甲开发的新功能,就会导致请求报错。
为了解决这样尴尬的问题,通常我们都是在naocs
上临时将其他节点下线,但这样不仅影响到其他人的开发进度,同时频繁的上下线也比较繁琐,所以从开发效率来看,需要我们另辟蹊径。
解决思路
新增nacos namespace
namespace
: 用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的 Group
或 Data ID
的配置。Namespace
的常用场景之一是不同环境的配置的区分隔离,例如开发测试环境和生产环境的资源(如配置、服务)隔离等
由上文描述可知,我们可以通过新增namespace
隔离服务请求,这样做确实可以防止接口请求乱窜,但意味着我需要启动接口链路的所有服务,很明显这种方法只能配合环境切换时使用,并不适合我们开发联调阶段,所以pass
修改负载路由规则
平台使用nacos
作为注册中心,spring cloud gateway
+spring cloud loadbalancer
作为网关路由的负载均衡客户端。那么如何保证由甲发出的请求最后会打到甲A实例上面呢?
ip
路由
在网关中实现功能:获取调用方ip
并与本机服务的ip
做比较,如果一致则将请求下发给集群中对应ip
的服务,否则按默认路由规则随机取一个实例。
这样可以让开发无感知的实现ip
策略的路由,但存在一定的局限性
-
网络环境千变万化,客户端真实的
ip
不一定能获取到 -
请求方(前端)和服务端需要在同一台电脑上,否则
ip
不一致,路由失效
充分利用nacos metadata
naocs
自带元数据管理,我们可以在nacos
中自定义一个元数据键值(值由开发指定),在前端项目中将该元数据键值塞入header
中,定义路由规则:如果请求方header
键值和服务实例中的元数据键值一致,则将请求下发给对应的服务,否则按默认路由规则随机取一个实例。
不同的方案对比之后,我决定还是采用第二种方案,虽然需要开发自己指定服务实例,但好在没有局限性,方案也比较可行,不会有什么阻力。
代码分析
敲定了方案,就可以开干了!上文说到整体微服务架构的网关是采用spring cloud loadbalancer
作为负载均衡的手段的,方案的关键需要自定义路由匹配规则,那么我们需要大致了解一下spring cloud loadbalancer
的实现方案。
浅析loadbalancer
为了简化理解,我们把spring cloud balancer
抽成上方图中所示的5个主要类。
LoadBalancerClientFactory
: 创建客户端、负载均衡器和客户端配置实例的工厂
LoadBalancerClientConfiguration
: 负载均衡器配置类
ReactorServiceInstanceLoadBalancer
:响应式负载路由接口
RandomLoadBalancer
:随机路由的负载均衡器,实现ReactorServiceInstanceLoadBalancer
接口
RoundRobinLoadBalancer
:轮询路由负载均衡器,实现ReactorServiceInstanceLoadBalancer
接口
我们就是要实现ReactorServiceInstanceLoadBalancer
接口,开发一个基于naocs
元数据的轮询负载器。现在目标明确了,下面从代码入手看看我们应该如何实现。
LoadBalancerClientFactory
LoadBalancerClientFactory
默认注入LoadBalancerClientConfiguration
作为负载均衡器的工厂类
LoadBalancerClientConfiguration
可以看出spring cloud balancer
默认的负载器是RoundRobinLoadBalancer
,并且因为方法中标有@ConditionalOnMissingBean
注解,所以我们可以扩展一个自己的ReactorLoadBalancer
。
注:这里注入的是 LazyProvider
,这主要因为在注册这个Bean
的时候依赖的其他 Bean
可能还没有被加载,所以利用 LazyProvider
机制,防止注入报错。
RoundRobinLoadBalancer
public Mono<Response<ServiceInstance>> choose(Request request) {
// 注入的时候注入的是Lazy Provider,这里取出实际的类 ServiceInstanceListSupplier
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);
// ServiceInstanceListSupplier实现了SelectedInstanceCallback的话,则执行下面的逻辑进行回调。SelectedInstanceCallback就是每次负载均衡器选择实例之后进行的回调方法
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();
}
// TODO 加入一些自己的路由规则来获取ServiceInstance
// Ignore the sign bit, this allows pos to loop sequentially from 0 to
// Integer.MAX_VALUE
// 循环规则:原子自增取绝对值
int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
// 取模运算
ServiceInstance instance = instances.get(pos % instances.size());
return new DefaultResponse(instance);
}
看完上面三个类代码,已经不需要考虑RoundRobinLoadBalancer
的实现了,我们只需要基于RoundRobinLoadBalancer
代码魔改出一套自己的路由规则即可。由此我们需要编写配置类注入自己的ReactorLoadBalancer
。
那么现在就只剩下一个问题,我们的配置类该如何注入?
LoadBalancerAutoConfiguration
我是看他名字觉得像是"罪恶之源",最后看代码并且打断点证实了我的想法,我们先看下他的源码
最后一个方法可以看出他是读取所有的LoadBalancerClientSpecification
作为LoadBalancerClientFactory
的配置,那就是要看
这些LoadBalancerClientSpecification
是如何创建的?我也没有好的办法,在这个jar
包中全局搜索LoadBalancerClientSpecification.class
类,最后让我定位到LoadBalancerClientConfigurationRegistrar
。那就看下他的代码实现吧。
LoadBalancerClientConfigurationRegistrar
public class LoadBalancerClientConfigurationRegistrar implements ImportBeanDefinitionRegistrar {
// 代码省略
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 这里获取LoadBalancerClients元数据
Map<String, Object> attrs = metadata.getAnnotationAttributes(LoadBalancerClients.class.getName(), true);
if (attrs != null && attrs.containsKey("value")) {
AnnotationAttributes[] clients = (AnnotationAttributes[]) attrs.get("value");
for (AnnotationAttributes client : clients) {
registerClientConfiguration(registry, getClientName(client), client.get("configuration"));
}
}
// 负载器默认配置类
if (attrs != null && attrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name, attrs.get("defaultConfiguration"));
}
// 获取LoadBalancerClient元数据
Map<String, Object> client = metadata.getAnnotationAttributes(LoadBalancerClient.class.getName(), true);
// 这个方法代码省略的部分 其实就是获取服务名称且LoadBalancerClient注解不指定名称会抛出IllegalStateException
String name = getClientName(client);
if (name != null) {
registerClientConfiguration(registry, name, client.get("configuration"));
}
}
private static void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
Object configuration) {
// buider模式构建一个LoadBalancerClientSpecification对象
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(LoadBalancerClientSpecification.class);
builder.addConstructorArgValue(name);
// 指定负载均衡器的配置类
builder.addConstructorArgValue(configuration);
// 注册对象
registry.registerBeanDefinition(name + ".LoadBalancerClientSpecification", builder.getBeanDefinition());
}
}
经过我们不懈的调试,这块加载类加载流程也正如我们想的那样,我们可以在启动类上定义@LoadBalancerClients
或者@LoadBalancerClient
注解,指定负载均衡器的配置类,进而自动装配我的负载器完成需求。
整个源码看完,思路一下子就打开了,下面就到了实现环节了!
代码实现
由上一节的代码分析可知,我们需要基于RoundRobinLoadBalancer
轮询策略实现一套自己的balancer
,我们起名为ServerNameLoadBalancer
实现如下
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.*;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.SelectedInstanceCallback;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 自定义路由规则 请求优先下发到指定的metadata.server-name
* 主体方法拷贝至RoundRobinLoadBalancer
*
* @see org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer#choose(Request)
* @data 2022-11-30 10:40
*/
public class ServerNameLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final Log log = LogFactory.getLog(ServerNameLoadBalancer.class);
final String serviceId;
final AtomicInteger position;
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
public static final String INSTANCE_SERVER_NAME = "server-name";
/**
* @param serviceInstanceListSupplierProvider a provider of
* {@link ServiceInstanceListSupplier} that will be used to get available instances
* @param serviceId id of the service for which to choose an instance
*/
public ServerNameLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,String serviceId) {
this(serviceInstanceListSupplierProvider, serviceId, new Random().nextInt(1000));
}
public ServerNameLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,String serviceId, int seedPosition) {
this.serviceId = serviceId;
this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
this.position = new AtomicInteger(seedPosition);
}
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
.getIfAvailable(NoopServiceInstanceListSupplier::new);
// 这里额外传入request对象用于解析请求头
return supplier.get(request).next()
.map(serviceInstances -> this.processInstanceResponse(request, supplier, serviceInstances));
}
private Response<ServiceInstance> processInstanceResponse(Request request, ServiceInstanceListSupplier supplier,
List<ServiceInstance> serviceInstances) {
// request对象带入getInstanceResponse方法
Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(request, serviceInstances);
if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
}
return serviceInstanceResponse;
}
private Response<ServiceInstance> getInstanceResponse(Request request, List<ServiceInstance> instances) {
if (instances.isEmpty()) {
if (log.isWarnEnabled()) {
log.warn("No servers available for service: " + serviceId);
}
return new EmptyResponse();
}
// 获取请求头server-name属性值
String serverName = StringUtils.EMPTY;
// 路由实例
ServiceInstance instance = null;
if (null != request.getContext() && request.getContext() instanceof RequestDataContext) {
List<String> serverNames = ((RequestDataContext) request.getContext()).getClientRequest().getHeaders().get(INSTANCE_SERVER_NAME);
// 有且只取其中一个
serverName = Optional.ofNullable(serverNames).map(m -> m.get(0)).orElse(StringUtils.EMPTY);
}
// Metadata.server-name 优先匹配
for (int i = 0; i < instances.size(); i++) {
ServiceInstance serviceInstance = instances.get(i);
// serverName一致
if (StringUtils.equals(serviceInstance.getMetadata().get(INSTANCE_SERVER_NAME),serverName)) {
instance = serviceInstance;
break;
}
}
// instance为空说明未配置server-name,或请求头未带server-name,走默认路由规则
if (null == instance) {
// Ignore the sign bit, this allows pos to loop sequentially from 0 to
// Integer.MAX_VALUE
int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
instance = instances.get(pos % instances.size());
}
return new DefaultResponse(instance);
}
}
然后我们需要定义一个LoadBalancer
配置类用于覆盖默认ReactorLoadBalancer
import com.justai.icp.gateway.balancer.ServerNameLoadBalancer;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
/**
* LoadBalancer配置类
*
* @data 2022-11-30 16:53
*/
public class LoadBalancerConfig {
/**
* 参考默认实现
*
* @see org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration#reactorServiceInstanceLoadBalancer
* @return
*/
@Bean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
// 加载自定义负载器
return new ServerNameLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
}
}
最后在启动类添加注解@LoadBalancerClients(defaultConfiguration = {LoadBalancerConfig.class})
,自定义网关负载器就实现啦!
后面就再也不用为服务乱窜而烦心了,哈哈~