Spring-Cloud 学习笔记-(4)负载均衡器Ribbon
1、前言
-
上个章节我们做了什么?
上个章节我们说了用eureka来实现服务的注册与发现,并且用过服务的seviceId拉取了服务列表
List<ServiceInstance> instances = discoveryClient.getInstances("user-service")
从而实现服务的调用方(order-service)调用服务的提供方(user-service) -
本章节我们会做什么?
负载均衡
2、什么是负载均衡
2.1、问题分析
上个章节我们实现了服务之间的调用,把原来代码里面写死的ip地址,换成用serviceId拉取服务列表,然后从服务列表中获取实例的方式,虽然代码变得复杂了,但是思想上我们得到的升级,但是还是存在一个问题,就是ServiceInstance instance = instances.get(0);
每次我们都取同一个ip,要想每次使用不同的ip,我们自己就要写负载均衡算法,从多个实例当中获取某一个实例进行调用,这次我们这一章节讲的就是负载均衡器Ribbon
,它里面内置了很多负载均衡的算法,帮我们从实例列表中获取一个实例。
2.2、什么是Ribbon
3、快速入门
我们按照上节课方法启动,一个Eureka注册中心,两个(方便演示负载均衡)服务的提供方(user-service),最后我们修改服务调用方(order-service)代码
pom文件
<!-- ribbon -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
3.1、实现方式一
3.1.1、修改代码
//注入 RibbonLoadBalancerClient
@Autowired
private LoadBalancerClient client;
...
//通过serviceId 拉取服务列表
//List<ServiceInstance> instances = discoveryClient.getInstances("user-service");
//ServiceInstance instance = instances.get(0);
ServiceInstance instance = client.choose("user-service");
String jsonStr = restTemplate.getForObject("http://"+instance.getHost()+":"+instance.getPort()+"/api/v1/user/2", String.class);
之前我们是通过serviceId(“user-service”)获取到的是服务列表,现在我们直接可以通过serviceId,返回的是单个实例,不是因为列表里面只有一个实例,是因为choose
方法中已经帮我们做了复杂均衡了。
3.2、实现方式二
3.2.1、启动类
/**
* 把RestTemplate注入到Spring容器中
*/
@Bean
@LoadBalanced //增加注解 让RestTemplate内置一个负载均衡器
public RestTemplate restTemplate(){
return new RestTemplate();
}
3.2.2、调用代码
String jsonStr = restTemplate.getForObject("http://user-service/api/v1/user/2", String.class);
之前我们把serviceId交出去,别人帮我们取一堆,或者取一个,现在直接把serviceId写在url路径中。
3.2.3、测试
启动order-service
访问:http://localhost:8781/api/v1/order/2
3.2.4、实现原理
其实实现方式二低层就是用的实现方式一,只不过实现方式二加了一个拦截器LoadBalancerInterceptor
对RestTemplate请求做了拦截,然后把请求路径中的serviceId("user-service")拿到,然后通过方式一获取某一个实例进行调用。
3.2.5、断点调式
我们按两下shift搜索到LoadBalancerInterceptor
这个拦截器,在intercept方法中打好断点
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
//1.拿到请求路径
URI originalUri = request.getURI();
//2.从路径中获取serviceId
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
//3.执行execute方法.. 我们接着看execute方法
return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
}
// RibbonLoadBalancerClient类中
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
//4.根据serviceId获取到负载均衡器
ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
//5.通过负载均衡器拿到某一个实例..我们接着看getServer方法
Server server = this.getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
} else {
RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
return this.execute(serviceId, ribbonServer, request);
}
}
//RibbonLoadBalancerClient类中
protected Server getServer(ILoadBalancer loadBalancer) {
//6.判断负载均衡器是不是为空,不为空调用chooseServer("default")方法,我们接着看
return loadBalancer == null ? null : loadBalancer.chooseServer("default");
}
//BaseLoadBalancer类中
public Server chooseServer(Object key) {
//这里key是默认值“default”
if (this.counter == null) {
this.counter = this.createCounter();
}
this.counter.increment();
if (this.rule == null) {
return null;
} else {
try {
//根据rule调用choose方法,其中IRule是一个接口,有很多实现类,每一个实现类对于不同的负载均衡策略,比如RandomRule随机,RoundRobinRule轮询等,我们BaseLoadBalancer类中有一个属性, private static final IRule DEFAULT_RULE = new RoundRobinRule();代表默认轮询策略,有兴趣的可以看一下每一个实现的choose方法,比如轮询策略RoundRobinRule,底层维护一个自增长的count,每调用一次count++,然后每次用count模于服务列表的长度(比如第一次:1%5=1,第二次:2%5=2)得到的值为服务列表的位置索引,从而实现轮询。
return this.rule.choose(key);
} catch (Exception var3) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", new Object[]{this.name, key, var3});
return null;
}
}
}
3.3、修改轮询策略
application.yml:
#--负载均衡轮询策略
# serviceId
user-service:
ribbon:
#负载均衡策略的className
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
3.4、重载机制
3.4.1、为什么要有重载机制
在上一章中我们说到,一个正常的eureka客户端,每间隔30秒没有给服务器发送心跳,如果90秒服务器还没有收到心跳,服务器就会认为这个客户端已经宕机,但是eureka不会马上剔除,没间隔60会同意剔除这些失效的客户端,这样导致,我们服务实际上已经宕机了但是服务列表里面还有,这样服务的消费者在调用的时候就会报错,假设我们服务的提供方有五个,虽然只宕机了一台,但是还有四台是正常的,在这个时候我们条用这个服务如果出现报错信息肯定不是我们希望看到的,所有我们就有了这个重载机制,Spring Cloud 整合了Spring Retry 来增强RestTemplate的重试能力,当一次服务调用失败后,不会立即抛出一次,而是再次重试下一个服务。
3.4.2、实现代码
application.yml
#服务名称
spring:
application:
name: order-service
cloud:
loadbalancer:
retry:
enabled: true # 开启Spring Cloud的重试功能
#负载均衡轮询策略
user-service:
ribbon:
#负载均衡策略的className
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
ConnectTimeout: 250 # Ribbon的连接超时时间
ReadTimeout: 1000 # Ribbon的数据读取超时时间
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试
MaxAutoRetriesNextServer: 1 # 切换实例的重试次数
MaxAutoRetries: 1 # 对当前实例的重试次数
我们测试发现,就算我们user-service宕机了,也能通过另一台服务实例获取到结果!