侧边栏壁纸
博主头像
林雷博主等级

斜月沉沉藏海雾,碣石潇湘无限路

  • 累计撰写 132 篇文章
  • 累计创建 47 个标签
  • 累计收到 3 条评论

目 录CONTENT

文章目录

5、客户端负载均衡:Spring Cloud Ribbon

林雷
2019-11-07 / 0 评论 / 0 点赞 / 283 阅读 / 30,869 字

一 Spring Cloud Ribbon

Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它是基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地面向服务的REST模板请求自动转换成客户端负载均衡的服务调用。

微服务间调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括Feign,也是基于Ribbon实现的工具。

1.1 客户端负载均衡

负载均衡在系统架构中是一个非常重要并且是不得不去实施的内容。因为负载均衡是对系统的高可用、网络压力的缓解和处理能力扩容的重要手段之一。我们通常说的负载均衡都指的是服务端负载均衡,其中分为硬件负载和软件负载。不论是采用硬件负载还是软件负载,只要是服务端负载都能以类似下图的架构方式构建:
image.png

硬件负载均衡的设备或是软件负载均衡都会维护一个下挂可用的服务端清单,通过心跳检测来剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。当客户端发送请求到负载均衡设备的时候,该设备按某种算法(比如线性轮训、按权重负载、按流量负载等)从维护的可用服务端清单中取出一台服务端的地址,然后进行转发。

客户端负载均衡和服务端负载均衡最大的不同点在于服务清单所存储的位置。在客户端负载均衡中,所有客户端节点都维护着自己要访问的服务端清单,而这些服务端的清单来自于服务注册中心。

在客户端负载均衡中也需要心跳去维护服务端清单的健康性,只是这个步骤需要与服务注册中心配置完成。

通过Spring Cloud Ribbon的封装,我们在微服务架构中使用客户端负载均衡调用非常简单,只需要如下两步:

  • 服务提供者只需要启动多个服务实例病注册到一个注册中心或是多个相关联的服务注册中心
  • 服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用

1.2 RestTemplate详解

1.2.1 GET请求

在RestTemplate中,对GET请求可以通过如下两个方式进行调用实现。

1.2.1.1 第一种: getForEntity函数

getForEntity函数。该方法返回的是ResponseEntity,该对象是Spring对HTTP请求响应的封装,其中主要存储了HTTP的几个重要元素,比如HTTP请求状态码的枚举对象HttpStatus(即200、404、500等),在它的父类HttpEntity中还存储着HTTP请求的头信息对象HttpHeaders以及泛型类型的请求体对象

比如下面的例子,就是访问SYSTEM-PROVIDER服务的/user请求,同时最后一个参数 1 会替换url中的 {1} 占位符,而返回ResponseEntity对象中的body内容类型会根据第二个参数转换为String类型

RestTemplate restTemplate = new RestTemplate();

ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://SYSTEM-PROVIDER/user?id={1}", String.class, "1");

String body = responseEntity.getBody();

而我们希望返回body是一个User对象类型,也可以这样实现

RestTemplate restTemplate = new RestTemplate();

ResponseEntity<User> responseEntity = restTemplate.getForEntity("http://SYSTEM-PROVIDER/user?id={1}", User.class, "1");

User body = responseEntity.getBody();

下面是比较常用的方法,getForEntity提供了三种重载实现

  • getForEntity(String ur, Class responseType, Object … urlVariables)
    该方法提供了三个参数,其中url为请求的地址,responseType为请求响应体body的包装类型,urlVariables为url中的参数绑定。GET请求的参数绑定通常使用url拼接的方式。比如:http://SYSTEM-PROVIDER/user?id=1, 但更好的方式是在url中使用占位符并配合urlVariables实现GET请求的参数绑定。比如url定义为:“http://SYSTEM-PROVIDER/user?name={1}”, 然后可以这样调用:getForEntity(“http://SYSTEM-PROVIDER/user?name={1}”, String.class, “zhangsan”),其中第三个参数"zhangsan"就替换url中的 {1} 占位符。注意:它的顺序会对应url中占位符定义的数字顺序

  • getForEntity(String url, Class responseType, Map urlVariables)
    该方法提供的参数使用Map类型,所以使用该方法进行参数绑定时需要在占位符中指定Map中参数的Key值。比如url定义为http://SYSTEM-PROVIDER/user?name={name}, 在Map类型的urlVariables中,我们就需要put一个key为name的参数来绑定url中 {name} 占位符的值,比如

RestTemplate restTemplate = new RestTemplate();

Map<String, Object> paramsMap = new HashMap<String, Object>();

