自定义spring cloud loadbalancer负载路由规则


title: 自定义spring cloud loadbalancer负载路由规则
date: 2022-12-01 19:51:10
tags:

  • loadbalancer
  • code analyze
  • 微服务
    categories:
  • [spring cloud gateway]
  • [spring cloud loadbalancer]

前提

在微服务的架构中,虽然每个服务都是独立开发,但是如果一个服务由多个人协同开发,就会出现请求乱窜的问题。即服务A由甲乙两个人共同开发,nacos上会生成A1A2两个服务实例,甲本地发起一个请求可能会打到乙启动的服务实例上,但该实例上并没有甲开发的新功能,就会导致请求报错。

为了解决这样尴尬的问题,通常我们都是在naocs上临时将其他节点下线,但这样不仅影响到其他人的开发进度,同时频繁的上下线也比较繁琐,所以从开发效率来看,需要我们另辟蹊径。

解决思路

新增nacos namespace

namespace: 用于进行租户粒度的配置隔离。不同的命名空间下,可以存在相同的 GroupData 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

result

为了简化理解,我们把spring cloud balancer抽成上方图中所示的5个主要类。

LoadBalancerClientFactory: 创建客户端、负载均衡器和客户端配置实例的工厂

LoadBalancerClientConfiguration: 负载均衡器配置类

ReactorServiceInstanceLoadBalancer:响应式负载路由接口

RandomLoadBalancer:随机路由的负载均衡器,实现ReactorServiceInstanceLoadBalancer接口

RoundRobinLoadBalancer:轮询路由负载均衡器,实现ReactorServiceInstanceLoadBalancer接口

我们就是要实现ReactorServiceInstanceLoadBalancer接口,开发一个基于naocs元数据的轮询负载器。现在目标明确了,下面从代码入手看看我们应该如何实现。

LoadBalancerClientFactory

result

LoadBalancerClientFactory默认注入LoadBalancerClientConfiguration作为负载均衡器的工厂类

LoadBalancerClientConfiguration

result

可以看出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

我是看他名字觉得像是"罪恶之源",最后看代码并且打断点证实了我的想法,我们先看下他的源码

result

最后一个方法可以看出他是读取所有的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}),自定义网关负载器就实现啦!

后面就再也不用为服务乱窜而烦心了,哈哈~

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Cloud Gateway支持多种负载均衡策略,例如随机、轮询、权重等。如果现有的负载均衡策略不能满足你的需求,你可以自定义负载均衡策略。 首先,你需要实现`org.springframework.cloud.client.loadbalancer.reactive.LoadBalancer`接口来定义你的负载均衡策略。然后,你需要创建一个`org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction`实例,用于将服务的URI重写为负载均衡的服务实例地址。最后,你需要将这个自定义负载均衡策略应用到Spring Cloud Gateway的路由规则中。 以下是一个示例,展示了如何定义一个基于特定请求头的自定义负载均衡策略: ```java public class CustomLoadBalancer implements LoadBalancer<ServiceInstance> { private final String headerName; public CustomLoadBalancer(String headerName) { this.headerName = headerName; } @Override public Mono<Response<ServiceInstance>> choose(Request request) { Object headerValue = request.headers().getFirst(headerName); String serviceName = "my-service"; // 根据请求头的值选择服务实例 ServiceInstance serviceInstance = ...; return Mono.just(new DefaultResponse(serviceInstance)); } } public class CustomLoadBalancerGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomLoadBalancerGatewayFilterFactory.Config> { public CustomLoadBalancerGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { LoadBalancer<ServiceInstance> loadBalancer = new CustomLoadBalancer(config.getHeaderName()); RewriteFunction<String, String> rewriteFunction = uri -> { // 将URI重写为负载均衡的服务实例地址 ServiceInstance serviceInstance = loadBalancer.choose(Request.create("", new HttpHeaders())).block().getServer(); return "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + uri; }; return new RewritePathGatewayFilterFactory().apply(new RewritePathGatewayFilterFactory.Config().setRewriteFunction(rewriteFunction)); } public static class Config { private String headerName; public String getHeaderName() { return headerName; } public void setHeaderName(String headerName) { this.headerName = headerName; } } } ``` 在上面的示例中,`CustomLoadBalancer`实现了`LoadBalancer`接口,并基于特定的请求头选择服务实例。`CustomLoadBalancerGatewayFilterFactory`则将`CustomLoadBalancer`应用到Spring Cloud Gateway的路由规则中,并将服务的URI重写为负载均衡的服务实例地址。最后,你可以在路由规则中使用`CustomLoadBalancerGatewayFilterFactory`来定义自定义负载均衡策略。 ```yaml spring: cloud: gateway: routes: - id: my-route uri: http://my-service filters: - CustomLoadBalancer=my-header ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值