springcloud系列之-ribbon使用及原理讲解

2020-11-19 16:05:02 浏览数 (1)

摘要

本章节将要学习springcloud的组件ribbon的使用,ribbon是一个实现了客户端负载均衡的工具,透明的实现了负载均衡策略,我们只需要在resttemplate加上loadbalenced注解就可以开启负载均衡,非常方便,学完这节后,你将会知道如何在你的项目上去使用这个小工具

什么是负载均衡?

负载均衡是将用户的请求按照某种算法,动态的去选择某一个主机去处理,对于用户来说是透明的,在没有负载均衡之前我们可能是这么去请求一个数据的

有了负载均衡之后,用户请求过程可能是这样的

负载均衡分为客户端负载均衡和集中式负载均衡

客户端负载均衡:消费者在调用请求接口之前首先获取哪些资源是可用的,然后按照均衡算法选择一个资源进行请求,这个是客户端自己进行选择的,所以称为客户端负载均衡,常见的客户端负载均衡有ribbon

集中式负载均衡:在消费者和服务方中间使用独立的代理方式进行负载,有硬件的(F5),软件的(nginx)

为什么使用负载均衡?

这个问题其实很好回答,一个新的东西出来必定是为了解决某一个问题,不然他出现的就毫无意义,负载均衡也是一样,负载均衡是为了达到最优化资源使用、最大化吞吐率、最小化响应时间、同时避免过载的目的,打个比方说,就相当于我们在火车站买票,如果只开了一个窗口,那么这个窗口排队的人肯定会很长,买票的效率会很低,如果我同时开通十个窗口,那么买票速度铁定会提升的,而且单个窗口的压力也会减少,响应素度也会更快的,这也就是为什么采用负载均衡的原因。

什么是ribbon?

ribbon其实是netflix开源的一款用于客户端负载均衡的一个框架。

在springcloud提供了ribbon用来做客户端负载均衡,通过springcloud对ribbon的封装,我们可以很轻松的通过负载均衡去调用我们开发的rest服务,不需要手动去处理因负载均衡而出现的各种棘手情况,ribbon并不需要向eureka和网关那样单独部署,他是和每一个微服务耦合在一起的,这一节我们介绍的是resttemplate与ribbon整合去实现负载均衡的调用,这里负载均衡的启用方式是需要我们手动在resttemplate实例配置上面添加loadbalence注解的,关于resttemplate我上一节已经介绍过了,在这个后一节我会介绍feign工具的使用,使用它的时候,它自动帮我们开启了负载均衡。

ribbon与resttemplate整合

关于resttemplate的整合,上一节中,我已经介绍过了,这一节主要介绍resttemplate的api使用

1

GET请求

代码语言:javascript复制
(1) public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables)
    url:请求的接口地址,例“http://192.168.31.168:8081/getOrder?name={1}”
    responseType:响应数据类型,例“String.class”,返回string类型数据,如果希望返回某一个实体对象也可以这样,User.class
    uriVariables:填充地址栏里面的占位符
    
(2) public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)
    url:url:请求的接口地址,例“http://192.168.31.168:8081/getOrder”
    responseType:响应数据类型,例“String.class”,返回string类型数据,如果希望返回某一个实体对象也可以这样,User.class
    uriVariables:参数的键值对
    
(3) public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)  
    url:请求的接口地址,例“http://192.168.31.168:8081/getOrder?name={1}”
    responseType:响应数据类型,例“String.class”,返回string类型数据,与上面不同的是,这个返回结果封装了http的响应头等信息,,如果希望返回某一个实体对象也可以这样,User.class
    uriVariables:填充地址栏里面的占位符
    
(4) public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables)
    url:url:请求的接口地址,例“http://192.168.31.168:8081/getOrder”
    responseType:响应数据类型,例“String.class”,返回string类型数据,与上面不同的是,这个返回结果封装了http的响应代码等数据,如果希望返回某一个实体对象也可以这样,User.class,
    uriVariables:参数的键值对

2

POST请求

代码语言:javascript复制
(1) public <T> T postForObject(String url, Class<T> responseType, Object... uriVariables)
    url:请求的接口地址,例“http://192.168.31.168:8081/getOrder?name={1}”
    responseType:响应数据类型,例“String.class”,返回string类型数据,如果希望返回某一个实体对象也可以这样,User.class
    uriVariables:填充地址栏里面的占位符
    
(2) public <T> T postForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)
    url:url:请求的接口地址,例“http://192.168.31.168:8081/getOrder”
    responseType:响应数据类型,例“String.class”,返回string类型数据,如果希望返回某一个实体对象也可以这样,User.class
    uriVariables:参数的键值对
    