paramsMap.put("name":"zhangsan");
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://SYSTEM-PROVIDER/user?name={name}", String.class, paramsMap);
  • getForEntity(URI url, class ResponseType)
    该方法使用URI对象来替代之前的url和urlVariables参数来指定访问地址和参数绑定。URI是JDK的java.net包下的一个类。如下例子
RestTemplate restTemplate = new RestTemplate();

UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://SYSTEM-PROVIDER/user?name={name}").build().expand("zhangsan").encode();

URI uri = uriComponents.toUri();

ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class).getBody();

1.2.1.2 第二种:getForObject函数

该方法可以理解为对getForEntity的进一步封装,它通过HttpMessageConverterExtractor对HTTP的请求响应体body内容进行对象转换,实现请求直接返回包装好的对象内容。比如

RespTemplate restTemplate = new RestTemplate();

String result = restTemplate.getForObject(uri, String.class);

当body是一个User对象,时,可以直接写成这样

RespTemplate restTemplate = new RestTemplate();

User result = restTemplate.getForObject(uri, User.class);

该方法也提供了三种重载

  • getForObject(String url, Class responseType, Object … urlVariables)
    与getForEntity的方法类似,url参数指定访问的地址,responseType参数定义该方法的返回类型,urlVariables参数为URL中占位符对应的参数
  • getForObject(String url, Class responseType, Map urlVariables)
    在该函数中,使用Map类型的urlVariables替换上面数组形式的urlVariables,因此使用时在url中需要将占位符的名称与Map类型中的key一一对应设置
  • getForObject(URI, Class responseType)
    该方法使用JDK的URI对象来替代之前的url和urlVariables参数使用。

1.2.2 POST请求

在RestTemplate中,对POST请求时可以通过如下三个方法进行调用

1.2.2.1 第一种:postForEntity函数

该方法同GET请求中的getFOrEntity类似,会在调用后返回ResponseEntity对象,其中T为请求响应的body类似。

如下,使用postForEntity提交POST请求到SYSTEM-PROVIDER服务的/user接口,提交body内容为user对象,请求响应返回的body类型为String。

RestTemplate restTemplate = new RestTemplate();

User user = new User("zhangsan", 30);

ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://SYSTEM-PROVIDER/user", user, String.class);

String body = responseEntity.getBody();

postForEntity函数有三种重载:

  • postForEntity(String url, Object request, Class responseType, Object … uriVariables)
  • postForEntity(String url, Object request, Class responseType, Map uriVariables)
  • postForEntity(URI url, Object request, Class responseType)

这些函数中的参数用法大部分与getForEntity一致。比如,第一个重载函数和第二个重载函数中的uriVariables参数都用来对url中的参数进行绑定使用;responseType参数是对请求响应body内容的类型定义。

这里需要注意的是增加了request参数,该参数可以是一个普通对象,也可以是一个HttpEntity对象。如果是一个普通对象,而非HttpEntity对象的时候,RestTemplate会将请求对象转换为一个HttpEntity对象来处理,其中Object就是request的类型,request内容会被视作完整的body来处理;而如果request是一个HttpEntity对象,那么就会被当做一个完整的HTTP请求对象来处理,这个request中不仅包含了body的内容,也包含了header的内容

1.2.2.2 第二种:postForObject函数

posrForObject函数,该方法跟getForObject的类型类似,它的作用是简化postForEntity的后续处理,通过直接将请求响应的body内容包装成对象来返回使用。如下面的例子

RestTemplate restTemplate = new RestTemplate();

User user = new User("zhangsan", 20);

String postRequest = restTemplate.postForObject("http://SYSTEM-PROVIDER/user", user, String.class);

该函数有三种重载:

  • postForObject(String url, Object request, Class responseType, Object … uriVariables)
  • postForObject(String url, Object request, Class responseType, Map uriVariables)
  • postForObject(URI url, Object request, Class responseType)

这个函数除了返回的对象类型不同,函数的传入参数均与postForEntity一致,可以参考上面的说明。

1.2.2.3 第三种:postForLocation函数

该方法实现了以POST请求提交资源,并返回新资源的URI,如下面的例子

RestTemplate restTemplate = new RestTemplate();

User user = new User("zhangsan", 20);

URI responseURI = restTemplate.postForLocation("http://SYSTEM-PROVIDER/user", user);

该函数有三种重载:

  • postForLocation(String url, Object request, Object … urlVariables)
  • postForLocation(String url, Object request, Map urlVariables)
  • postForLocation(URI url, Object request)

