9

2021升级版微服务教程6—Ribbon使用+原理+整合Nacos权重+实战优化 一篇搞定

 3 years ago
source link: https://my.oschina.net/u/3555122/blog/4899187
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

2021升级版SpringCloud教程从入门到实战精通「H版&alibaba&链路追踪&日志&事务&锁」

教程全目录「含视频」https://gitee.com/bingqilinpeishenme/Java-Wiki

Ribbon使用+原理+整合Nacos权重+实战优化 一篇搞定

Ribbon基本使用

Ribbon是一个客户端负载均衡工具,封装Netflix Ribbon组件,能够提供客户端负载均衡能力。

理解Ribbon最重要的就是理解客户端这个概念,所谓客户端负载均衡工具不同于Nginx(服务端负载均衡),Ribbon和应用程序绑定,本身不是独立的服务,也不存储服务列表,需要负载均衡的时候,会通过应用程序获取注册服务列表,然后通过列表进行负载均衡和调用。

  • Nginx独立进程做负载均衡,通过负载均衡策略,将请求转发到不同的服务上
  • 客户端负载均衡,通过在客户端保存服务列表信息,然后自己调用负载均衡策略,分摊调用不同的服务

Ribbon的负载均衡有两种方式

  1. 和 RestTemplate 结合 Ribbon+RestTemplate
  2. 和 OpenFeign 结合

Ribbon的核心子模块

  1. ribbon-loadbalancer:可以独立使用或者和其他模块一起使用的负载均衡API
  2. ribbon-core:Ribbon的核心API

订单服务集成Ribbon

订单服务调用商品服务

配置过程 分两步

  1. 在订单服务中导入ribbon的依赖

    <!--ribbon-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    </dependency>
    
  2. 配置 RestTemplate

    1594002129980

订单服务调用商品服务

  1. 订单服务调用商品服务的链接 不能写成ip+端口号,需要写成商品服务的服务名称

    1594002347393

  2. 重启 订单服务 测试负载均衡

    1594002358421

    1594002385509

Ribbon负载均衡简单版实现的流程

  1. RestTemplate发送的请求是服务名称http://nacos-product/product/getProductById/1
  2. 获取@LoadBalanced注解标记的RestTemplate
  3. RestTemplate添加一个拦截器,当使用RestTemplate发起http调用时进行拦截
  4. 根据url中的服务名称 以及自身的负载均衡策略 去订单服务的服务列表中找到一个要调用的ip+端口号 localhost:8802
  5. 访问该目标服务,并获取返回结果

1594002530793

服务列表实际上是个map

image-20210106112037719

Ribbon负载均衡原理 [了解]

获取@LoadBalanced注解标记的RestTemplate。

Ribbon将所有标记@LoadBalanced注解的RestTemplate保存到一个List集合当中,具体源码如下:

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();

具体源码位置是在LoadBalancerAutoConfiguration中。

RestTemplate添加一个拦截器

拦截器不是Ribbon的功能

RestTemplate添加拦截器需要有两个步骤,首先是定义一个拦截器,其次是将定义的拦截器添加到RestTemplate中。

定义一个拦截器

实现ClientHttpRequestInterceptor接口就具备了拦截请求的功能,该接口源码如下:

public interface ClientHttpRequestInterceptor {
    /**
     *实现该方法,在该方法内完成拦截请求后的逻辑内容。
     *对于ribbon而言,在该方法内完成了根据具体规则从
     *服务集群中选取一个服务,并向该服务发起请求的操作。
     */
   ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException;

}

ribbon中对应的实现类是LoadBalancerInterceptor具体源码如下:

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 {
      final URI originalUri = request.getURI();
      String serviceName = originalUri.getHost();
      /**
       *拦截请求,并调用loadBalancer.execute()方法
       *在该方法内部完成server的选取。向选取的server
       *发起请求,并获得返回结果。
       */
      return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
   }
}

将拦截器添加到RestTemplate中

RestTemplate继承了InterceptingHttpAccessor,在InterceptingHttpAccessor中提供了获取以及添加拦截器的方法,具体源码如下:

public abstract class InterceptingHttpAccessor extends HttpAccessor {

    /**
     * 所有的拦截器是以一个List集合形式进行保存。
     */
   private List<ClientHttpRequestInterceptor> interceptors = new ArrayList<ClientHttpRequestInterceptor>();

   /**
    * 设置拦截器。
    */
   public void setInterceptors(List<ClientHttpRequestInterceptor> interceptors) {
      this.interceptors = interceptors;
   }

   /**
    * 获取当前的拦截器。
    */
   public List<ClientHttpRequestInterceptor> getInterceptors() {
      return interceptors;
   }

   //省略部分代码...
}

通过这两个方法我们就可以将刚才定义的LoadBalancerInterceptor添加到有@LoadBalanced注解标识的RestTemplate中。具体的源码如下(LoadBalancerAutoConfiguration)省略部分代码:

public class LoadBalancerAutoConfiguration {

    /**
      * 获取所有带有@LoadBalanced注解的restTemplate
     */
   @LoadBalanced
   @Autowired(required = false)
   private List<RestTemplate> restTemplates = Collections.emptyList();

    /**
     * 创建SmartInitializingSingleton接口的实现类。Spring会在所有
     * 单例Bean初始化完成后回调该实现类的afterSingletonsInstantiated()
     * 方法。在这个方法中会为所有被@LoadBalanced注解标识的
     * RestTemplate添加ribbon的自定义拦截器LoadBalancerInterceptor。
     */
   @Bean
   public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
         final List<RestTemplateCustomizer> customizers) {
      return new SmartInitializingSingleton() {
         @Override
         public void afterSingletonsInstantiated() {
            for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
               for (RestTemplateCustomizer customizer : customizers) {
                  customizer.customize(restTemplate);
               }
            }
         }
      };
   }
    /**
     * 创建Ribbon自定义拦截器LoadBalancerInterceptor
     * 创建前提是当前classpath下不存在spring-retry。
     * 所以LoadBalancerInterceptor是默认的Ribbon拦截
     * 请求的拦截器。
     */
    @Configuration
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
   static class LoadBalancerInterceptorConfig {
      @Bean
      public LoadBalancerInterceptor ribbonInterceptor(
            LoadBalancerClient loadBalancerClient,
            LoadBalancerRequestFactory requestFactory) {
         return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
      }

      /**
       * 添加拦截器具体方法。首先获取当前拦截器集合(List)
       * 然后将loadBalancerInterceptor添加到当前集合中
       * 最后将新的集合放回到restTemplate中。
       */
      @Bean
      @ConditionalOnMissingBean
      public RestTemplateCustomizer restTemplateCustomizer(
            final LoadBalancerInterceptor loadBalancerInterceptor) {
         return new RestTemplateCustomizer() {
            @Override
            public void customize(RestTemplate restTemplate) {
               List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                     restTemplate.getInterceptors());
               list.add(loadBalancerInterceptor);
               restTemplate.setInterceptors(list);
            }
         };
      }
   }
}

至此知道了ribbon拦截请求的基本原理,接下来我们看看Ribbon是怎样选取server的。

Ribbon选取server原理概览

通过上面的介绍我们知道了当发起请求时ribbon会用LoadBalancerInterceptor这个拦截器进行拦截。在该拦截器中会调用LoadBalancerClient.execute()方法,该方法具体代码如下:

@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
  /**
   *创建loadBalancer的过程可以理解为组装选取服务的规则(IRule)、
   *服务集群的列表(ServerList)、检验服务是否存活(IPing)等特性
   *的过程(加载RibbonClientConfiguration这个配置类),需要注意
   *的是这个过程并不是在启动时进行的,而是当有请求到来时才会处理。
   */
   ILoadBalancer loadBalancer = getLoadBalancer(serviceId);

   /**
    * 根据ILoadBalancer来选取具体的一个Server。
    * 选取的过程是根据IRule、IPing、ServerList
    * 作为参照。
    */
   Server server = getServer(loadBalancer);
   if (server == null) {
      throw new IllegalStateException("No instances available for " + serviceId);
   }
   RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
         serviceId), serverIntrospector(serviceId).getMetadata(server));

   return execute(serviceId, ribbonServer, request);
}

通过代码我们可知,首先创建一个ILoadBalancer,这个ILoadBalancer是Ribbon的核心类。可以理解成它包含了选取服务的规则(IRule)、服务集群的列表(ServerList)、检验服务是否存活(IPing)等特性,同时它也具有了根据这些特性从服务集群中选取具体一个服务的能力。 Server server = getServer(loadBalancer);这行代码就是选取举一个具体server。 最终调用了内部的execute方法,该方法代码如下(只保留了核心代码):

@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
   try {
      //发起调用
      T returnVal = request.apply(serviceInstance);
      statsRecorder.recordStats(returnVal);
      return returnVal;
   }
   catch (IOException ex) {
      statsRecorder.recordStats(ex);
      throw ex;
   }
   catch (Exception ex) {
      statsRecorder.recordStats(ex);
      ReflectionUtils.rethrowRuntimeException(ex);
   }
   return null;
}

接下来看下request.apply(serviceInstance)方法的具体做了那些事情(LoadBalancerRequestFactory中):

@Override
public ClientHttpResponse apply(final ServiceInstance instance)
      throws Exception {
   HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, loadBalancer);
   //省略部分代码...
   /**
    * 发起真正请求。
    */
   return execution.execute(serviceRequest, body);
}

看到这里整体流程的原理就说完了,接下来我们结合一张图来回顾下整个过程:

img

