负载均衡 -LoadBalance

目录

问题分析

负载均衡

服务端负载均衡

客户端负载均衡

[Spring Cloud LoadBalancer](#Spring Cloud LoadBalancer)

自定义负载均衡策略

实现原理


问题分析

服务注册与发现------Eureka-CSDN博客 中,我们根据应用名称 获取了服务实例列表,并从列表中选择了一个服务实例:

若一个服务对应多个实例,是否能够将流量合理的分配到多个实例呢?

我们启动多个 product-service 实例

修改端口号:

再添加一个实例,并启动:

观察 Eureka,可以看到 product-service 中有三个实例:

此时,我们多次访问 127.0.0.1:8080/order/1

可以看到,多次访问的都是同一台机器,我们启动多个实例,就是希望能够减轻单机压力,也就是每个实例处理部分请求,而不是让同一台机器处理所有请求

那么,如何实现多个机器分摊负荷呢?

我们可以依次将请求分发给服务器列表中的每一台机器 ,因此,我们对 OrderService中代码进行修改:

java 复制代码
@Slf4j
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private DiscoveryClient discoveryClient;

    // 当前选择实例
    private static AtomicInteger atomicInteger = new AtomicInteger(1);
    // 实例列表
    private static  List<ServiceInstance> instances;

    @PostConstruct
    public void init() {
        // 获取服务列表
        instances = discoveryClient.getInstances("product-service");
    }
    public OrderInfo findOrderInfoById(Integer orderId) {
        OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
        // 计算当前访问实例
        int index = atomicInteger.getAndIncrement() % instances.size();
        EurekaServiceInstance instance = (EurekaServiceInstance)instances.get(index);
        log.info("选择实例: " + instance.getInstanceId());
        // 拼接 URL
        String url = instance.getUri() + "/product/" + orderInfo.getProductId();
        ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
        orderInfo.setProductInfo(productInfo);
        return orderInfo;
    }
}

多次访问 127.0.0.1:8080/order/1

此时请求被均衡地分配到了不同实例上,上述这种方式,就是负载均衡

负载均衡

负载均衡(Load Balance,简称 LB) 是高并发、高可用系统必不可少的关键组件,目的是将网络流量或计算任务智能地分配到多个服务器(或资源) 上**,** 从而 提高系统性能 (响应更快)、增强可用性与可靠性 (一台服务器宕机,其他还能继续服务)、提升可扩展性 (通过增加服务器来应对更多请求)以及 避免单点过载(防止某台服务器被压垮)

我们通过一个生活中的示例来理解:

想象你有一家热门奶茶店,若只有一个收银员时,此时队伍会排得很长

而如果开了 3 个收银台,并有一个引导员把顾客平均分配到各窗口 ,整体效率就大幅提升 ------ 这个"引导员"就是负载均衡器

负载均衡的核心作用

1. 分发请求:把用户请求(如 HTTP 请求)分给后端多个服务器。

2. 健康检查:自动检测服务器是否宕机,剔除故障节点。

3. 缓存与压缩:部分负载均衡器还能缓存静态内容,加速响应

常见负载均衡策略

策略 说明
轮询(Round Robin) 依次轮流分配请求(最简单常用)
加权轮询 性能强的服务器分配更多请求(如 A 权重 3,B 权重 1)
最少连接 把请求发给当前连接数最少的服务器
IP Hash 根据用户 IP 固定分配到某台服务器(实现会话保持)
响应时间优先 选择响应最快、负载最低的服务器

负载均衡的实现可分为服务端负载均衡客户端负载均衡, 这两种不同的流量分发策略**,** 核心区别在于:"谁来决定请求发给哪台后端服务器?"

服务端负载均衡

独立的负载均衡器(如 Nginx、云 LB) 位于客户端和后端服务之间,统一接收所有请求并分发到后端服务器

客户端负载均衡

客户端自己决定 将请求发给哪一台后端服务器。客户端需事先获取服务实例列表 (通常通过服务注册中心),并在本地执行负载均衡算法

接下来,我们来学习 Spring Cloud LoadBalance

Spring Cloud LoadBalancer

