如何设置 LoadBalancer 的负载均衡策略?

1.什么是 LoadBalancer?

负载均衡器(LoadBalancer)是一种用于在计算机网络或服务器集群中分配工作负载的设备或服务。就是确保每个服务器都能够有效地处理请求,防止某个服务器过载而导致性能下降或服务中断。

  • 服务器端的负载均衡。
  • 客户端负载均衡,如 SpringCloudLoadBalancer等。

1.1 负载均衡的策略

不管是服务器端还是客户端,它们的负载均衡策略都是相同的,下面是一些常见的策略。

  1. 轮询:按照顺序将每个新的请求分发给后端服务器,依次循环。这种策略适用于服务器性能相近,且请求处理时间相近的情况。
  2. 最小连接数:将新的请求分发到当前连接数最少的服务器上,以确保负载更均衡。
  3. 随机选择:随机选择一个后端服务器来处理每个新的请求。这种策略适用于服务器性能相似,且请求处理时间相近的情况,但不保证请求的分发是均匀的。
  4. 加权轮询:根据服务器的不同处理能力,给每个服务器分配不同的权重,使其能够接受相应权重数的请求。这种策略能确保高性能的服务器得到更多的使用率,避免低性能的服务器负载过重。
  5. 最短响应时间:将请求发送到具有最短响应时间的服务器上,以提高整体性能。
  6. IP 哈希:它使用客户端的 IP 地址信息来决定将请求分发到哪个服务器。对于给定的 IP 地址,通过哈希函数计算得到一个数值,然后根据这个数值将请求发送到相应的服务器上。

2.LoadBalancer默认的均衡策略

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

2.1 代码演示

  • 创建 comsumer 模块和一个 provider 模块,开启OpenFeign并连接 Nacos【详细步骤:Nacos + OpenFeign:微服务中的服务注册、发现与调用 - 掘金 (juejin.cn)】。

    • provider模块:
    java 复制代码
    @RequestMapping("/user")
    @RestController
    public class UserController {
        @Autowired
        private ServletWebServerApplicationContext context;
    
        @RequestMapping("/getname") 
        public String getName(@RequestParam("id")Integer id){
            //返回当前的端口号 + id
            return "Port:" + context.getWebServer().getPort() + " Provider-id: " + id;
        }
    }
    • comsumer模块:
    java 复制代码
    @RestController
    public class CallController {
        
        @Autowired
        private UserService userService;
        
        @RequestMapping("/getname") 
        public String getName(Integer id){
            return userService.getName(id);
        }
    }
    java 复制代码
    @Service
    @FeignClient("loadbalancer-service")
    public interface UserService {
        @RequestMapping("/user/getname") //调用 provider 模块的服务
        String getName(@RequestParam("id")Integer id);
    }
    • 多复制几个provider模块的配置:

全部启动,然后访问comsumer

刷新多次,就可以体现出轮询的策略。

2.2 源码分析

Spring Cloud LoadBalancer 的配置类 LoadBalancerClientConfiguration 中可以看到它的默认策略:

这个方法就是用来配置策略的,来看看ReactorLoadBalancer有哪些实现(Ctrl+H):

所以,Spring Cloud LoadBalancer内置了两种负载均衡策略:轮询负载均衡策略(默认)随机负载均衡策略NacosLoadBalancer是基于权重负载均衡策略 ,是由alibaba提供的。

3.设置随机负载策略

  1. 创建随机负载均衡器:这里跟LoadBalancerClientConfiguration中的源码一样,就是返回的类型变成了RandomLoadBalancer
java 复制代码
   //注意,这里不能加 @Configuration !!!
   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);
       }
   }
  1. 设置随机负载均衡器 (局部设置) :设置位置在OpenFeign接口定义上。
java 复制代码
@Service
@FeignClient("loadbalancer-service")
//name 为应用程序的名称(在 application.yml 配置文件中设置过)
//configuration 为自定义配置类的 Class 类
@LoadBalancerClient(name = "loadbalancer-service",configuration = RandomLoadBalancerConfig.class)
public interface UserService {
    @RequestMapping("/user/getname")
    String getName(@RequestParam("id")Integer id);
}
  1. 设置随机负载均衡器 (全局设置) :在启动类里面设置。
java 复制代码
@SpringBootApplication
@EnableFeignClients
//全局设置
@LoadBalancerClients(defaultConfiguration = RandomLoadBalancerConfig.class)
public class ComsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ComsumerApplication.class, args);
    }
}

局部与全局的区别:局部负载均衡策略是指仅对特定的服务或服务实例应用负载均衡策略。全局负载均衡策略是指将负载均衡策略应用于所有服务或服务实例。

4.设置权重负载均衡策略(Nacos负载均衡器)

  1. 创建Nacos负载均衡器
java 复制代码
@LoadBalancerClients(defaultConfiguration = NacosLoadBalancerConfig.class)
public class NacosLoadBalancerConfig {
    @Resource
    private NacosDiscoveryProperties nacosDiscoveryProperties; //这个类主要包含了与 Nacos 相关的配置信息

    @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); //这里多了一个参数
    }
}
  1. 设置随机负载均衡器 (局部设置)
