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都只能请求到我们自己本地的服务。
我的版本如下:
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 ObjectProviderserviceInstanceListSupplierProvider; public CustomSpringCloudLoadBalancer(String serviceId, LoadBalancerTypeEnum type, ObjectProvider serviceInstanceListSupplierProvider) { this(serviceId, new Random().nextInt(1000), type, serviceInstanceListSupplierProvider); } public CustomSpringCloudLoadBalancer(String serviceId, int seedPosition, LoadBalancerTypeEnum type, ObjectProvider serviceInstanceListSupplierProvider) { this.serviceId = serviceId; this.position = new AtomicInteger(seedPosition); this.type = type; this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider; } @Override public Mono > choose(Request request) { ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new); return supplier.get(request).next().map(serviceInstances -> processInstanceResponse(request, supplier, serviceInstances)); } private Response processInstanceResponse(Request request, ServiceInstanceListSupplier supplier, List serviceInstances) { Response serviceInstanceResponse = getInstanceResponse(request, serviceInstances); if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) { ((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer()); } return serviceInstanceResponse; } private Response getInstanceResponse(Request request, List 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 getGatewayDevelopmentInstance(Request request, List instances) { //把request转为默认的DefaultRequest,从request中拿到请求的ip信息,再选择ip一样的微服务 DefaultRequest defaultRequest = Convert.convert(new TypeReference >() {}, 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 getDevelopmentInstance(List instances) { //获取本机ip String 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 getRandomInstance(List 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 getRoundRobinInstance(List 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"; } //前端请求自定义请求头,转发到哪个服务 ListipList = 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 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 ReactorLoadBalancercustomLoadBalancer(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
最开始只有想法,但是不知道怎么实现,百度也没找到合适的方案。所以就开始看源码,研究了一下,然后照着源码写,测试了一下真的就实现了。所以,多看看源码还是有好处的。