> 文章列表 > 【SpringCloud系列】开发环境下重写Loadbalancer实现自定义负载均衡

【SpringCloud系列】开发环境下重写Loadbalancer实现自定义负载均衡

【SpringCloud系列】开发环境下重写Loadbalancer实现自定义负载均衡

前言

        spring-cloud-starter-netflix-ribbon已经不再更新了,最新版本是2.2.10.RELEASE,最后更新时间是2021年11月18日,详细信息可以看maven官方仓库:https://search.maven.org/artifact/org.springframework.cloud/spring-cloud-starter-netflix-ribbon,SpringCloud官方推荐使用spring-cloud-starter-loadbalancer进行负载均衡。我们在开发的时候,多人开发同一个微服务,都注册到同一个nacos,前端请求的时候,网关Gateway默认轮训请求注册中心的服务,OpenFeign也会轮询请求注册中心的服务,这样就会导致前端有时会无法请求到我们本地写的接口,而是请求到别人的服务中。所以我们可以重写Loadbalancer默认的负载均衡策略,实现自定义负载均衡策略,不管是Gateway还是OpenFeign都只能请求到我们自己本地的服务。

        我的版本如下:

        <spring-boot.version>2.7.3</spring-boot.version>
        <spring-cloud.version>2021.0.4</spring-cloud.version>
        <spring-cloud-alibaba.version>2021.0.4.0</spring-cloud-alibaba.version>

一、添加负载方式配置

        1、定义负载均衡方式的枚举

public enum LoadBalancerTypeEnum {/* 开发环境,获取自己的服务*/DEV,/* 网关,根据请求地址获取对应的服务*/GATEWAY,/* 轮循*/ROUND_ROBIN,/* 随机*/RANDOM;}

        2、添加配置类,默认使用轮训方式

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;/* 负载均衡配置项*/
@Data
@ConfigurationProperties(prefix = "spring.cloud.loadbalancer")
public class LoadBalanceProperties {private LoadBalancerTypeEnum type = LoadBalancerTypeEnum.ROUND_ROBIN;}

二、参考默认实现自定义

        默认的负载均衡策略是这个类:

org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer

        我们参考这个类实现自己的负载均衡策略即可,RoundRobinLoadBalancer实现了ReactorServiceInstanceLoadBalancer这个接口,实现了choose这个方法,如下图:

        在choose方法中调用了processInstanceResponse方法,processInstanceResponse方法中调用了getInstanceResponse方法,所以我们我们可以复制RoundRobinLoadBalancer整个类,只修改getInstanceResponse这个方法里的内容就可以实现自定义负载均衡策略。

        在自定义的类中,我们实现了四种负载均衡策略

        1、getRoundRobinInstance方法是直接复制的RoundRobinLoadBalancer类中的实现;

        2、getRandomInstance方法参考org.springframework.cloud.loadbalancer.core.RandomLoadBalancer类中的实现;

        3、getDevelopmentInstance方法是返回所有服务中和当前机器ip一致的服务,如果没有,则轮训返回;

        4、getGatewayDevelopmentInstance方法是返回所有服务中和网关请求头中ip一致的服务。

