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 个。

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

相关推荐
程序员爱钓鱼9 分钟前
Rust 开发环境安装(Windows|macOS|Linux)
后端·rust
爱勇宝8 小时前
第 1 章:别把“需求文档”当成真正的需求
前端·后端·程序员
IT_陈寒13 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码14 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo14 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志15 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..15 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects15 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰15 小时前
一些基础和问题解决
后端
陈随易15 小时前
前端项目部署只要30秒
前端·后端·程序员