文章目录
Spring Cloud(07)——Ribbon负载均衡服务的介绍和使用
在Spring Cloud(04)——Eureka的简介和部署使用、Spring Cloud(05)——Spring cloud整合Zookeeper代替Eureka和Spring Cloud(06)——Consul的介绍和使用中,我们分别使用了Eureka、Zookeeper和Consul实现了服务注册与发现的功能,并分析了三个注册中心的异同点。下面开始实现服务消费者向远程的服务提供者发送请求时,实现负载均衡算法和服务调用。
1、Ribbon简介
-
Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。通过 Spring Cloud 的封装,可以让我们轻松地将面向服务的 REST 模版请求自动转换成客户端负载均衡的服务调用。
-
Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、配置中心、API 网关那样需要独立部署,但是它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。因为微服务间的调用,API 网关的请求转发等内容,实际上都是通过 Ribbon 来实现的。
负载均衡(LB:LoadBalancer)
负载均衡:将用户的请求平摊的分配到多个服务上,从而达到系统的高可用(HA)。
Ribbon本地负载均衡客户端 VS Ngisx服务端负载均衡区别:
- Nginx是服务器负载均衡,客户单虽有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是有服务端实现的。
- Ribbon本地负载均衡,在调用微服务接口的时候,会在注册中心获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。
2、Ribbon的负载均衡和RestTemplate调用
Ribbon一句话总结就是:负载均衡 + RestTemplate调用
Ribbon的负载均衡演示
Ribbon的负载均衡我们在Spring Cloud(04)——Eureka的简介和部署使用中就演示了,那时我们实现的负载均衡策略是轮询,为什么在eureka中就可以实现ribbon负载均衡呢?
那是因为我在引入spring-cloud-starter-netflix-eureka-client依赖时,自动帮我们引入了ribbon依赖:
RestTemplate
RestTemplate中**getForObject()和getForEntity()**的区别:
- getForObject():返回对象为响应体中数据转化成的对象,基本可以理解为json
- getForEntity():返回对象为ResponseEntity对象,包含了相应中的一些中要信息,比如响应头。响应状态码、响应体等
3、Ribbon核心组件IRule
Ribbon 作为一款客户端负载均衡框架,默认的负载策略是轮询,同时也提供了很多其他的策略,能够让用户根据自身的业务需求进行选择。
IRule:根据特点算法从服务列表中选取一个要访问的服务
整体负载策略如下表所示:
策略类 | 命名 | 描述 |
---|---|---|
RandomRule | 随机策略 | 随机选择server |
RoundRobinRule | 轮询策略 | 轮询选择, 轮询index,选择index对应位置的Server; |
RetryRule | 重试策略 | 对选定的负载均衡策略机上重试机制,在一个配置时间段内当选择Server不成功,则一直尝试使用subRule的方式选择一个可用的server; |
BestAvailableRule | 最低并发策略 | 逐个考察server,如果server断路器打开,则忽略,再选择其中并发链接最低的server |
ResponseTimeWeightedRule | 响应时间加权重策略 | 根据server的响应时间分配权重,响应时间越长,权重越低,被选择到的概率也就越低。响应时间越短,权重越高,被选中的概率越高,这个策略很贴切,综合了各种因素,比如:网络,磁盘,io等,都直接影响响应时间 |
AvailabilityFilteringRule | 可用过滤策略 | 过滤掉一直失败并被标记为circuit tripped的server,过滤掉那些高并发链接的server(active connections超过配置的阈值)或者使用一个AvailabilityPredicate来包含过滤server的逻辑,其实就就是检查status里记录的各个Server的运行状态; |
ZoneAvoidanceRule | 区域权重策略 | 综合判断server所在区域的性能,和server的可用性,轮询选择server并且判断一个AWS Zone的运行性能是否可用,剔除不可用的Zone中的所有server |
4、替换负载均衡策略
1、修改cloud-consumer-order80模块
自定义一个配置类,注意:这个自定义配置类不能放在@ComponentScan所扫描的当前包以及子包下,否则我们自定义的这个配置类就会被所有Robbon 客户端所共享,达不到特殊化的目的了。
目录结构如下:
2、编写自定义的配置类
@Configuration
public class MySelfRule {
@Bean
public IRule myRule(){
return new RandomRule();//定义负载策略为随机
}
}
3、主启动类加注解@RibbonClient
@SpringBootApplication
@EnableEurekaClient
//name = "CLOUD-PAYMENT-SERVICE"服务提供者名称 configuration = MySelfRule.class 使用客户端自己选择的负载策略
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class,args);
}
}
4、测试
- 启动cloud-eureka-server7001和cloud-eureka-server7002模块
- 启动cloud-provider-payment8001和cloud-provider-payment8002模块
- 启动cloud-consumer-order80模块
访问请求:http://localhost/consumer/payment/get/1
第一次访问:8002端口
第二次访问:还是8002
第三次访问:8001
到这里我们就实现了自定义负载策略 / 算法,如果想用其他几种策略,稍作修改即可。
5、深入理解Ribbon负载均衡算法
5.1、默认负载策略轮询的原理
轮询原理:rest接口第几次请求数 % 服务集群总数量 = 实际调用服务器位置下标
注:每次服物重启后,rest接口计数从1开始
5.2、RoundRobinRule轮询源码
RoundRobinRule的源码如下(只粘贴实现负载的代码):
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();//获得活着的服务
List<Server> allServers = lb.getAllServers();//获得全部的服务
int upCount = reachableServers.size();//获得活着的服务数量
int serverCount = allServers.size();//获得全部的服务的数量
//如果活着的服务和全部的服务为0
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
int nextServerIndex = incrementAndGetModulo(serverCount);//CAS + 自旋锁获得服务下标
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
5.3、自定义实现轮询算法
分析完源码后,我们按照源码的思路来自定义实现轮询算法。
1、启动cloud-eureka-server7001和cloud-eureka-server7002模块
2、修改cloud-provider-payment8001和cloud-provider-payment8002模块的controller
在两个模块的controller上都增加下面方法:
@GetMapping(value = "/payment/lb")
public String getPaymentLb(){
return serverPort;
}
3、修改cloud-consumer-order80模块
-
注释掉@LoadBalanced注解:不使用ribbon的负载均衡策略
@Configuration public class ConfigBean { @Bean //@LoadBalanced public RestTemplate getRestTemplate(){ return new RestTemplate(); } }
4、新建LoadBalancer接口
LoadBalancer接口:
public interface LoadBalancer {
//获得服务所有的实例
ServiceInstance instances(List<ServiceInstance> serviceInstanceList);
}
编写LoadBalancer接口的实现类LoadBalancerImpl:
package com.cheng.springcloud.lb;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@Component
public class LoadBalancerImpl implements LoadBalancer{
//AtomicInteger是一个提供原子操作的Integer类,通过线程安全的方式操作加减。初始值为0
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement(){
int current;
int next;
do {
current = this.atomicInteger.get();//获得当前的值
next = current >= 2147483647 ? 0 : current + 1;//2147483647int的最大值 如果current>=1194525857,就让next为0,否则就让next=current + 1
//利用CSA和自旋操作取到我们想要的值
}while(!this.atomicInteger.compareAndSet(current,next));
System.out.println("==============next="+next);
return next;//返回rest接口第几次请求数
}
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstanceList) {
//实际调用服务器位置下标 = rest接口第几次请求数 % 服务集群总数量
int index = getAndIncrement() % serviceInstanceList.size();
return serviceInstanceList.get(index);//通过下标返回具体的服务实例
}
}
CSA和自旋操作博主还不是很了解,有错误请指点。
5、编写controller
@GetMapping(value = "/consumer/payment/lb")
public String getPaymentLb(){
//通过服务名获得该服务的所有服务实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if (instances == null || instances.size() <= 0){
return null;
}
//调用loadBalancer接口中的instances方法,参数为服务实例列表instances,返回的是一个具体的实例serviceInstance
ServiceInstance serviceInstance = loadBalancer.instances(instances);
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri+"/payment/lb",String.class);
}
访问请求:http://localhost/consumer/payment/lb
第一次访问:
第二次访问:
自定义实现轮询算法成功!