由于postForLocation函数会返回新资源URI,该URI就相当于指定了返回类型,所以此方法实现POST请求不需要像postForEntity和postForObject那样指定responseType。其他参数用法相同

1.2.3 PUT请求

在RestTemplate中,对PUT请求可以通过put方法进行调用实现,比如:

RestTemplate restTemplate = new RestTemplate();
Long id = 100L;
User user = new User("zhangsan", 40);
restTemplate.put("http://SYSTEM-PROVIDER/user/{1}", user, id);

put函数也实现了三种不同的重载方法,put函数是void类型,所以没有返回内容,其他与postForObject基本一致

  • put(String url, Object request, Object … urlVariables)
  • put(String url, Object request, Map urlVariables)
  • put(URI url, Object request)

1.2.4 DELETE请求

在RestTemplate中,对DELETE请求可以通过delete方法进行调用实现,比如:

RestTemplate restTemplate = new RestTemplate();
Long id = 100L;
User user = new User("zhangsan", 40);
restTemplate.delete("http://SYSTEM-PROVIDER/user/{1}", user, id);

delete函数也实现了三种不同的重载方法:

  • delete(String url, Object … urlVariables)
  • delete(String url, Map urlVariables)
  • delete(URI url)

通常将DELETE请求的唯一标识拼接在url中,所以DELETE请求也不需要request的body信息

1.3 Spring Cloud Ribbon使用

在介绍完RestTemplate之后,可以开发消费者服务了。消费者服务一般与客户端(如Web、手机等)直接进行交互。此时消费端需要调用提供端的数据,可以利用Ribbon结合RestTemplate完成集群形式的调用。本次测试架构图如下:
image.png

Comsumer-Ribbon通过RestTemplate + Ribbon的形式直接调用后端的System-Provider集群服务,并将结果返回给Client。使用Eureka作为服务注册中心。服务注册中心在此不做介绍。可参考如下项目
springcloudeurekatest.zip

1.3.1 服务方集群

创建服务方工程,目录结构如下
image.png

需要注入的包pom.xml如下

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.bianjf</groupId>
    <artifactId>cloud-system-provider</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <parent>
         <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
         <version>1.5.10.RELEASE</version>
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR5</spring-cloud.version>
    </properties>
    <dependencyManagement>
         <dependencies>
             <!-- Spring Cloud Start -->
             <dependency>
                 <groupId>org.springframework.cloud</groupId>
                 <artifactId>spring-cloud-dependencies</artifactId>
                 <version>${spring-cloud.version}</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
             <!-- Spring Cloud End -->
         </dependencies>
    </dependencyManagement>
    <dependencies>
         <!-- Spring Boot Start -->
         <!-- Spring Boot Web Start -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
         <!-- Spring Boot Web End -->
         <!-- Spring Boot Test Start -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
         </dependency>
         <!-- Spring Boot Test End -->
         <!-- Spring Boot Actuator Start -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-actuator</artifactId>
         </dependency>
         <!-- Spring Boot Actuator End -->
         <!-- Spring Boot End -->
         <!-- Spring Cloud Eureka Client Start -->
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-eureka</artifactId>
         </dependency>
         <!-- Spring Cloud Eureka Client End -->
    </dependencies>
    <build>
         <plugins>
             <!-- Spring Boot启动插件 Start -->
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
             </plugin>
             <!-- Spring Boot启动插件 End -->
             <!-- 编译工具 Start -->
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
                 <configuration>
                      <source>1.8</source>
                      <target>1.8</target>
                      <encoding>UTF-8</encoding>
                 </configuration>
             </plugin>
             <!-- 编译工具 End -->
             <!-- Maven Install 跳过Test阶段 Start -->
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
                 <configuration>
                      <skip>true</skip>
                 </configuration>
             </plugin>
             <!-- Maven Install 跳过Test阶段 End -->
         </plugins>
    </build>
</project>

配置文件application.yml如下

server:
  port: 8088
spring:
  application:
    name: cloud-system-provider
eureka:
  client:
    service-url:
      defaultZone: http://10.0.0.1:8761/eureka,http://10.0.0.1:8762/eureka

其服务SystemController发布如下

package com.bianjf.web.controller;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.bianjf.service.SystemService;

/**
* 对外发布的系统服务
*
*/
@RestController
@RequestMapping("/")
public class SystemController {
    @Autowired
    private SystemService systemService;
    
    /**
     * 获取数据
     * @return
     */
    @RequestMapping(value = "/{name}/system/getData", method = RequestMethod.GET)
    public Map<String, Object> getData(@PathVariable("name") String name) {
        Map<String, Object> resultMap = this.systemService.getData(name);
        return resultMap;
    }
}

提供的接口SystemService如下

package com.bianjf.service;
import java.util.Map;
/**
 * 系统服务
 *
 */
public interface SystemService {
    /**
     * 获取数据
     * @param name
     * @return
     */
    Map<String, Object> getData(String name);
}

接口实现类SystemServiceImpl如下

package com.bianjf.service.impl;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Service;
import com.bianjf.service.SystemService;

@Service
public class SystemServiceImpl implements SystemService {

    @Override
    public Map<String, Object> getData(String name) {
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("id", 1);
        resultMap.put("name", name);
        return resultMap;
    }
}

Spring Boot入口类CloudSystemProviderApplication如下

package com.bianjf.application;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan(basePackages = {"com.bianjf"})//扫描其他包的注解,将其放入Spring的IoC容器
@EnableDiscoveryClient//开启Eureka服务发现
@SpringBootApplication
public class CloudSystemProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(CloudSystemProviderApplication.class, args);
    }
}

cloud-system-provider可放入多台服务器提供数据计算服务,组成一个集群

1.3.2 Ribbon消费者

本次通过Ribbon进行集群的功能。创建工程目录结构如下
image.png

需要注入的包pom.xml如下

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.bianjf</groupId>
    <artifactId>cloud-ribbon-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <parent>
         <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
         <version>1.5.10.RELEASE</version>
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
         <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR5</spring-cloud.version>
    </properties>
    <dependencyManagement>
         <dependencies>
             <!-- Spring Cloud Start -->
             <dependency>
                 <groupId>org.springframework.cloud</groupId>
                 <artifactId>spring-cloud-dependencies</artifactId>
                 <version>${spring-cloud.version}</version>
                 <type>pom</type>
                 <scope>import</scope>
             </dependency>
             <!-- Spring Cloud End -->
         </dependencies>
    </dependencyManagement>
    <dependencies>
         <!-- Spring Boot Start -->
         <!-- Spring Boot Web Start -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>
         </dependency>
         <!-- Spring Boot Web End -->
         <!-- Spring Boot Test Start -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-test</artifactId>
         </dependency>
         <!-- Spring Boot Test End -->
         <!-- Spring Boot Actuator Start -->
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-actuator</artifactId>
         </dependency>
         <!-- Spring Boot Actuator End -->
         <!-- Spring Boot End -->
         <!-- Spring Cloud Eureka Client Start -->
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-eureka</artifactId>
         </dependency>
         <!-- Spring Cloud Eureka Client End -->
         <!-- Spring Cloud Ribbon Start -->
         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-ribbon</artifactId>
         </dependency>
         <!-- Spring Cloud Ribbon End -->
    </dependencies>
    <build>
         <plugins>
             <!-- Spring Boot启动插件 Start -->
             <plugin>
                 <groupId>org.springframework.boot</groupId>
                 <artifactId>spring-boot-maven-plugin</artifactId>
             </plugin>
             <!-- Spring Boot启动插件 End -->
             <!-- 编译工具 Start -->
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
                 <configuration>
                      <source>1.8</source>
                      <target>1.8</target>
                      <encoding>UTF-8</encoding>
                 </configuration>
             </plugin>
             <!-- 编译工具 End -->
             <!-- Maven Install 跳过Test阶段 Start -->
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-surefire-plugin</artifactId>
                 <configuration>
                      <skip>true</skip>
                 </configuration>
             </plugin>
             <!-- Maven Install 跳过Test阶段 End -->
         </plugins>
    </build>
</project>

配置文件application.yml如下

server:
  port: 8080
spring:
  application:
    name: cloud-ribbon-consumer
eureka:
  client:
    service-url:
      defaultZone: http://10.0.0.1:8761/eureka,http://10.0.0.1:8762/eureka

客户端接口SystemController如下

package com.bianjf.web.controller;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class SystemController {
    @Autowired
    private RestTemplate restTemplate;
    
    private final String SYSTEM_PROVIDER_BASE_URL = "http://CLOUD-SYSTEM-PROVIDER";
    
    /**
     * 获取用户信息
     * @param name
     * @return
     */
    @RequestMapping(value = "/{name}/system/getUser")
    public Map<String, Object> getUser(@PathVariable("name") String name) {
        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put("name", name);
        
        @SuppressWarnings("unchecked")
        Map<String, Object> resultMap = this.restTemplate.getForObject(this.SYSTEM_PROVIDER_BASE_URL + "/{name}/system/getData", Map.class, paramsMap);
        return resultMap;
    }
}

配置RestTemplate集群GlobalConfig如下

