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
posted @ 2020-08-16 21:19  Soulballad  阅读(256)  评论(0编辑  收藏  举报