• 1.SpringCloud学习(一)——Spring Cloud Ribbon 实现负载均衡


    1.简介

    1.1 概述

    Ribbon is a client-side load balancer that gives you a lot of control over the behavior of HTTP and TCP clients. Feign already uses Ribbon, so, if you use @FeignClient, this section also applies.

    A central concept in Ribbon is that of the named client. Each load balancer is part of an ensemble of components that work together to contact a remote server on demand, and the ensemble has a name that you give it as an application developer (for example, by using the @FeignClient annotation). On demand, Spring Cloud creates a new ensemble as an ApplicationContext for each named client by using RibbonClientConfiguration. This contains (amongst other things) an ILoadBalancer, a RestClient, and a ServerListFilter.

    Ribbon是客户端负载平衡器,可让您对HTTP和TCP客户端的行为进行大量控制。 Feign已经使用了Ribbon,因此,如果使用 @FeignClient,则本节也适用。

    Ribbon中的中心概念是指定客户端的概念。每个负载平衡器都是组件的一部分,这些组件可以一起工作以按需联系远程服务器,并且该组件具有一个名称,您可以将其命名为应用程序开发人员(例如,使用 @FeignClient 注解)。根据需要,Spring Cloud通过使用 RibbonClientConfiguration 为每个命名客户端创建一个新的集合作为 ApplicationContext。它包含(除其他事项外)一个 ILoadBalancer,一个 RestClient 和一个ServerListFilter

    1.2 自定义 Ribbon

    You can configure some bits of a Ribbon client by using external properties in <client>.ribbon.*, which is similar to using the Netflix APIs natively, except that you can use Spring Boot configuration files. The native options can be inspected as static fields in CommonClientConfigKey (part of ribbon-core).

    您可以使用 <client>.ribbon.*中的外部属性来配置Ribbon客户端的某些地方,这与本机使用Netflix API相似,不同之处在于可以使用Spring Boot配置文件。可以在 CommonClientConfigKey(功能区核心的一部分)中将本地选项检查为静态字段。

    The following list shows the supported properties>:

    • <clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer
    • <clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule
    • <clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing
    • <clientName>.ribbon.NIWSServerListClassName: Should implement ServerList
    • <clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerListFilter

    2.演示环境

    1. JDK 1.8.0_201
    2. Spring Boot 2.2.0.RELEASE、Spring Cloud Hoxton.RELEASE
    3. 构建工具(apache maven 3.6.3)
    4. 开发工具(IntelliJ IDEA )

    3.演示代码

    image-20200810222057805

    • nfx-ribbon-client:ribbon 客户端,通过 ribbon 负载均衡调用服务;
    • nfx-ribbon-server:ribbon 服务端,负责提供服务供客户端调用。

    3.1 nfx-ribbon-server

    3.1.1 代码说明

    提供一个 restful 接口,供客户端调用

    3.1.2 maven 依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    

    3.1.3 配置文件

    spring.application.name=nfx-ribbon-server
    # 配置浮动ip
    server.port=${random.int[8081,8084]}
    

    3.1.4 java代码

    NfxRibbonServerApplication.java

    @SpringBootApplication
    public class NfxRibbonServerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(NfxRibbonServerApplication.class, args);
        }
    }
    

    RibbonServerController.java

    @RestController
    @RequestMapping(value = "/server")
    public class RibbonServerController {
    
        private static final Logger LOGGER = LoggerFactory.getLogger(RibbonServerController.class);
    
        @GetMapping(value = "/ribbon")
        public String ribbon() {
            LOGGER.info("ribbon server is calling!");
            return "Ribbon server has started!";
        }
    }
    

    3.2 nfx-ribbon-client

    3.2.1 代码说明

    通过 ribbon 的负载均衡机制,调用 server 提供的接口

    3.2.2 maven 依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>
    </dependencies>
    

    3.2.3 配置文件

    spring.application.name=nfx-netflix-ribbon
    server.port=8070
    ribbon.eureka.enable=false
    # 由于没有使用eureka,所以需要配置服务端地址,前面 nfx-ribbon-server 为服务端应用名
    nfx-ribbon-server.ribbon.listOfServers=localhost:8081,localhost:8082,localhost:8083
    nfx-ribbon-server.ribbon.ServerListRefreshInterval=1500
    

    3.2.4 java代码

    NfxRibbonClientApplication.java

    @SpringBootApplication
    public class NfxRibbonClientApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(NfxRibbonClientApplication.class, args);
        }
    }
    

    RibbonConfig.java

    @Configuration
    public class RibbonConfig {
    
        @Bean
        @LoadBalanced
        public RestTemplate restTemplate() {
            return new RestTemplate();
        }
    
        @Bean
        public IRule ribbonRule() {
            // 轮询策略
            return new RoundRobinRule();
        }
    }
    

    RibbonClientController.java

    @RestController
    @RequestMapping(value = "/client")
    public class RibbonClientController {
    
        @Autowired
        private RestTemplate restTemplate;
    
        @Autowired
        private LoadBalancerClient loadBalancer;
    
        /**
         * 直接使用 ribbon api
         */
        @GetMapping(value = "/ribbon")
        public String ribbon() {
            ServiceInstance instance = loadBalancer.choose("nfx-ribbon-server");
            String host = instance.getHost();
            int port = instance.getPort();
            System.err.println("ribbon request: " + host + ":" + port);
            return "ribbon request: " + host + ":" + port;
        }
    
        /**
         * 使用 restTemplate 调用ribbon
         */
        @GetMapping(value = "/rest")
        public String rest() {
            return restTemplate.getForObject("http://nfx-ribbon-server/server/ribbon", String.class);
        }
    }
    

    3.3 git 地址

    spring-cloud-nfx-04-ribbon: Spring Cloud 整合 Ribbon实现的分布式负载均衡方案

    4.效果展示

    启动三个 nfx-ribbon-server,端口分别为 8081/8082/8083;

    image-20200810223000569

    然后再启动 nfx-ribbon-client ,在 spring-cloud-netflix-ribbon.http 访问下列地址,观察输出信息是否符合预期。

    ### GET /client/ribbon (每次返回的地址都是不一样的)
    GET http://localhost:8070/client/ribbon
    

    可以看到,由于配置了 RoundRobinRule 的轮询策略,所以会依次输出:ribbon request: localhost:8081、ribbon request: localhost:8082、ribbon request: localhost:8083

    image-20200810223247585

    在 nfx-ribbon-client 的控制台,可以看到如下内容

    image-20200810223343819

    ### GET /client/rest
    GET http://localhost:8070/client/rest
    

    同理,使用 restTemplate 调用 nfx-ribbon-server 的接口时,会分别在三个控制台打印:ribbon server is calling!

    5.源码分析

    5.1 RestTemplate 调用如何负载均衡?

    通过上面的调用流程可以发现,在 createRequest 时,创建了 InterceptingClientHttpRequest

    @Override
    protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) {
       return new InterceptingClientHttpRequest(requestFactory, this.interceptors, uri, httpMethod);
    }
    

    所以在调用 request.execute 方法时,实际调用的是 InterceptingClientHttpRequest.execute,InterceptingClientHttpRequest 的类图如下

    image-20200811205239027

    所以会先调用 AbstractClientHttpRequest 和 AbstractBufferingClientHttpRequest,最终调用 InterceptingClientHttpRequest 时,构造了一个调用链 execution

    protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
       InterceptingRequestExecution requestExecution = new InterceptingRequestExecution();
       return requestExecution.execute(this, bufferedOutput);
    }
    

    InterceptingRequestExecution 的构造函数中,传入一个 iterator 迭代器

    public InterceptingRequestExecution() {
       this.iterator = interceptors.iterator();
    }
    

    所以执行 requestExecution.execute 时,会调用 interceptor 的拦截方法

    @Override
    public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
       if (this.iterator.hasNext()) {
          ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
          return nextInterceptor.intercept(request, body, this);
       }
       else {
          // ...
       }
    }
    

    这里的 interceptor 实际上是 LoadBalancerInterceptor, 所以最终通过 LoadBalancerInterceptor 实现了负载均衡,选择一个服务端进行调用。

    5.2 关键类是如何初始化的?

    通过上面的分析,发现有几个关键的类:

    • InterceptingClientHttpRequestFactory:创建了 InterceptingClientHttpRequest
    • InterceptingClientHttpRequest:调用了requestExecution.execute方法,最终执行了拦截器
    • LoadBalancerInterceptor:实现负载均衡的拦截器,通过构造函数传入 InterceptingRequestExecution 中
    • RibbonLoadBalancerClient: Ribbon 负载均衡客户端
    • ZoneAwareLoadBalancer:具体的负载均衡选择器

    InterceptingClientHttpRequestFactory

    RestTemplate 继承自 InterceptingHttpAccessor,所以在 createRequest 中调用 getRequestFactory 方法时,会调用到 InterceptingHttpAccessor.getRequestFactory

    public ClientHttpRequestFactory getRequestFactory() {
        // 获取 interceptors
        List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
        if (!CollectionUtils.isEmpty(interceptors)) {
            ClientHttpRequestFactory factory = this.interceptingRequestFactory;
            if (factory == null) {
                // 初始化一个 InterceptingClientHttpRequestFactory
                factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
                this.interceptingRequestFactory = factory;
            }
            return factory;
        }
        else {
            return super.getRequestFactory();
        }
    }
    

    InterceptingClientHttpRequest

    InterceptingClientHttpRequestFactory 继承自 AbstractClientHttpRequestFactoryWrapper,所以最终是在 InterceptingClientHttpRequestFactory.createRequest 方法中创建了 InterceptingClientHttpRequest 对象

    @Override
    protected ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod, ClientHttpRequestFactory requestFactory) {
        return new InterceptingClientHttpRequest(requestFactory, this.interceptors, uri, httpMethod);
    }
    

    LoadBalancerInterceptor

    负载均衡有一个自动装配类 LoadBalancerAutoConfiguration,在这个类中有一个 LoadBalancerInterceptorConfig 的内部类,它通过 ribbonInterceptor 方法声明了一个 LoadBalancerInterceptor 对象,然后又通过 restTemplateCustomizer 方法声明了一个 RestTemplateCustomizer对象。

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    static class LoadBalancerInterceptorConfig {
    
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(
            LoadBalancerClient loadBalancerClient,
            LoadBalancerRequestFactory requestFactory) {
            // 声明一个 LoadBalancerInterceptor 拦截器对象
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }
    
        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(
            final LoadBalancerInterceptor loadBalancerInterceptor) {
            // 这里声明了一个匿名类,通过lambda实现了customize方法
            return restTemplate -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                    restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
        }
    
    }
    

    RestTemplateCustomizer 是一个接口,它里面只有一个 customize 方法

    public interface RestTemplateCustomizer {
    
        void customize(RestTemplate restTemplate);
    }
    

    而在 loadBalancedRestTemplateInitializerDeprecated 方法中,这里遍历所有的 customizers,调用它的 customize 方法,所以最终将 loadBalancerInterceptor 放入到了 restTemplate 的 interceptors 字段中。

    @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);
                }
            }
        });
    }
    

    RibbonLoadBalancerClient

    在 RibbonAutoConfiguration#loadBalancerClient 中进行声明

    @Bean
    @ConditionalOnMissingBean(LoadBalancerClient.class)
    public LoadBalancerClient loadBalancerClient() {
        return new RibbonLoadBalancerClient(springClientFactory());
    }
    

    ZoneAwareLoadBalancer

    在 RibbonClientConfiguration#ribbonLoadBalancer 中进行声明

    @Bean
    @ConditionalOnMissingBean
    public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
                                            ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
                                            IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
        if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
            return this.propertiesFactory.get(ILoadBalancer.class, config, name);
        }
        return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
                                           serverListFilter, serverListUpdater);
    }
    

    6.参考

    1. 官方文档-Spring Cloud Netflix/Ribbon
  • 相关阅读:
    SDUT 猴子分桃
    SDUT 母牛的故事
    SDUT 小鑫の日常系列故事(六)——奇遇记 递推算法
    SDUT 爬楼梯
    SDUT 王小二切饼
    linux 排序命令sort
    shell 判断语句
    shell统计指定范围内的所有质数以及它们的和
    shell 1到指定数累加
    shell九九乘法表
  • 原文地址:https://www.cnblogs.com/col-smile/p/13514356.html
Copyright © 2020-2023  润新知