package com.bianjf.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
* 全局配置类
*
*/
@Configuration
public class GlobalConfig {
    /**
     * 集群形式的RestTemplate
     * @return
     */
    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

通过**@LoadBalanced**注解将RestTemplate赋予集群的功能,这样通过@Autowired注解直接引入RestTemplate,此时RestTemplate就有了集群的功能。

SpringBoot程序入口如下

package com.bianjf.application;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan(basePackages = {"com.bianjf"})//扫描其他包的注解
@EnableDiscoveryClient//开启Eureka服务发现功能
@SpringBootApplication
public class CloudRibbonApplication {
    public static void main(String[] args) {
        SpringApplication.run(CloudRibbonApplication.class, args);
    }
}

这样,一个简单的服务提供与服务消费就完成了。通过Ribbon对RestTemplate进行的集群功能。

本实例的两个工程如下
cloudribbon.zip

二 Ribbon

2.1 Ribbon介绍

在我们了解了Spring Cloud Ribbon使用之后,在介绍为什么通过注解@LoadBalanced就能将RestTemplate进行客户端集群之前,我们首先了解一下Ribbon。

2.1.1 Ribbon子模块

Ribbon主要有三大子模块:

  • ribbon-core:Ribbon核心项目,主要包括负载均衡器接口定义、客户端接口定义,内置的负载均衡实现等API
  • ribbon-eureka:为Eureka客户端提供的负载均衡实现类
  • ribbon-httpclient:对Apache的HttpClient进行封装,该模块提供了含有负载均衡功能的REST客户端。

2.1.2 负载均衡器组件

Ribbon的负载均衡器主要与集群中的各个服务器进行通信,负载均衡器主要提供以下基础功能:

  • 维护服务器的IP、DNS名称等信息
  • 根据特定的逻辑在服务器列表中循环
  • Rule:一个逻辑组件,这些逻辑将会决定,从服务器列表中返回哪个服务器实例
  • Ping:该组件主要使用定时器,来确保服务器网络可以连接
  • ServerList:服务器列表,通过静态的配置确定负载的服务器,也可以动态指定服务器列表。

2.2 第一个Ribbon程序

本次演示的环境如下
image.png

2.2.1 编写服务

首先需要编写一个REST服务。通过指定不同的端口,让服务可以启动多个实例。

新建工程first-ribbon-server,添加依赖如下:

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

新建Spring Boot启动类,RibbonServerApplication,如下

package com.bianjf.application;
import java.util.Scanner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.ComponentScan;

@ComponentScan(basePackages = {"com.bianjf"})
@SpringBootApplication
public class RibbonServerApplication {
    public static void main(String[] args) {
        System.out.println("请输入端口号: ");
        //读取控制台输入作为端口
        @SuppressWarnings("resource")
        Scanner scan = new Scanner(System.in);
        
        String port = scan.nextLine();
        
        //设置启动的服务器端口
        new SpringApplicationBuilder(RibbonServerApplication.class).properties("server.port=" + port).run(args);
        
    }
}

运行main方法,并在控制台输入端口号,即可启动Web服务。

提供者如下

package com.bianjf.web.controller;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MyController {
    @RequestMapping(value = "/findPerson", method = RequestMethod.GET)
    public Map<String, Object> findPerson(HttpServletRequest request) {
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("result", true);
        resultMap.put("msg", "查询成功.调用的URL是----->" + request.getRequestURL().toString() );
        return resultMap;
    }
}

工程源码如下
firstribbonserver.zip

2.2.2 编写请求客户端
新建客户端工程:first-ribbon-client,添加依赖如下

         <dependency>
             <groupId>com.netflix.ribbon</groupId>
             <artifactId>ribbon</artifactId>
             <version>2.2.2</version>
         </dependency>
         <dependency>
             <groupId>com.netflix.ribbon</groupId>
             <artifactId>ribbon-httpclient</artifactId>
             <version>2.2.2</version>
         </dependency>
         <dependency>
             <groupId>com.netflix.ribbon</groupId>
             <artifactId>ribbon-loadbalancer</artifactId>
             <version>2.2.2</version>
         </dependency>

接下来,使用Ribbon的客户端发送请求,RestClientTest如下

package com.bianjf.test;
import com.netflix.client.ClientFactory;
import com.netflix.client.http.HttpRequest;
import com.netflix.client.http.HttpResponse;
import com.netflix.config.ConfigurationManager;
import com.netflix.niws.client.http.RestClient;

@SuppressWarnings("deprecation")
public class RestClientTest {
    public static void main(String[] args) throws Exception {
        //设置请求的服务器
        ConfigurationManager.getConfigInstance()
            .setProperty("first-client.ribbon.listOfServers", "localhost:8080,localhost:8081");
        //设置REST请求客户端
        RestClient client = (RestClient)ClientFactory.getNamedClient("first-client");
        
        //创建请求实例
        HttpRequest request = HttpRequest.newBuilder().uri("/findPerson").build();
        
        //发送6次请求到服务器
        for (int i = 0; i < 6; i ++) {
            HttpResponse response = client.executeWithLoadBalancer(request);
            String result = response.getEntity(String.class);
            System.out.println("结果: " + result);
        }
    }
}

使用了ConfigurationManager类来配置了请求的服务器列表,再使用RestClient对象,向/findPerson地址发送6次请求,控制台输出如下
image.png

根据输出结果可知,RestClient轮流向8080与8081端口发送请求,可见RestClient中已经实现了负载均衡的功能。

2.2.3 Ribbon配置

上述使用代码ConfigurationManager来设置配置项,也可以通过配置文件(properties或者yml)来配置,配置格式如下:

<client>.<nameSpace>.<property>=<value>
  • :声明该配置属于哪一个客户端。如果为空,则会作用在所有客户端
  • :命名空间,默认为“ribbon”
  • :属性名
  • :属性值

2.3 Ribbon负载均衡器

2.3.1 负载均衡器

Ribbon的负载均衡器接口,定义了服务器的操作,主要用于进行服务器选择。在RestClient类中,发送请求时,会使用负载均衡器(ILoadBalancer)接口。根据特定的逻辑来选择服务器,服务器列表可使用listOfServers进行配置。
如下代码,为负载均衡器选择服务器,ChooseServerTest

package com.bianjf.test;
import java.util.ArrayList;
import java.util.List;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;

public class ChooseServerTest {
    public static void main(String[] args) {
        //创建负载均衡器
        ILoadBalancer loadBalance = new BaseLoadBalancer();
        
        //服务器列表
        List<Server> serverList = new ArrayList<>();
        //添加服务器
        serverList.add(new Server("localhost", 8080));
        serverList.add(new Server("localhost", 8081));
        loadBalance.addServers(serverList);
        
        //进行6次服务器选择
        for (int i = 0; i < 6; i ++) {
            Server server = loadBalance.chooseServer(null);
            System.out.println("选择的服务器: " + server);
        }
    }
}

代码中使用了BaseLoadBalancer这个负载均衡器,将两个服务器对象加入到负载均衡器中,再调用6次chooseServer方法,可以看到输出如下

在默认情况下,会使用RoundRobinRule的逻辑规则

2.3.2 自定义负载规则

选择哪个服务器进行请求处理,由ILoadBalancer接口的chooseServer方法决定。而在BaseLoadBalancer类中,则使用IRule接口的choose方法来决定选择哪一个服务器对象。

如果自定义负载均衡规定,可以编写一个IRule接口的实现类:

package com.bianjf.test;
import java.util.List;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.Server;

/**
* 自定义负载均衡的规则
* 需要实现netflix的IRule接口
*
*/
public class MyRule implements IRule{
    private ILoadBalancer loadBalance;

    public MyRule() {
    }

    public MyRule(ILoadBalancer loadBalance) {
        this.loadBalance = loadBalance;
    }

    @Override
    public Server choose(Object key) {
        //获取全部的服务器
        List<Server> serverList = this.loadBalance.getAllServers();
        
        //永远只返回第一个服务器
        return serverList.get(0);
    }

    @Override
    public void setLoadBalancer(ILoadBalancer lb) {
        this.loadBalance = lb;
    }