首先获取所有标识@LoadBalanced注解的RestTemplate(可以理解成获取那些开启了Ribbon负载均衡功能的RestTemplate),然后将Ribbon默认的拦截器LoadBalancerInterceptor添加到RestTemplate中,这样当使用RestTemplate发起http请求时就会起到拦截的作用。当有请求发起时,ribbon默认的拦截器首先会创建ILoadBalancer(里面包含了选取服务的规则(IRule)、服务集群的列表(ServerList)、检验服务是否存活(IPing)等特性)。在代码层面的含义是加载RibbonClientConfiguration配置类)。然后使用ILoadBalancer从服务集群中选择一个服务,最后向这个服务发送请求。

Ribbon负载均衡规则

参考资料:https://www.jianshu.com/p/79b9cf0d0519

Ribbon默认负载均衡规则

根据上述Ribbon的原理,可以知道IRule接口负责负载均衡的实现,具体如下:

image-20210105193640996

规则名称 特点 AvailabilityFilteringRule 过滤掉一直连接失败的被标记为circuit tripped的后端Server,并 过滤掉那些高并发的后端Server或者使用一个AvailabilityPredicate 来包含过滤server的逻辑,其实就是检查status里记录的各个server 的运行状态 BestAvailableRule 选择一个最小的并发请求的server,逐个考察server, 如果Server被tripped了,则跳过 RandomRule 随机选择一个Server ResponseTimeWeightedRule 已废弃,作用同WeightedResponseTimeRule WeightedResponseTimeRule 权重根据响应时间加权,响应时间越长,权重越小,被选中的可能性越低 RetryRule 对选定的负载均衡策略加上重试机制,在一个配置时间段内当 选择Server不成功,则一直尝试使用subRule的方式选择一个 可用的Server RoundRobinRule 轮询选择,轮询index,选择index对应位置的Server ZoneAvoidanceRule 默认的负载均衡策略,即复合判断Server所在区域的性能和Server的可用性 选择Server,在没有区域的环境下,类似于轮询(RandomRule)

其中RandomRule表示随机策略、RoundRobinRule表示轮询策略、WeightedResponseTimeRule表示加权策略、BestAvailableRule表示请求数最少策略等等

随机源码:

image-20210105194052894

轮询源码:

image-20210105194240761

修改默认的自定义规则

默认是轮询 可以修改为任意的规则

修改为随机算法

  1. 创建具有负载均衡功能的RestTemplate实例

    [@Bean](https://my.oschina.net/bean)
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
    

    使用RestTemplate进行rest操作的时候,会自动使用负载均衡策略,它内部会在RestTemplate中加入LoadBalancerInterceptor这个拦截器,这个拦截器的作用就是使用负载均衡。

    默认情况下会采用轮询策略,如果希望采用其它策略,则指定IRule实现,如:

    [@Bean](https://my.oschina.net/bean)
    public IRule ribbonRule() {
        return new BestAvailableRule();
    }
    

    这种方式对OpenFeign也有效。

修改为按照Nacos配置的权重进行负载均衡

  1. 在nacos中对集群进行权重的配置

    image-20210106152111687

    image-20210106152129819

    image-20210106152146617

  2. 在项目中,选择使用 NacosRule

    image-20210106152259628

Ribbon实战优化

Ribbon默认懒加载,意味着只有在发起调用的时候才会创建客户端

ribbon:
  eager-load:
    # 开启ribbon饥饿加载
    enabled: true
    # 配置user-center使用ribbon饥饿加载,多个使用逗号分隔
    clients: user-center

主要调整请求的超时时间,是否重试

如果业务没有做幂等性的话建议把重试关掉:ribbon.MaxAutoRetriesNextServer=0

# 从注册中心刷新servelist的时间 默认30秒,单位ms
ribbon.ServerListRefreshInterval=15000
# 请求连接的超时时间 默认1秒,单位ms
ribbon.ConnectTimeout=30000
# 请求处理的超时时间 默认1秒,单位ms
ribbon.ReadTimeout=30000
# 对所有操作请求都进行重试,不配置这个MaxAutoRetries不起作用 默认false
#ribbon.OkToRetryOnAllOperations=true
# 对当前实例的重试次数 默认0
# ribbon.MaxAutoRetries=1
# 切换实例的重试次数 默认1
ribbon.MaxAutoRetriesNextServer=0

如果MaxAutoRetries=1MaxAutoRetriesNextServer=1请求在1s内响应,超过1秒先同一个服务器上重试1次,如果还是超时或失败,向其他服务上请求重试1次。

那么整个ribbon请求过程的超时时间为:ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1)

如果你觉得这篇内容对你挺有有帮助的话:

  1. 点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)

  2. 欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。

  3. 觉得不错的话,也可以关注 编程鹿 的个人公众号看更多文章和讲解视频(感谢大家的鼓励与支持🌹🌹🌹)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK