负载均衡-LoadBalance
前提回顾:
order-service远程调用的代码是这样写的,先是根据应用名称获取了服务实例列表,接着从列表中选择了第一个服务实例。
List<ServiceInstance> instances = discoveryClient.getInstances("product-service");
String uri = instances.get(0).getUri().toString();
也就是说,我可能是不止一个服务实例,那么问题来了,如果我一个服务对应着多个实例,那流量是否可以合理的分配到多个实例呢?
我们现在来做个简单的测试:
我们再启动2个product-service实例,先选中要启动的服务,右键选择 Copy Configuration
添加 VM options : -Dserver.port=9091
其中9091是服务启动的端口号,可以根据情况修改。
同样的操作,在创建一个,现在就一共有了三个product-service服务。
然后右键选中要启动的项目,点击 run
从eureka中我们看到product-service有三个实例
多次访问:http://127.0.0.1:8080/order/1 看看日志情况
通过日志我们发现,多次请求,访问的都是同一台机器。
这与我们的预期相差太多,我们启动多个实例,是希望可以分担负荷,那该如何实现呢?
解决办法:
修改一下order-service中service层的代码
//计数器,使用原子类,保证线程安全。private AtomicInteger count = new AtomicInteger(1);private List<ServiceInstance> instances;//保证eureka返回来的实例是固定的。@PostConstructpublic void init(){//从Eureka中获取服务列表instances = discoveryClient.getInstances("product-service");}public OrderInfo selectOrderById(Integer orderId){OrderInfo orderInfo = orderMapper.selectOrderById(orderId);//计算轮流的实例indexint index = count.getAndIncrement() %instances.size();//获取实例String uri = instances.get(index).getUri().toString();//拼接urlString url = uri + "/product/" + orderInfo.getProductId();log.info("远程调用url:{}",url);ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);orderInfo.setProductInfo(productInfo);return orderInfo;}
思路:
通过日志可以看出来,请求被平均的分配到不同的实例上,这就是负载均衡。
负载均衡
负载均衡(Load Balance,简称 LB) , 是⾼并发, ⾼可⽤系统必不可少的关键组件。
当服务流量增⼤时, 通常会采⽤增加机器的⽅式进⾏扩容, 负载均衡就是⽤来在多个机器或其他资源中, 按照⼀定的规则(负载均衡策略)合理分配负载。
负载均衡分为 服务端负载均衡 和 客户端负载均衡。
刚才在上面例子是采用的轮询的方式来实现的负载均衡。当然还是有其他的方式来实现负载均衡,来应对不同的场景。
一些负载均衡算法:
算法 | 工作原理 | 适用场景 |
---|---|---|
轮询 (Round Robin) | 依次分配请求 | 服务器性能均等时 |
加权轮询 | 按权重比例分配请求 | 服务器配置不一时 |
最少连接 | 选择当前连接数最少的服务器 | 长连接服务(如WebSocket) |
源IP哈希 | 相同客户端IP分配到相同服务器 | 需要会话保持的应用 |
响应时间加权 | 优先选择响应最快的服务器 | 对延迟敏感的服务 |
服务多机部署时, 开发⼈员都需要考虑负载均衡的实现, 所以也出现了⼀些负载均衡器, 来帮助我们实
现负载均衡。
服务端负载均衡
在服务端进⾏负载均衡的算法分配.
⽐较有名的服务端负载均衡器是Nginx. 请求先到达Nginx负载均衡器, 然后通过负载均衡算法, 在多个
服务器之间选择⼀个进⾏访问。
客户端负载均衡
在客⼾端进⾏负载均衡的算法分配。
把负载均衡的功能以库的⽅式集成到客⼾端, ⽽不再是由⼀台指定的负载均衡设备集中提供。
⽐如Spring Cloud的Ribbon, 请求发送到客⼾端, 客⼾端从注册中⼼(⽐如Eureka)获取服务列表, 在发
送请求前通过负载均衡算法选择⼀个服务器,然后进⾏访问。
Ribbon是Spring Cloud早期的默认实现, 由于不维护了, 所以最新版本的Spring Cloud负载均衡集成的
是Spring Cloud LoadBalancer(Spring Cloud官⽅维护)。
客⼾端负载均衡和服务端负载均衡最⼤的区别在于服务清单所存储的位置。
Spring Cloud LoadBalancer实现负载均衡
1.添加注解
给RestTemplate 这个 Bean 添加 @Loadbalanced
注解
@Configuration
public class BeanConfig {@Bean@LoadBalancedpublic RestTemplate restTemplate(){return new RestTemplate();}}
2.修改远程调用代码,把IP和端口号改成应用名
public OrderInfo selectOrderById(Integer orderId){OrderInfo orderInfo = orderMapper.selectOrderById(orderId);String url = "<http://product-service/product/>" + orderInfo.getProductId();log.info("远程调用url:{}",url);ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);orderInfo.setProductInfo(productInfo);return orderInfo;}
3.测试用例
先启动多个product-service实例,再多次访问http://127.0.0.1:8080/order/1
观察日志,就会发现请求被平均的分配到每个实例上了。
Spring Cloud LoadBalancer负载均衡策略
负载均衡策略是⼀种思想, ⽆论是哪种负载均衡器, 它们的负载均衡策略都是相似的。 Spring Cloud
LoadBalancer 仅⽀持两种负载均衡策略: 轮询策略 和 随机策略。
-
轮询(Round Robin): 轮询策略是指服务器轮流处理⽤⼾的请求. 这是⼀种实现最简单, 也最常⽤的
策略. ⽣活中也有类似的场景, ⽐如学校轮流值⽇, 或者轮流打扫卫⽣。
-
随机选择(Random): 随机选择策略是指随机选择⼀个后端服务器来处理新的请求。
自定义随机负载均衡策略
Spring Cloud LoadBalancer 默认负载均衡策略是 轮询策略, 实现是 RoundRobinLoadBalancer, 如果
服务的消费者如果想采⽤随机的负载均衡策略, 也⾮常简单。
参考官⽹地址:Spring Cloud LoadBalancer :: Spring Cloud Commons
-
定义随机算法对象,通过
@Bean
将其加载到Spring容器中此处使用 SpringCloudLoadBalancer提供的RandomLoadBalancer
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 CustomLoadBalancerConfiguration {@BeanReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,LoadBalancerClientFactory loadBalancerClientFactory) {String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);return new RandomLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),name);}
}
<aside> 💡
注意:该类需要满足的条件:
-
不用
@Configuration
注解 -
在组件扫描范围内 </aside>
-
使用
@LoadBalancerClient
或者@LoadBalancerClients
注解
在RestTemplate 配置类上方,使用 @LoadBalancerClient
或者 @LoadBalancerClients
注解,可以对不同的服务提供方配置不同的客户端负载均衡算法策略。
@LoadBalancerClient
:一个服务的提供者。
@LoadBalancerClients
:多个服务的提供者。
@LoadBalancerClient(name = "product-service" , configuration = CustomLoadBalancerConfiguration.class)
@Configuration
public class BeanConfig {@Bean@LoadBalancedpublic RestTemplate restTemplate(){return new RestTemplate();}}
其中:
- name:该负载均衡策略对哪个服务生效(服务提供方)
- configuration:该负载均衡策略 用哪个负载均衡策略实现。
LoadBalancer 原理(不发)
LoadBalancer 的实现, 主要是 LoadBalancerInterceptor , 这个类会对 RestTemplate 的请求进⾏拦截, 然后从Eureka根据服务id获取服务列表,随后利⽤负载均衡算法得到真实的服务地址信息,替换服务id。 我们来看看源码实现:
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {//...public ClientHttpResponse intercept(final HttpRequest request, finalbyte[] body, final ClientHttpRequestExecution execution) throws IOException {URI originalUri = request.getURI();String serviceName = originalUri.getHost();Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));}
}
可以看到这⾥的intercept⽅法, 拦截了⽤⼾的HttpRequest请求,然后做了⼏件事:
- request.getURI() 从请求中获取uri, 也就是 http://product-service/product/1001
- originalUri.getHost() 从uri中获取路径的主机名, 也就是服务id, product-service
- loadBalancer.execute 根据服务id, 进⾏负载均衡, 并处理请求
点进去继续跟踪
public class BlockingLoadBalancerClient implements LoadBalancerClient {public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {String hint = this.getHint(serviceId);LoadBalancerRequestAdapter<T, TimedRequestContext> lbRequest = new LoadBalancerRequestAdapter(request, this.buildRequestContext(request, hint));Set<LoadBalancerLifecycle> supportedLifecycleProcessors = this.getSupportedLifecycleProcessors(serviceId);supportedLifecycleProcessors.forEach((lifecycle) -> lifecycle.onStart(lbRequest));//根据serviceId,和负载均衡策略, 选择处理的服务ServiceInstance serviceInstance = this.choose(serviceId, lbRequest);if (serviceInstance == null) {supportedLifecycleProcessors.forEach((lifecycle) -> lifecycle.onComplete(new CompletionContext(Status.DISCARD, lbRequest, new EmptyResponse())));throw new IllegalStateException("No instances available for " + serviceId);} else {return (T)this.execute(serviceId, serviceInstance, lbRequest);}}/*** 根据serviceId,和负载均衡策略, 选择处理的服务**/public <T> ServiceInstance choose(String serviceId, Request<T> request) {//获取负载均衡器ReactiveLoadBalancer<ServiceInstance> loadBalancer = this.loadBalancerClientFactory.getInstance(serviceId);if (loadBalancer == null) {return null;} else {//根据负载均衡算法, 在列表中选择⼀个服务实例Response<ServiceInstance> loadBalancerResponse = (Response)Mono.from(loadBalancer.choose(request)).block();return loadBalancerResponse == null ? null : (ServiceInstance)loadBalancerResponse.getServer();}}}