    @Override
    public ILoadBalancer getLoadBalancer() {
        return this.loadBalance;
    }   
}

在自定义规则类中,实现了choose方法,调用了ILoadBalancer的getAllServers方法返回全部的服务器。为了简单,本实例只返回第一个服务器。在负载均衡器中使用如下

package com.bianjf.test;
import java.util.ArrayList;
import java.util.List;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;

public class MyRuleTest {
    public static void main(String[] args) {
        // 创建负载均衡器
        BaseLoadBalancer loadBalance = new BaseLoadBalancer();

        // 设置自定义的负载规则: 永远只选择第一台服务器
        loadBalance.setRule(new MyRule(loadBalance));

        // 添加服务器
        List<Server> serverList = new ArrayList<>();
        serverList.add(new Server("localhost", 8080));
        serverList.add(new Server("localhost", 8081));
        loadBalance.setServersList(serverList);

        // 进行6次服务器选择
        for (int i = 0; i < 6; i++) {
            Server server = loadBalance.chooseServer(null);
            System.out.println("选择的服务器: " + server);
        }
    }
}

在Ribbon中使用自定义负载规则,代码配置如下

package com.bianjf.test;
import com.netflix.client.ClientFactory;
import com.netflix.client.http.HttpRequest;
import com.netflix.client.http.HttpResponse;
import com.netflix.config.ConfigurationManager;
import com.netflix.niws.client.http.RestClient;

@SuppressWarnings("deprecation")
public class MyRuleConfigTest {
    public static void main(String[] args) throws Exception {
        //添加请求服务器
        ConfigurationManager.getConfigInstance()
            .setProperty("first-client.ribbon.listOfServers", "localhost:8080,localhost:8081");
        //配置规则处理类
        ConfigurationManager.getConfigInstance()
            .setProperty("first-client.ribbon.NFLoadBalancerRuleClassName", MyRule.class.getName());
        
        //获取Rest请求客户端
        RestClient client = (RestClient)ClientFactory.getNamedClient("first-client");
        
        //创建请求实例
        HttpRequest request = HttpRequest.newBuilder().uri("/findPerson").build();
        
        //发送10次请求到服务器
        for (int i = 0; i < 10; i ++) {
            HttpResponse response = client.executeWithLoadBalancer(request);
            String result = response.getEntity(String.class);
            System.out.println("结果: " + result);
        }
        
    }
}

请求客户端中,加入了“first-client.ribbon.NFLoadBalancerRuleClassName”属性,设置了自定义规则处理类为MyRule。这个配置项同样可以在配置文件这种使用

2.3.3 Ribbon自带的负载规则

  • RoundRobinRule:系统默认的规则,通过简单的轮询服务器列表来选择服务器
  • AvailabilityFilteringRule:该规则会忽略以下服务器
  • 无法连接的服务器:在默认情况下,如果3次连接失败,该服务器将会被置为“短路”状态,该状态将持续30秒,如果再次连接失败,“短路”状态持续时间将会以几何级增加。可以通过修改:niws.loadbalancer..connectionFailureCountThreshold属性来配置连接失败的次数
  • 并发数过高的服务器:如果连接到该服务器的并发数过高,也会被这个规则忽略,可以通过修改:.ribbon.ActiveConnectionsLimit属性来设定最高并发数
  • WeightedResponseTimeRule:为服务器赋予一个权重值,服务器的响应时间越长,该权重值就越少,这个规则会随机选择服务器,这个权重值有可能会决定服务器的选择。
  • ZoneAvoidanceRule:该规则以区域、可用服务器为基础,进行服务器选择。
  • BestAvailableRule:忽略“短路”的服务器,并选择并发数较低的服务器
  • RandomRule:随机选择可用的服务器
  • RetryRule:含有重试的选择逻辑,如果使用RoundRobinRule选择服务器无法连接,那么将会重新选择服务器

2.3.4 Ping机制

在负载均衡器中,提供了Ping的机制,每隔一段时间,会去Ping服务器,判断服务器是否存活。该工作由IPing接口的实现类负责。如果单独使用Ribbon,在默认情况下,不会激活Ping机制,默认的实现类为DummyPing。

如下,使用另外一个IPing实现类PingUrl

package com.bianjf.test;
import java.util.ArrayList;
import java.util.List;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.PingUrl;
import com.netflix.loadbalancer.Server;

public class PingUrlTest {
    public static void main(String[] args) throws InterruptedException {
        //创建负载均衡器
        BaseLoadBalancer loadBalance = new BaseLoadBalancer();
        //添加服务器
        List<Server> serverList = new ArrayList<>();
        //8080端口连接正常
        serverList.add(new Server("localhost", 8080));
        //一个不存在的端口
        serverList.add(new Server("localhost", 8081));
        loadBalance.addServers(serverList);
        
        //设置Ping实现类
        loadBalance.setPing(new PingUrl());
        //设置Ping的时间间隔为2秒
        loadBalance.setPingInterval(2);
        
        Thread.sleep(10000);
        
        for (Server server : loadBalance.getAllServers()) {
            System.out.println(server.getHostPort() + " 状态: " + server.isAlive());
        }
    }
}

也可以在Ribbon中进行配置IPing的实现类:

package com.bianjf.test;
import java.util.List;
import com.netflix.client.ClientFactory;
import com.netflix.config.ConfigurationManager;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.niws.client.http.RestClient;

@SuppressWarnings("deprecation")
public class PingUrlConfigTest {
    public static void main(String[] args) throws InterruptedException {
        //设置请求服务器列表
        ConfigurationManager.getConfigInstance()
            .setProperty("first-client.ribbon.listOfServers", "localhost:8080,localhost:8888");
        //设置Ping类
        ConfigurationManager.getConfigInstance()
            .setProperty("first-client.ribbon.NFLoadBalancerPingClassName", MyPing.class.getName());
        //设置Ping的时间间隔
        ConfigurationManager.getConfigInstance()
            .setProperty("first-client.ribbon.NFLoadBalancePingInterval", 2);
        
        //获取RestClient客户端
        RestClient client = (RestClient)ClientFactory.getNamedClient("first-client");
        
        Thread.sleep(10000);
        
        //获取全部服务器
        ILoadBalancer loadBalancer = client.getLoadBalancer();
        List<Server> allServers = loadBalancer.getAllServers();
        
        //输出状态
        for (Server server : allServers) {
            System.out.println(server.getHostPort() + " 状态: " + server.isAlive());
        }
    }
}
  • .ribbon.NFLoadBalancerPingClassName:配置IPing的实现类
  • .ribbon.NFLoadBalancerPingInterval:配置Ping操作的时间间隔

2.3.5 自定义Ping

自定义Ping,实现IPing接口,然后使用..NFLoadBalancerPingClassName配置即可

2.3.6 其他配置