import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.TypeReference;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.utils.ip.IpUtils;
import com.ruoyi.common.loadbalance.config.LoadBalancerTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultRequest;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.RequestData;
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
import org.springframework.cloud.client.loadbalancer.Response;
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 org.springframework.http.HttpHeaders;
import reactor.core.publisher.Mono;import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;/* 自定义 SpringCloud 负载均衡算法* 负载均衡算法的默认实现是 {@link org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer}/
@Slf4j
public class CustomSpringCloudLoadBalancer implements ReactorServiceInstanceLoadBalancer {private final String serviceId;private final AtomicInteger position;private final LoadBalancerTypeEnum type;private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;public CustomSpringCloudLoadBalancer(String serviceId,LoadBalancerTypeEnum type,ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {this(serviceId, new Random().nextInt(1000), type, serviceInstanceListSupplierProvider);}public CustomSpringCloudLoadBalancer(String serviceId,int seedPosition,LoadBalancerTypeEnum type,ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {this.serviceId = serviceId;this.position = new AtomicInteger(seedPosition);this.type = type;this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;}@Overridepublic Mono<Response<ServiceInstance>> choose(Request request) {ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);return supplier.get(request).next().map(serviceInstances -> processInstanceResponse(request, supplier, serviceInstances));}private Response<ServiceInstance> processInstanceResponse(Request request,ServiceInstanceListSupplier supplier,List<ServiceInstance> serviceInstances) {Response<ServiceInstance> serviceInstanceResponse = 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();}if (Objects.equals(type, LoadBalancerTypeEnum.ROUND_ROBIN)){return this.getRoundRobinInstance(instances);}else if (Objects.equals(type, LoadBalancerTypeEnum.RANDOM)){return this.getRandomInstance(instances);}else if (Objects.equals(type, LoadBalancerTypeEnum.DEV)){return this.getDevelopmentInstance(instances);}else if (Objects.equals(type, LoadBalancerTypeEnum.GATEWAY)){return this.getGatewayDevelopmentInstance(request, instances);}return this.getRoundRobinInstance(instances);}/* 获取网关本机实例 @param instances 实例* @return {@link Response }<{@link ServiceInstance }>* @author : lwq* @date : 2022-12-15 14:22:13*/private Response<ServiceInstance> getGatewayDevelopmentInstance(Request request, List<ServiceInstance> instances) {//把request转为默认的DefaultRequest,从request中拿到请求的ip信息,再选择ip一样的微服务DefaultRequest<RequestDataContext> defaultRequest = Convert.convert(new TypeReference<DefaultRequest<RequestDataContext>>() {}, request);RequestDataContext context = defaultRequest.getContext();RequestData clientRequest = context.getClientRequest();HttpHeaders headers = clientRequest.getHeaders();String requestIp = IpUtils.getIpAddressFromHttpHeaders(headers);log.debug("客户端请求gateway的ip:{}", requestIp);//先取得和本地ip一样的服务,如果没有则按默认来取for (ServiceInstance instance : instances) {String currentServiceId = instance.getServiceId();String host = instance.getHost();log.debug("注册服务:{},ip:{}", currentServiceId, host);if (StringUtils.isNotEmpty(host) && StringUtils.equals(requestIp, host)) {return new DefaultResponse(instance);}}return getRoundRobinInstance(instances);}/* 获取本机实例 @param instances 实例* @return {@link Response }<{@link ServiceInstance }>* @author : lwq* @date : 2022-12-15 14:22:13*/private Response<ServiceInstance> getDevelopmentInstance(List<ServiceInstance> instances) {//获取本机ipString hostIp = IpUtils.getHostIp();log.debug("本机Ip:{}", hostIp);//先取得和本地ip一样的服务,如果没有则按默认来取for (ServiceInstance instance : instances) {String currentServiceId = instance.getServiceId();String host = instance.getHost();log.debug("注册服务:{},ip:{}", currentServiceId, host);if (StringUtils.isNotEmpty(host) && StringUtils.equals(hostIp, host)) {return new DefaultResponse(instance);}}return getRoundRobinInstance(instances);}/* 使用随机算法* 参考{link {@link org.springframework.cloud.loadbalancer.core.RandomLoadBalancer}} @param instances 实例* @return {@link Response }<{@link ServiceInstance }>* @author : lwq* @date : 2022-12-15 13:32:11*/private Response<ServiceInstance> getRandomInstance(List<ServiceInstance> instances) {int index = ThreadLocalRandom.current().nextInt(instances.size());ServiceInstance instance = instances.get(index);return new DefaultResponse(instance);}/* 使用RoundRobin机制获取节点 @param instances 实例* @return {@link Response }<{@link ServiceInstance }>* @author : lwq* @date : 2022-12-15 13:28:31*/private Response<ServiceInstance> getRoundRobinInstance(List<ServiceInstance> instances) {// 每一次计数器都自动+1,实现轮询的效果int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;ServiceInstance instance = instances.get(pos % instances.size());return new DefaultResponse(instance);}}