java 复制代码
@Service
@FeignClient("loadbalancer-service")
@LoadBalancerClient(name = "loadbalancer-service",configuration = NacosLoadBalancerConfig.class)
public interface UserService {
    @RequestMapping("/user/getname")
    String getName(@RequestParam("id")Integer id);
}
  1. 设置随机负载均衡器 (全局设置) :在启动类里面设置。
java 复制代码
@SpringBootApplication
@EnableFeignClients
@LoadBalancerClients(defaultConfiguration = NacosLoadBalancerConfig.class)
public class ComsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ComsumerApplication.class, args);
    }
}

5.自定义负载均衡器

5.1 从零创建自定义负载均衡器

自定义负载均衡器可能很复杂,但是我们可以照猫画虎,这里模拟一个 IP 哈希策略,可以借鉴RandomLoadBalancer等均衡器来实现。我们先看看RandomLoadBalancer源码是怎么写的。

RandomLoadBalancer实现了ReactorServiceInstanceLoadBalancer接口,这个接口要实现choose方法:

这个choose方法就是用来实现选择服务实例的逻辑 了。这里就不实现了,直接把RandomLoadBalancer中的所有代码复制过去(看源码的时候可以借助GPT):

java 复制代码
public class HashLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private static final Log log = LogFactory.getLog(RandomLoadBalancer.class);
    private final String serviceId;
    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    public HashLoadBalancer(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) -> {
            // 处理选择的服务实例,返回一个 Response 对象
            return this.processInstanceResponse(supplier, serviceInstances);
        });
    }

    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) {
        // 通过服务实例列表获取 Response 对象
        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 {
            // 从服务实例列表中随机选择一个实例【这是核心,更改这里我们可以实现自定义负载策略】
            //int index = ThreadLocalRandom.current().nextInt(instances.size());
            //ServiceInstance instance = (ServiceInstance)instances.get(index);
            // 返回一个包含选定服务实例的响应对象
            return new DefaultResponse(instance);
        }
    }
}

可以看出来,getInstanceResponse是实现策略的逻辑,我们只需要更改getInstanceResponse方法就行了。

java 复制代码
    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 {
            // 【这是核心,更改这里我们可以实现自定义负载策略】
            //获取 Request 对象
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            String ipAddress = request.getRemoteAddr(); //ip地址
            int hash = ipAddress.hashCode();
            int index = hash % instances.size();//计算下标
            ServiceInstance instance = instances.get(index);//获取对应的实例
            // 返回一个包含选定服务实例的响应对象
            return new DefaultResponse(instance);
        }
    }

完整的代码:

java 复制代码
public class HashLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    private static final Log log = LogFactory.getLog(RandomLoadBalancer.class);
    private final String serviceId;
    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;

    public HashLoadBalancer(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) -> {
            // 处理选择的服务实例,返回一个 Response 对象
            return this.processInstanceResponse(supplier, serviceInstances);
        });
    }

    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) {
        // 通过服务实例列表获取 Response 对象
        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 {
            // 【这是核心,更改这里我们可以实现自定义负载策略】
            //获取 Request 对象
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            String ipAddress = request.getRemoteAddr(); //ip地址
            int hash = ipAddress.hashCode();
            int index = hash % instances.size();//计算下标
            ServiceInstance instance = instances.get(index);//获取对应的实例
            // 返回一个包含选定服务实例的响应对象
            return new DefaultResponse(instance);
        }
    }
}

5.2 设置自定义负载均衡器

同样的道理,直接复制。

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

这里只演示局部设置,全局设置与上面是一样的。

java 复制代码
@Service
@FeignClient("loadbalancer-service")
@LoadBalancerClient(name = "loadbalancer-service",configuration = HashLoadBalancerConfig.class)
public interface UserService {
    @RequestMapping("/user/getname")
    String getName(@RequestParam("id")Integer id);
}

6.设置 LoadBalancer 缓存

LoadBalancer 缓存指的是负载均衡器在运行时缓存的服务实例信息,包括服务的主机名、端口、状态、权重等。会缓存负载均衡器已知的服务实例列表,这些实例可以是同一服务的不同节点,用于分发请求。

LoadBalancer默认是开启了缓存服务列表的功能的,其中过期时间默认为 35s(每35s向注册中心获取列表),最大缓存个数为 256个。

设置缓存配置:

yml 复制代码
spring:
  cloud:
    loadbalancer:
      cache:
        enabled: false #关闭缓存(为 true 时,表示开启,默认为 true)
        ttl: 20 # 缓存存活时间
        capacity: 300 # 缓存的最大个数
相关推荐
渣哥1 小时前
原来 Java 里线程安全集合有这么多种
java
间彧1 小时前
Spring Boot集成Spring Security完整指南
java
间彧1 小时前
Spring Secutiy基本原理及工作流程
java
Java水解2 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆5 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学5 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole5 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端
华仔啊5 小时前
基于 RuoYi-Vue 轻松实现单用户登录功能,亲测有效
java·vue.js·后端
程序员鱼皮6 小时前
刚刚 Java 25 炸裂发布!让 Java 再次伟大
java·javascript·计算机·程序员·编程·开发·代码