Spring Cloud LoadBalancerSpring Cloud 提供的一个客户端负载均衡器 ,用于在微服务架构中,让服务消费者(客户端)能够从多个服务实例中智能选择一个可用实例进行调用

在 Spring Cloud Netflix Ribbon 被弃用后,Spring Cloud LoadBalancer 成为了官方推荐的替代方案

要使用 Spring Cloud LoadBalancer 实现负载均衡策略非常简单,只需要给 RestTemplate 添加 @LoadBalanced 注解即可

java 复制代码
@Configuration
public class BeanConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

IP 和端口号 修改为服务名称

java 复制代码
@Slf4j
@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RestTemplate restTemplate;
    
    public OrderInfo findOrderInfoById(Integer orderId) {
        OrderInfo orderInfo = orderMapper.selectOrderById(orderId);
        // 访问 URL
        String url = "http://product-service/product/" + orderInfo.getProductId();
        ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);
        orderInfo.setProductInfo(productInfo);
        return orderInfo;
    }
}

多次发起请求 127.0.0.1:8080/order/1,观察 product-service 的日志,可以看到请求被分配到这三个实例上:

Spring Cloud LoadBalancer 仅支持两种负载均衡策略:轮询策略随机策略 ,默认的负载均衡策略是 轮询策略RoundRobinLoadBalancer

若需要采用随机策略,也非常简单

自定义负载均衡策略

定义随机策略对象,并通过**@Bean** 将其加载到 Spring 容器中:

java 复制代码
public class CustomLoadBalancerConfiguration {
    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
                                                            LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(loadBalancerClientFactory
                .getLazyProvider(name, ServiceInstanceListSupplier.class),
                name);
    }
}

使用**@LoadBalancerClient**注解配置上述随机策略:

java 复制代码
@LoadBalancerClient(name = "product-service", configuration = CustomLoadBalancerConfiguration.class)
@Configuration
public class BeanConfig {
    @LoadBalanced
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

name:配置的负载均衡策略对哪个服务生效

configuration:使用的负载均衡策略

多次发起请求 127.0.0.1:8080/order/1,观察product-service 的日志:

此时请求分布接近均匀

那么 Spring Cloud LoadBalancer 具体是如何实现负载均衡的呢?接下来我们就来看 Spring Cloud LoadBalancer 的具体实现

实现原理

Spring Cloud LoadBalancer 主要是通过 LoadBalancerInterceptor 来实现的,LoadBalancerInterceptor 会对 RestTemplate请求进行拦截 ,然后从 Eureka 根据服务 id 获取服务列表 ,最后根据负载均衡算法 得到真实的服务地址信息 ,并替换服务 id

我们来看 LoadBalancerInterceptor的具体实现:

java 复制代码
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

	private LoadBalancerClient loadBalancer;

	private LoadBalancerRequestFactory requestFactory;

	@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
			final ClientHttpRequestExecution execution) throws IOException {
        // 从请求中获取 url, 如 http://product-service/product/1001
		final URI originalUri = request.getURI();
        // 获取路径主机名, 也就是服务id, 即 product-service
		String serviceName = originalUri.getHost();
        // 判断 serviceName 是否为空
		Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        // 根据服务 id 进行负载均衡,并处理请求
		return this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
	}
}

继续看 execute实现:

java 复制代码
public class BlockingLoadBalancerClient implements LoadBalancerClient {

	private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory;

	@Override
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
		String hint = getHint(serviceId);
		LoadBalancerRequestAdapter<T, TimedRequestContext> lbRequest = new LoadBalancerRequestAdapter<>(request,
				buildRequestContext(request, hint));
		Set<LoadBalancerLifecycle> supportedLifecycleProcessors = getSupportedLifecycleProcessors(serviceId);
		supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
		// 根据 serviceId 和 lbRequest(负载均衡策略)选择服务
        ServiceInstance serviceInstance = choose(serviceId, lbRequest);
		if (serviceInstance == null) {
			supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
					new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, new EmptyResponse())));
			throw new IllegalStateException("No instances available for " + serviceId);
		}
		return execute(serviceId, serviceInstance, lbRequest);
    }

    @Override
	public <T> ServiceInstance choose(String serviceId, Request<T> request) {
        // 获取负载均衡器
		ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId);
		if (loadBalancer == null) {
			return null;
		}
        // 根据负载均衡算法,从列表中选择一个服务实例
		Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block();
		if (loadBalancerResponse == null) {
			return null;
		}
		return loadBalancerResponse.getServer();
	}
}