(3) public <T> ResponseEntity<T> postForEntity(String url, Class<T> responseType, Object... uriVariables)  
    url:请求的接口地址,例“http://192.168.31.168:8081/getOrder?name={1}”
    responseType:响应数据类型,例“String.class”,返回string类型数据,与上面不同的是,这个返回结果封装了http的响应头等信息,,如果希望返回某一个实体对象也可以这样,User.class
    uriVariables:填充地址栏里面的占位符
    
(4) public <T> ResponseEntity<T> postForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables)
    url:url:请求的接口地址,例“http://192.168.31.168:8081/getOrder”
    responseType:响应数据类型,例“String.class”,返回string类型数据,与上面不同的是,这个返回结果封装了http的响应代码等数据,如果希望返回某一个实体对象也可以这样,User.class,
    uriVariables:参数的键值对

ribbon负载均衡源码跟踪

设想:ribbon是如何实现负载均衡的呢?根据我们前面说ribbon实现的的客户端负载均衡,所以他自己肯定有一个可用的服务列表,服务列表里面存储的是可用服务的地址,这是第一个条件;第二个是我们在resttemplate上面添加了注解后,他就自动实现了负载均衡,这个我们肯定会想到他拦截了我们的请求,所以这里肯定会有一个拦截器在帮助我们实现此功能,第三个是为什么我们加上了注解后,也可以使用服务名直接去调用了呢,肯定也有组件帮我们实现了替换的,让我们带着者三个疑问一起去追踪源码吧。

再看源码之前,我们都会从一个入口出发,然后进行调试。研究springmvc源码时,我们都知道肯定都是从dispatcherservlet出发,然后往下走,这里也是一样,通过我们上面的分析,发现实现了负载均衡时和这个注解有关的,所以我们先看看这个注解是如何定义的

代码语言:javascript复制
/**
 * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
 * @author Spencer Gibb
 */
 @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 @Inherited
 @Qualifierpublic 
 @interface LoadBalanced {
}

进入了LoadBalanced 注解后,我们看到他是这么定义的,并且注意到了上面的一行注释,通过注解标记resttemplate使用LoadBalancerClient去代理使用,这个我们先不关注,我们都知道,在springboot中,有一个申明式的注解,必定在其同名的包下面会有一个这样的(xxxAutoConfiguration)配置类,去配置这个注解,我们定位到该包下面,看到

同名的包下面确实有这样一个配置类LoadBalancerAutoConfiguration

我们看下这个类的注解信息

代码语言:javascript复制
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)

这里会涉及到spring的以注解使用,我挑几个常用的简单说下

@Configuration

该注解可以用java代码的形式实现spring中xml配置文件配置的效果。

@ConditionalOnClass

其用途是判断当前classpath下是否存在指定类,若是则将当前的配置装载入spring容器

@ConditionalOnBean

当给定的在bean存在时,则实例化当前Bean

@EnableConfigurationProperties

如果该类只使用了@ConfigurationProperties注解,然后该类没有在扫描路径下或者没有使用@Component等注解,导致无法被扫描为bean,那么就必须在配置类上使用@EnableConfigurationProperties注解去指定这个类,这个时候就会让该类上的@ConfigurationProperties生效,然后作为bean添加进spring容器中

@ConditionalOnMissingBean

容器中不存在指定bean

介绍完几个基本注解之后,我们继续分析,看到

代码语言:javascript复制
@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();

这里面会注入只有被loadbalence注解的所有的resttemplate实例,我们再点击resttemplate进去,看到此类继承了Intecepting,看来真的有拦截器在起作用,我们可以看到InterceptingHttpAccessor类中有一个setInterceptors方法,这里标记下

然后我们看看他是在哪里调用这份方法的,我们再回到LoadBalancerAutoConfiguration配置类中

代码语言:javascript复制
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
    final LoadBalancerInterceptor loadBalancerInterceptor) {
  return restTemplate -> {
              List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                      restTemplate.getInterceptors());
              list.add(loadBalancerInterceptor);
              restTemplate.setInterceptors(list);
          };
}

看到这里再往resttemplate中添加拦截器LoadBalancerInterceptor,我们点击这个拦截器进去,主要看这段代码,我们在这打个断点,

代码语言:javascript复制
  @Override
  public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
      final ClientHttpRequestExecution execution) throws IOException {
    final URI originalUri = request.getURI();
    String serviceName = originalUri.getHost();
    Assert.state(serviceName != null, "Request URI does not contain a valid hostname: "   originalUri);
    return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
  }

这里就是做拦截作用了,实现真正的逻辑实在execute方法中,我们点进去看看

代码语言:javascript复制
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
        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);
        }
    }