  • NFLoadBalancerCLassName:指定负载均衡器的实现类。可利用该配置,实现自己的负载均衡器
  • NIWSServerListClassName:服务器列表处理类。用来维护服务器列表。Ribbon已经实现动态服务器列表
  • NIWSServerListFilterClassName:用于处理服务器列表拦截

而在Eureka环境中使用Ribbon,如果要自定义Rule或者Ping的话,可以在配置类中使用@Bean注解进行配置即可,类似RestTemplate的配置

2.3.7 重试机制

由于Spring Cloud Eureka实现的服务治理机制强调了CAP理论中的AP,即可用性与可靠性,它与ZooKeeper这类强调CP(一致性、可靠性)的服务治理框架最大的区别就是,Eureka为了实现更高的服务可用性,牺牲了一定的一致性,在极端情况下它宁愿接受故障实例也不要丢掉“健康”实例。

比如,当服务注册中心的网络发生故障断开时,由于所有的服务实例无法维持续约心跳,在强调AP的服务治理中将会把所有服务实例都剔除掉,而Eureka则会因为超过85%的实例丢失心跳而会出发保护机制,注册中心将会保留此时的所有节点,以实现服务间依然可以进行互相调用的场景,即使其中有部分故障节点,但这样做可以继续保障大多数的服务正常消费。

可以在配置文件中增加如下内容:

spring.cloud.loadbalancer.retry.enabled=true

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=1000

first-ribbon-server.ribbon.ConnectTimeout=250
first-ribbon-server.ribbon.ReadTimeout=1000
first-ribbon-server.ribbon.OkToRetryOnAllOperations=true
first-ribbon-server.ribbon.MaxAutoRetriesNextServer=2
first-ribbon-server.ribbon.MaxAutoRetries=1
  • spring.cloud.loadbalancer.retry.enabled:用来是否开启重试机制。默认是关闭的。
  • hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds:断路器的超时时间需要大于Ribbon的超时时间,不然不会触发重试
  • first-ribbon-server.ribbon.ConnectTimeout:请求连接的超时时间
  • first-ribbon-server.ribbon.ReadTimeout:请求处理的超时时间
  • first-ribbon-server.ribbon.OkToRetryOnAllOperations:对所有操作请求都进行重试
  • first-ribbon-server.ribbon.MaxAutoRetriesNextServer:切换实例的重试次数
  • first-ribbon-server.ribbon.MaxAutoRetries:对当前实例的重试次数

根据如上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由MaxAutoRetries配置),如果不行,就换一次实例进行访问,如果还是不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置),如果依然不行,返回失败信息

2.4 总结

源码在此就不做分析了。但是经过源码可知:
Ribbon的负载均衡,主要通过LoadBalancerClient来实现的,而LoadBalancerClient具体交给了ILoadBalancer来处理,ILoadBalancer通过配置IRule、IPing等信息,并向EurekaClient获取注册列表的信息,并默认10秒一次向EurekaClient发送“ping”,进而检查是否更新服务列表。最后,得到注册列表后,ILoadBalancer根据IRule的策略进行负载均衡。
而RestTemplate被@LoadBalance注解后,通过用负载均衡,主要是维护了一个呗@LoadBalance注解的RestTemplate列表,并给列表的RestTemplate添加连接器,进而交给负载均衡器去处理

0

评论区