zoukankan      html  css  js  c++  java
  • SpringCloud组件Ribbon入门解析

    一、Ribbon简介

    分布式系统中,当服务提供者集群部署时,服务消费者就需要从多个服务提供者当中选择一个进行服务调用,那么此时就会涉及负载均衡,将请求分发到不同的服务实例上。

    常用的负载均衡有两种实现方式,一种是独立部署负载均衡程序,比如nginx;一种是将负载均衡的逻辑嵌入到服务消费者端的程序中。

    前者代码代码侵入性低但是需要独立部署负载均衡组件;后者有一定的代码侵入性但是不需要独立部署负载均衡组件,可以降低服务器成本。

    Netfilx的开源Ribbon就是以第二种方式实现的负载均衡组件,将负载均衡的逻辑封装在客户端。

    Ribbon默认提供了七种负载均衡策略

    1、BestAvailableRule:选择最小请求数

    2、ClientConfigEnabledRoundRobinRule:轮询

    3、RondomRule:随机选择server

    4、RoundRobinRule:轮询选择server

    5、RetryRule:根据轮询的方式重试

    6、WeightedResponseTimeRule:根据相应时间分配一个权重weight,权重越低被选择的概率越小

    7、ZoneAvoidanceRule:根据server的zone区域和可用性轮询选择

    二、Ribbon实现原理

    2.1、@LoadBalanced注解

    Ribbon的使用比较简单,首先需要添加Ribbon相关依赖

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-ribbon</artifactId>
            </dependency>

    然后只需要在注入HTTP客户端如RestTemplate实例时添加@LoadBalanced注解即可,那么该RestTemplate进行HTTP请求调用服务时就会根据本地缓存的服务提供者列表信息进行负载均衡调用。

        @Autowired
        @LoadBalanced
        private RestTemplate restTemplate;

    所以研究Ribbon的原理主要是从@LoadBalanced注解入手,@LoadBalanced注解定义如下:

    @Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @Qualifier
    public @interface LoadBalanced {
    }

    @LoadBalanced注解本身没有任何逻辑,所以可以猜测@LoadBalanced注解只是一个标记的作用,核心逻辑在LoadBalancerAutoConfiguration中,

        @LoadBalanced
        @Autowired(required = false)
        private List<RestTemplate> restTemplates = Collections.emptyList();
    
        @Bean
        public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
                final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
            return () -> restTemplateCustomizers.ifAvailable(customizers -> {
                for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                    for (RestTemplateCustomizer customizer : customizers) {
                        customizer.customize(restTemplate);
                    }
                }
            });
        }

     首先注入了所有被@LoadBalanced注解修饰的RestTemplate实例,然后再遍历调用所有RestTemplateCustomizer的customize进行定制化处理,实现类位于LoadBalancerAutoConfiguration的内部类LoadBalancerInterceptorConfig中,如下示:

        @Configuration
        @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
        static class LoadBalancerInterceptorConfig {
            @Bean
            public LoadBalancerInterceptor ribbonInterceptor( LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
                /** 初始化负载均衡拦截器*/
                return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
            }
    
            @Bean
            @ConditionalOnMissingBean
            public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
                return restTemplate -> {
                    List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                            restTemplate.getInterceptors());
                    list.add(loadBalancerInterceptor);
                    /** 给RestTemplate设置拦截器*/
                    restTemplate.setInterceptors(list);
                };
            }
        }

     实际就是给RestTemplate对象添加了一个拦截器LoadBalancerInterceptor, 那么就可以得出结论, RestTemplate的方法调用会通过LoadBalancerInterecptor的intercept方法进行拦截处理,所以负载均衡的逻辑就全部在LoadBalancerInterceptor中。

     2.2、LoadBalancerInterceptor

    LoadBalancerInterceptor内部持有负载均衡客户端LoadBalancerClient的实例,实际的负载均衡逻辑也都委托给了LoadBalancerClient实例来处理,源码如下:

    /** 负载均衡客户端 */
        private LoadBalancerClient loadBalancer;
    
        /** LoadBalancerInterceptor 拦截方法 */
        @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);
            /** 调用负载均衡客户端的execute方法进行拦截处理 */
            return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
        }

    LoadBalancerClient的实现类是RibbonLoadBalancerClient,execute方法实现逻辑如下:

    @Override
        public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
            /** 1.根据服务ID获取 ILoadBalancer 对象 */
            ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
            /** 2.根据 ILoadBalancer对象选取适合的服务实例 */
            Server server = getServer(loadBalancer);
            if (server == null) {
                throw new IllegalStateException("No instances available for " + serviceId);
            }
            /** 3.构建RibbonServer对象 */
            RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
                    serviceId), serverIntrospector(serviceId).getMetadata(server));
            /** 4.向目标服务器发送请求 */
            return execute(serviceId, ribbonServer, request);
        }

     这里核心两步分别是构造ILoadBalancer对象和根据ILoadBalancer对象选取目标服务器,而选取目标服务器的getServer方法实际就是调用了ILoadBalancer对象的chooseServer方法,所以核心就在于ILoadBalancer对象

    2.3、ILoadBalancer

    ILoadBalancer接口定义了一系列负载均衡的方法,源码如下:

     1 public interface ILoadBalancer {
     2 
     3         /**
     4          * 添加服务器列表
     5          */
     6         public void addServers(List<Server> newServers);
     7 
     8         /**
     9          * 根据key选择服务器
    10          */
    11         public Server chooseServer(Object key);
    12 
    13         /**
    14          * 标记服务器下线
    15          */
    16         public void markServerDown(Server server);
    17 
    18         /**
    19          * 获取可用服务器列表
    20          */
    21         public List<Server> getReachableServers();
    22 
    23         /**
    24          * 获取所有服务器列表
    25          */
    26         public List<Server> getAllServers();
    27     }

    ILoadBalancer的实现类为DynamicServerListLoadBalancer,DynamicServerListLoadBalancer构造函数如下:

    public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
                                             ServerList<T> serverList, ServerListFilter<T> filter,
                                             ServerListUpdater serverListUpdater) {
            super(clientConfig, rule, ping);
            this.serverListImpl = serverList;
            this.filter = filter;
            this.serverListUpdater = serverListUpdater;
            if (filter instanceof AbstractServerListFilter) {
                ((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
            }
            restOfInit(clientConfig);
        }

    调用父类BaseLoadBalancer的构造函数初始化,然后和调用了restOfInit方法进行初始化,在restOfInit方法中会执行updateListOfServer()方法,该方法的逻辑是调用ServerList接口的实现类的getUpdateListOfServers()方法,

    最终会调用Eureka客户端的获取服务注册列表的功能。另外ILoadBalancer并非只从Eureka服务器拉取一次服务列表,而是创建了一个PingTask定时任务,每隔10秒遍历向所有服务实例发送一次ping心跳,如果返回的结果和预期的不一样,

    就会重新从Eureka服务器拉取最新的服务注册列表。所以DynamicServerListLoadBalancer内部是缓存了所有服务器实例列表,并且通过向服务器实例发送心跳的方式检测服务实例是否存活。

    当ILoadBalancer实例有了服务列表,接下来就需要选择服务器了,实现方法在BaseLoadBalancer的chooseServer方法,源码如下:

     /**
         * BaseLoadBalanacer的 选取服务器方法
         * */
        public Server chooseServer(Object key) {
            /** 创建计数器*/
            if (counter == null) {
                counter = createCounter();
            }
            /** 计数器计数*/
            counter.increment();
            if (rule == null) {
                return null;
            } else {
                try {
                    /** 调用IRule的choose方法 */
                    return rule.choose(key);
                } catch (Exception e) {
                    logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                    return null;
                }
            }
        }

    执行的是IRule的实例的choose方法进行处理,IRule接口定义如下:

    public interface IRule {
            /**
             * 选择可用服务实例
             */
            public Server choose(Object key);
    
            /**
             * 设置 ILoadBalancer对象
             */
            public void setLoadBalancer(ILoadBalancer lb);
    
            /**
             * 获取 ILoadBalancer对象
             */
            public ILoadBalancer getLoadBalancer();
        }

    IRule的实现类比较多,根据不同负载均衡策略有不同的实现类,根据不同的负载均衡策略有对应的实现类,共有BestAvailableRule、ClientConfigEnabledRoundRobinRule、RandomRule、RoundRobinRule、RetryRule、WeightedResponseTimeRule、

    ZoneAvoidanceRule等七种策略。不同的策略都是需要根据ILoadBalancer获取全部服务实例列表和当前在线服务实例列表,然后根据对应的算法选取合适的服务器实例。

    如BestAvailableRule策略就是根据最小请求数选择服务器,实现逻辑就是本地缓存每一个服务器实例的选择次数,然后选择次数最小的一台服务器即可;而RoundRobinRule实现逻辑就是记录每次访问的服务器索引,依次递增从服务器列表中选择下一个服务实例。

    2.4、总结

    Ribbon的使用通过@LoadBalance注解来配置, 被@LoadBalance注解修饰的RestTemplate在初始化时LoadBalancerAutoConfiguration会给RestTemplat添加拦截器LoadBalanceInterceptor;

    RestTemplate调用HTTP接口时就会通过拦截器进行拦截并进行负载均衡处理。拦截器将请求交给ILoadBalancer处理,ILoadBalancer初始化时会从Eureka服务器拉取注册的服务列表,并且创建PingTask定时任务每10秒进行一次服务器ping判断是否可用;

    ILoadBalancer处理请求时由负载均衡规则IRule对象的choose方法进行服务器选择,根据不同的策略选择合适的服务器信息。

    三、负载均衡组件对比

    负载均衡高可用框架的必不可少的组件之一,可以提升系统的整体高可用性,通过负载均衡将流量分发到多个服务器,同时多服务器能够消除这部分服务器的单点故障。

    负载均衡通常有两种实现模式,一种是独立部署负载均衡服务器,客户端所有请求都经过负载均衡服务器,负载均衡服务器根据路由规则将请求在分发到目标服务器;还有一种是将负载均衡逻辑集成在客户端,客户端本地维护可用服务器信息列表,在请求时

    根据负载均衡策略选择目标服务器。

    两种负载均衡实现方式各有优缺点

    独立部署:优点是客户端无需关心负载均衡逻辑,不需要维护服务器信息列表,服务器信息由负载均衡服务器集中式管理;缺点是负载均衡服务器需要独立部署,且同样需要保证高可用性;

    客户端集成:优点是无需独立部署,部署简单;缺点是客户端本地需要维护服务器信息,且需要定时刷新和发送心跳确保服务器可用;

    Ribbon的负载均衡实现就是客户端集成的负载均衡,而集中式负载均衡的实现最热门的就是nginx,包括热门的SLB负载均衡等;

  • 相关阅读:
    java.lang.NoSuchMethodError
    asm相关内容想下载(包括 jar 包)
    Initialization of bean failed; nested exception is java.lang.NoClassDefFoundError: org/objectweb/asm/Type
    用Navicat连接mysql报错:2003-Can't connect to MySql server on '10.100.0.109'(10039)
    The type java.lang.reflect.AnnotatedElement cannot be resolved. It is indirectly referenced from required .class files
    The type java.lang.CharSequence cannot be resolved. It is indirectly referenced from required .class files
    交通测速方式
    卡口和电子警察的区别
    Myeclipse连接Mysql数据库时报错:Error while performing database login with the pro driver:unable
    在window上安装mysql
  • 原文地址:https://www.cnblogs.com/jackion5/p/15783080.html
Copyright © 2011-2022 走看看