Spring Cloud Loadbalancer 的使用

一、默认负载均衡策略

Spring Cloud LoadBalancer 默认的负载均衡策略是轮询。

轮询效果示例

我们需要示例一个请求分发到不同的模块上,所以我们需要创建多模块项目。

新建 Spring Boot (3.0.2)的 Maven 项目(JDK 17)(父模块),添加依赖 Spring Web、Nacos Service Discovery、OpenFeign、Cloud LoadBalancer,然后删除 src 与 HELP.md 文档,修改 pom.xml (如删除模块运行)

创建子模块,然后在子模块的 pom.xml 中配置父类,再删除父 pom.xml 包含的声明,最后在父模块中配置子类声明。此时就可以在子模块的配置文件中配置配置中心。

我们先在子模块写一个服务UserController。

java 复制代码
package org.example.provider.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private ServletWebServerApplicationContext context;

    @RequestMapping("/getname")
    public String getName(@RequestParam("id") Integer id) {
        return "Provider-name-" + id +
                " | port: " + context.getWebServer().getPort();
    }
}

由于Loadbalancer默认是轮询的所以我们创建两个实例运行。

接着我们用同样的方式新建consumer子模块,然后在运行类中添加 OpenFeign 的注解(开启OpenFeign),再写声明服务(Service)与调用服务(Controller),然后我们就可以访问对应的服务,可以看到它的轮询效果。

java 复制代码
package org.example.consumer.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Service
@FeignClient("loadbalancer-service")
public interface UserService {
    @RequestMapping("/user/getname")
    String getName(@RequestParam("id") Integer id);
}
java 复制代码
package org.example.consumer.controller;

import org.example.consumer.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CallController {
    @Autowired
    private UserService userService;

    @RequestMapping("/getname")
    public String getName(@RequestParam("id") Integer id) {
        return userService.getName(id);
    }

}

Loadbalancer 轮询的源码

我们需要看 Spring Cloud LoadBalancer 的配置类 LoadBalancerClientConfiguration 的部分源码:

其中 ReactorLoadBalancer 方法非常重要:

java 复制代码
    @Bean
    @ConditionalOnMissingBean
    public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty("loadbalancer.client.name");
        return new RoundRobinLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name);
    }

可以看到默认设置的负载均衡策略是 RoundRobinLoadBalancer (可以直接看最下面的三行代码,即轮询负载均衡算法的实现)。

java 复制代码
    public RoundRobinLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId, int seedPosition) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
        this.position = new AtomicInteger(seedPosition);
    }

    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map((serviceInstances) -> {
            return this.processInstanceResponse(supplier, serviceInstances);
        });
    }
java 复制代码
    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) {
        Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances);
        if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
            ((SelectedInstanceCallback)supplier).selectedServiceInstance((ServiceInstance)serviceInstanceResponse.getServer());
        }

        return serviceInstanceResponse;
    }

    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + this.serviceId);
            }

            return new EmptyResponse();
        } else if (instances.size() == 1) {
            return new DefaultResponse((ServiceInstance)instances.get(0));
        } else {
            int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
            ServiceInstance instance = (ServiceInstance)instances.get(pos % instances.size());
            return new DefaultResponse(instance);
        }
    }

二、随机负载均衡策略

Spring Cloud LoadBalancer 内置了两种负载均衡策略:

  1. 轮询负载均衡策略,默认负载均衡策略。
  2. 随机负载均衡策略。

而要实现随机负载均衡策略的步骤如下:

  1. 创建随机负载均衡策略。
  2. 设置随机负载均衡策略。

创建随机负载均衡器

和源码类似的,返回ReactorLoadBalancer,通过 new 随机负载均衡 创建出来。那么我们可以直接复制源码然后修改方法名就是创建了随机负载均衡器:

java 复制代码
package com.example.comsumer.config;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
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;

public class RandomLoadBalancerConfig {
    @Bean
    public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
            Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty("loadbalancer.client.name");
        return new RandomLoadBalancer(
                loadBalancerClientFactory.getLazyProvider(name,
                        ServiceInstanceListSupplier.class), name);
    }
}

设置局部负载均衡策略

找到 comsumer.service 的接口,加注解 @LoadBalancerClient,然后设置对应的参数:

但是设置局部策略在一些版本是无效的,所以我们可以设置全局的负载均衡器

设置全局负载均衡策略

在启动类上设置注解 @LoadBalancerClients,然后设置对应的参数:

三、Nacos 权重负载均衡策略

新建 Nacos 负载均衡器

java 复制代码
package com.example.comsumer.config;

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancer;
import jakarta.annotation.Resource;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
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;

@LoadBalancerClients(defaultConfiguration = NacosLoadBalancerConfig.class)
public class NacosLoadBalancerConfig {
    @Resource
    private NacosDiscoveryProperties nacosDiscoveryProperties;

    @Bean
    public ReactorLoadBalancer<ServiceInstance> nacosLoadBalancer(
            Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty("loadbalancer.client.name");
        return new NacosLoadBalancer(
                loadBalancerClientFactory.getLazyProvider(name,
                        ServiceInstanceListSupplier.class), name
                , nacosDiscoveryProperties);
    }
}

设置局部负载均衡策略

关掉前面开启的随机全局负载均衡策略。

再在service中修改为nacos局部负载均衡策略

此时要查看 Nacos 负载均衡策略的效果的话需要在 Nacos中设置权重。

四、自定义负载均衡

实现自定义负载均衡策略需要以下 3步:

  1. 创建自定义负载均衡器
  2. 封装自定义负载均衡器
  3. 为服务设置自定义负载均衡器

新建负载均衡类(创建自定义负载均衡器)

自定义的我们需要实现顶级类的接口 ReactorServiceInstanceLoadBalancer。

复制顶级类的代码,修改核心策略即可。

java 复制代码
package com.example.comsumer.config;

import jakarta.servlet.http.HttpServletRequest;
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.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;

public class CustomLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private static final Log log = LogFactory.getLog(RandomLoadBalancer.class);
    private final String serviceId;
    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    public CustomLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    }

    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get(request).next().map((serviceInstances) -> {
            return this.processInstanceResponse(supplier, serviceInstances);
        });
    }

    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) {
        Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances);
        if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
            ((SelectedInstanceCallback)supplier).selectedServiceInstance((ServiceInstance)serviceInstanceResponse.getServer());
        }

        return serviceInstanceResponse;
    }

    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
        if (instances.isEmpty()) {
            if (log.isWarnEnabled()) {
                log.warn("No servers available for service: " + this.serviceId);
            }

            return new EmptyResponse();
        } else {
            // 核心: 自定义随机策略
            ServletRequestAttributes attributes = (ServletRequestAttributes)
                    RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            String ipAddress = request.getRemoteAddr();
            System.out.println("用户 IP: " + ipAddress);
            int hash = ipAddress.hashCode();
            // 自定义负载均衡策略[关键代码]
            int index = hash % instances.size();
            // 得到服务实例方法
            ServiceInstance instance = (ServiceInstance)instances.get(index);
            return new DefaultResponse(instance);
        }
    }
}

新建一个负载均衡对象(封装自定义负载均衡器)

java 复制代码
package com.example.comsumer.config;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
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;

public class CustomLoadBalancerConfig {
    @Bean
    public ReactorLoadBalancer<ServiceInstance> customLoadBalancer(
            Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty("loadbalancer.client.name");
        return new CustomLoadBalancer(
                loadBalancerClientFactory.getLazyProvider(name,
                        ServiceInstanceListSupplier.class), name);
    }
}

设置负载均衡策略()为服务设置自定义负载均衡器

为了防止冲突,注释掉Nacos的策略。

五、缓存

Spring Cloud LoadBalancer 在获取实例时有两种选择:

  1. 即时获取: 每次从注册中心得到最新健康的实例,效果好、开销太大。
  2. 缓存服务列表: 每次得到服务列表之后,缓存一段时间,这样既能保证性能,同时也能兼容一定的及时性

而 Spring Cloud LoadBalancer 中默认开启了缓存服务列表的功能。

Spring Cloud LoadBalancer 默认缓存的重要特性有两项:

  1. 缓存的过期时间为 35s。
  2. 缓存保存个数为 256 个。

我们可以通过以下配置来改变这些配置:

相关推荐
海绵波波1071 小时前
flask后端开发(10):问答平台项目结构搭建
后端·python·flask
网络风云3 小时前
【魅力golang】之-反射
开发语言·后端·golang
Q_19284999063 小时前
基于Spring Boot的电影售票系统
java·spring boot·后端
运维&陈同学4 小时前
【Kibana01】企业级日志分析系统ELK之Kibana的安装与介绍
运维·后端·elk·elasticsearch·云原生·自动化·kibana·日志收集
小天努力学java5 小时前
【面试系列】深入浅出 Spring
java·spring·面试
Javatutouhouduan6 小时前
如何系统全面地自学Java语言?
java·后端·程序员·编程·架构师·自学·java八股文
后端转全栈_小伵6 小时前
MySQL外键类型与应用场景总结:优缺点一目了然
数据库·后端·sql·mysql·学习方法
编码浪子7 小时前
Springboot高并发乐观锁
后端·restful
uccs7 小时前
go 第三方库源码解读---go-errorlint
后端·go
唐 城7 小时前
Solon v3.0.5 发布!(Spring 可以退休了吗?)
java·spring·log4j