第一行通过serviceID找到对应的服务实例均衡器,serviceId其实就是每个微服务的应用名称("

")

loadBalancer 这里面保存了所有的服务实例,可用的,宕机的都在里面

然后通过调用getServer方法,使用均衡算法拿到可用的服务实例(从几个中选择一个,达到均衡的作用),返回server,然后拿到这个server就可以继续执行目标请求,我们在这里也打一个断点,到目前位置,我们的第一步已经完成,我们现在要做的就是模拟一个请求,跟踪请求来调试源码,

源码调试

我们还是使用上一节的项目来调试。如果需要获取项目,请关注我的公众号“乐哉码农”,回复“eureka”获取资料

启动项目,在启动过程中,我们来验证下,ribbon是否拿到所有被loadbalence注解的resttemplate

我们可以看到resttemplates里面有两个resttemplate,我们在restteplate的定义中是否有两个被loadbalence注解

确实有两个,最上面一个没有被loadbalenced注解,这里写两个是为了让我们理解的更加清晰点,没有实际作用,

我们放掉断点,看到他到了这里

在这里,他为我们的template添加了拦截器,我们跳过,让项目启动完成

访问接口http://localhost:8082/getOrderForLoadBalence,进入断点

往下走,看到进入了拦截器(LoadBalancerInterceptor)里面的断点

在这里面获取到了我们的servicename,然后通过servicename去均衡器里面获取服务地址,继续往下走,

我们根据serviceid获取到了一个均衡器,我们看看这里面存的什么

可以看到upserverlist里面保存了两个地址,这个地址我们应该很熟悉,就是我们启动的两个服务实例,所以说明getLoadBalancer方法根据serviceid拿到了可用的服务实例,将我们的服务名转换成了具体的实例地址,因为我们最终肯定只调用一个服务地址,所以我们继续往下面看,

往下走一步发现得到了一个server,这个server绑定的地址是8085端口,所以可以看出这里使用了均衡算法,返回一个服务实例给我们,后面肯定是拿着这个地址去替换我们现有的请求地址去发送请求,后的的不是我们这边关注的,所以整个源码分析也就到这里结束了。

ribbon的常见配置

1

禁用eureka

当我们在resttemplate上面添加loadbalence注解后,就可以使用服务名去调用,如果我们向关闭这个功能,可以使用ribbon.eureka.enable=false

2

配置接口地址列表

如果我们关闭了eureka之后,我们还想用服务名去调用,就需要手动配置服务配置列表

服务名.ribbon.listOfServers=IP:PORT1,IP:PORT2

3

配置负载均衡策略

服务名.ribbon.NFLoadBalencerRuleClassName=策略class全类名

4

超时时间

ribbon中有两种和超时时间相关的配置

ribbon.ConnectTimeout=2000 请求连接的超时时间

ribbon.ReadTimeout=5000 请求处理的超时时间

可以在前面加上具体的服务名,为指定的服务配置

5

并发参数

ribbon.MaxTotalConnections=500 最大连接数

ribbon,MaxConnectionsPerHost=500 每个host最大连接数

6

重试机制1

在集群环境中,用多个节点来提供相同的服务,当一个节点出现故障时,nginx会继续请求另外一个节点,而在eureka中,牺牲了数据一致性,保证了AP原则,有可能在某一个节点出现故障时,他短时间内还不会将这个节点去移除,他会在一段时间内等待这个节点重启,所以在这个时间段内,eurekaui一直将这个注册表保存在可用的服务列表中,导致我们去请求时,请求失败,这是我们客户端发现请求失败后就必须进行重试操作,在ribbon可以通过服务名.ribbon.NFLoadBalencerRuleClassName=xxx来指定重试机制

7

重试机制2

除了使用ribbon自带的重试机制,还可以集成spring-retry,

ribbon.maxAutoRetries=1 当前实例重试次数

ribbon.maxAutoRetriesNextServer=3 切换节点的重试次数

ribbon.okToRetryOnAllOperations=true,对所有请求进行重试

ribbon.retryableStatusCodes=500,402 对指定错误代码进行重试

ribbon面试必问

1.ribbon是什么?

答:Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix的中间层服务连接在一起。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。

2.ribbon是什么类型的负载均衡?

答:ribbon实现的是客户端负载均衡

3.常见的实现了负载均衡有哪些?

答:nginx(集中式),ribbon(客户端),F5(集中式)

4.负载均衡的算法都有哪些?

答:IPhash,轮询,权重

总结

通过本章的学习,对ribbon和负载均衡有了一个整体的学习,通过源码跟踪,了解了其负载均衡实现的原理,最后通过讲解一些ribbon的配置,对ribbon的常见自定义配置有了大概的理解,后面一章将会介绍feign的使用,使用feign更加快速高效的调用远程服务。

0 人点赞