我们继续看不同负载均衡策略选择 实现,先来看轮询策略RoundRobinLoadBalancer)实现:

java 复制代码
public class RoundRobinLoadBalancer implements ReactorServiceInstanceLoadBalancer {
    	@Override
	public Mono<Response<ServiceInstance>> choose(Request request) {
		ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
				.getIfAvailable(NoopServiceInstanceListSupplier::new);
        // 通过 processInstanceResponse 进行处理
		return supplier.get(request).next()
				.map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
	}
    
    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
			List<ServiceInstance> serviceInstances) {
        // 调用 getInstanceResponse 获取选择的实例
		Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances);
		if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
			((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
		}
		return serviceInstanceResponse;
	}

    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
		if (instances.isEmpty()) {
			if (log.isWarnEnabled()) {
				log.warn("No servers available for service: " + serviceId);
			}
			return new EmptyResponse();
		}
		if (instances.size() == 1) {
			return new DefaultResponse(instances.get(0));
		}
        // 原子性地增加并返回新值 同时 任何数字 & MAX_VALUE = 保留低31位,清除符号位
		int pos = this.position.incrementAndGet() & Integer.MAX_VALUE;
        // 将无限增长的 pos 映射到有限的实例范围内
		ServiceInstance instance = instances.get(pos % instances.size());
		return new DefaultResponse(instance);
	}
}

再来看随机策略RandomLoadBalancer)实现:

java 复制代码
public class RandomLoadBalancer implements ReactorServiceInstanceLoadBalancer {
	@Override
	public Mono<Response<ServiceInstance>> choose(Request request) {
		ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider
				.getIfAvailable(NoopServiceInstanceListSupplier::new);
        // 通过 processInstanceResponse 进行处理
		return supplier.get(request).next()
				.map(serviceInstances -> processInstanceResponse(supplier, serviceInstances));
	}

	private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,
			List<ServiceInstance> serviceInstances) {
        // 调用 getInstanceResponse 获取选择的实例
		Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances);
		if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
			((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());
		}
		return serviceInstanceResponse;
    }

	private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
		if (instances.isEmpty()) {
			if (log.isWarnEnabled()) {
				log.warn("No servers available for service: " + serviceId);
			}
			return new EmptyResponse();
		}
        // ThreadLocalRandom.current(): 获取当前线程的随机数生成器
        // instances.size(): 获取可用服务实例的数量
        // nextInt(bound): 生成 [0, bound) 范围内的随机整数
        // 生成一个介于0(包含)和instances.size()(不包含)之间的随机整数
		int index = ThreadLocalRandom.current().nextInt(instances.size());

		ServiceInstance instance = instances.get(index);

		return new DefaultResponse(instance);
	}

}
相关推荐
milanyangbo32 分钟前
深入解析 Disruptor:从RingBuffer到缓存行填充的底层魔法
java·数据库·后端·架构
计算机学姐44 分钟前
基于Python的智能点餐系统【2026最新】
开发语言·vue.js·后端·python·mysql·django·flask
今天你TLE了吗1 小时前
Java:基于注解实现去重表消息防止重复消费
java·spring boot·分布式·spring cloud·幂等
没有bug.的程序员1 小时前
大规模微服务下的 JVM 调优实战指南
java·jvm·spring·wpf·延迟
哈哈老师啊1 小时前
Springboot学生选课系统576i3(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
IT_陈寒1 小时前
React性能优化实战:5个被低估的Hooks技巧让你的应用提速30%
前端·人工智能·后端
回家路上绕了弯1 小时前
CAP 与 BASE:分布式系统的核心思想与实践指南
分布式·后端
CappuccinoRose1 小时前
Docker配置过程完整梳理
后端·python·docker·容器·环境配置
I'm Jie1 小时前
Java 字节码工具 ASM,实现类的动态增强
java·spring boot·spring·asm·cglib·class