        其中的工具类如下:

import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.URLUtil;
import org.springframework.http.HttpHeaders;/* 获取IP方法*/
public class IpUtils{/* 获取IP地址 @return 本地IP地址*/public static String getHostIp(){try{return InetAddress.getLocalHost().getHostAddress();}catch (UnknownHostException e){}return "127.0.0.1";}/* 获取客户端IP @param httpHeaders 请求头* @return IP地址*/public static String getIpAddressFromHttpHeaders(HttpHeaders httpHeaders){if (httpHeaders == null){return "unknown";}//前端请求自定义请求头,转发到哪个服务List<String> ipList = httpHeaders.get("forward-to");String ip = CollectionUtil.get(ipList, 0);//请求自带的请求头if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){ipList = httpHeaders.get("x-forwarded-for");ip = CollectionUtil.get(ipList, 0);}//从referer获取请求的ip地址if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){//从referer中获取请求的ip地址List<String> refererList = httpHeaders.get("referer");String referer = CollectionUtil.get(refererList, 0);URL url = URLUtil.url(referer);if (Objects.nonNull(url)){ip = url.getHost();}else {ip = "unknown";}}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){ipList = httpHeaders.get("Proxy-Client-IP");ip = CollectionUtil.get(ipList, 0);}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){ipList = httpHeaders.get("X-Forwarded-For");ip = CollectionUtil.get(ipList, 0);}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){ipList = httpHeaders.get("WL-Proxy-Client-IP");ip = CollectionUtil.get(ipList, 0);}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){ipList = httpHeaders.get("X-Real-IP");ip = CollectionUtil.get(ipList, 0);}return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);}
}

        getIpAddressFromHttpHeaders方法中,是从请求头总拿到了自定义的请求头forward-to,要想实现此功能,就需要前端发送请求的时候携带这个请求头,例如

        在若依的request.js中可以这么写: config.headers['forward-to'] = '192.168.0.145'

三、配置负载均衡策略

       默认的配置在这里:

org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration

        我们参考这个配置,实现自己的配置。启用上面写好的配置类LoadBalanceProperties,然后传到自定义的负载均衡策略类CustomSpringCloudLoadBalancer,其他的复制就可以。

import com.ruoyi.common.loadbalance.core.CustomSpringCloudLoadBalancer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
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.context.annotation.Configuration;
import org.springframework.core.env.Environment;/* 自定义负载均衡客户端配置/
@SuppressWarnings("all")
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(LoadBalanceProperties.class)
public class CustomLoadBalanceClientConfiguration {@Bean@ConditionalOnBean(LoadBalancerClientFactory.class)public ReactorLoadBalancer<ServiceInstance> customLoadBalancer(LoadBalanceProperties loadBalanceProperties,Environment environment,LoadBalancerClientFactory loadBalancerClientFactory) {String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);return new CustomSpringCloudLoadBalancer(name, loadBalanceProperties.getType(),loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class));}}

        然后使用LoadBalancerClients注解加载一下配置

import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;/* 自定义负载均衡自动配置/
@LoadBalancerClients(defaultConfiguration = CustomLoadBalanceClientConfiguration.class)
public class CustomLoadBalanceAutoConfiguration {}

四、使用

        将以上代码独立成一个模块,然后再其他微服务中的pom文件中引入,然后添加对应的配置就可以实现自定义负载均衡了

        1、在微服务中配置如下即可实现调用其他服务时,调用自己本地开发环境的微服务

    spring.cloud.loadbalancer.type=dev

        2、在网关中配置如下即可实现调用固定某个服务

 spring.cloud.loadbalancer.type=gateway

写在最后的话

        最开始只有想法,但是不知道怎么实现,百度也没找到合适的方案。所以就开始看源码,研究了一下,然后照着源码写,测试了一下真的就实现了。所以,多看看源码还是有好处的。