Ribbon 入门教程 - 基础篇

Ribbon 介绍

Ribbon 是什么

Ribbon 是 Netflix 公司开发的一个负载均衡组件,诞生于 2013 年 1 月,一直是 Netflix 活跃度较高的项目,由 Pivotal 公司将其整合进 Spring Cloud 生态。Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,通过 Spring Cloud 的封装, 可以轻松地将面向服务的 REST 模板请求自动转换成客户端负载均衡的服务调用。 此外,Ribbon 拥有丰富的负载均衡策略、重试机制、支持多协议的异步与响应式模型、容错、缓存与批次处理等功能。Ribbon 虽然只是一个工具类框架,它不像服务注册中心、配置中心、API 网关那样需要独立部署,但是它几乎存在于每一个 Spring Cloud 构建的微服务和基础设施中。 因为微服务间的调用,API 网关的请求转发等内容实际上都是通过 Ribbon 来实现的,Feign、Zuul 已经集成了 Ribbon,更多介绍可参考:Ribbon 项目Ribbon 官方英文文档Spring Cloud Ribbon 官方中文文档

Ribbon 与负载均衡

负载均衡(Load Balance),即利用特定方式将流量分摊到多个操作单元上的一种手段,它对系统吞吐量与系统处理能力有着质的提升,当今极少企业没有用到负载均衡器或是负载均衡策略。常见的负载均衡实现有 Nginx 与 LVS,且不管它们的使用方式,工作在什么层次,本质还是对流量的疏导。业界对于负载均衡有不少分类,最常见的有软负载与硬负载,代表产品是 Nginx 与 F5;还有一组分类最能体现出 Ribbon 与传统负载均衡的差别,那就是集中式负载均衡与进程内负载均衡。集中式负载均衡指位于因特网与服务提供者之间,并负责把网络请求转发到各个提供单位,这时候 Nginx 与 F5 就可以归为一类,也可以称是服务端负载均衡。进程内负载均衡是指从一个实例库选取一个实例进行流量导入,在微服务的范畴内,实例库一般存储在 Zookeeper、Eureka、Consul、etcd 这样的注册中心,而此时的负载均衡器就是类似 Ribbon 的 IPC(Inter-Process Communication,进程间通信)组件,因此进程内负载均衡也叫做客户端负载均衡。

ribbon-load-aalance

Ribbon 入门案例

1. 版本说明

在下面的的教程中,使用的 Spring Cloud 版本是 Finchley.RELEASE,对应的 Spring Boot 版本是 2.0.3,点击下载完整的案例代码

2. 创建 Maven 父级 Pom 工程

在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
</parent>

<!-- 利用传递依赖,公共部分 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<!-- 管理依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<!--注意:这里需要添加以下配置,否则可能会有各种依赖问题 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

3. 创建 Eureka Server 工程

创建 Eureka Server 的 Maven 工程,配置工程里的 pom.xml 文件,需要引入 spring-cloud-starter-netflix-eureka-server

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>

创建 Eureka Server 的启动主类,这里添加相应注解,作为程序的入口:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

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

添加 Eureka Server 需要的 application.yml 配置文件到工程中

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8090

eureka:
instance:
hostname: 127.0.0.1
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

4. 创建 Provider 源服务工程

为了测试 Ribbon 的负载均衡功能,必须要有一个源服务(服务提供者),并且可以选择启动多个实例,在每个实例中需要有一个标识(例如端口)来识别每次的调用是到了不同的服务实例上。这里可以使用一份代码,采取改变端口号的方式启动多次,就能启动多个相同的服务实例。创建 Provider 的 Maven 工程后,由于需要将服务注册到 Eureka Server,工程下的 pom.xml 文件需要引入 spring-cloud-starter-netflix-eureka-client

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>

创建 Provider 的启动主类,添加注解 @EnableDiscoveryClient,将服务注册到 Eureka Server:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {

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

application.yml 文件中指定服务名称(provider、注册中心地址与端口号,后面启动多实例只需要修改这里的端口号即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 9090

spring:
application:
name: provider

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8090/eureka
instance:
prefer-ip-address: true

创建用于测试的 Controller 类:

1
2
3
4
5
6
7
8
@RestController
public class ProviderController {

@GetMapping("/provider/add")
public String add(Integer a, Integer b, HttpServletRequest request) {
return "From Port: " + request.getServerPort() + ", Result: " + (a + b);
}
}

5. 创建 Ribbon 客户端工程

要使用 Ribbon,需要在 pom.xml 文件中引入依赖 spring-cloud-starter-netflix-ribbon,另外由于需要从 Eureka Server 获取服务列表,即作为 Eureka 客户端,还需要引入 spring-cloud-starter-netflix-eureka-client

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>

创建启动主类,添加注解 @EnableDiscoveryClient

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
public class RibbonLoadBalanceApplication {

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

创建统一配置类,声明 RestTemplate 的 Bean,并且添加注解 @LoadBalanced,指定该 RestTemplate 需要使用客户端负载均衡:

1
2
3
4
5
6
7
8
9
@Configuration
public class CommonConfiguration{

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}

创建用于测试的 Controller 类,因为 Ribbon 客户端需要创建一个 API 来供第三方调用 Provider 源服务的那个自定义 API,这里需要用 RestTemplate 来调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class CalculateController {

@Autowired
private RestTemplate restTemplate;

/**
* 这里的"PROVIDER"是服务提供者的实例名称的英文大写
*/
@GetMapping("/add")
public String add(Integer a, Integer b) {
String result = restTemplate.getForObject("http://PROVIDER/provider/add?a=" + a + "&b=" + b, String.class);
return result;
}
}

application.yml 文件中配置端口号、注册中心地址即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 8080

spring:
application:
name: ribbon-loadbalance

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8090/eureka
instance:
prefer-ip-address: true

6. 测试

  1. 启动 Eureka Server 后,更改 Provider 源服务的端口号为 9091 与 9092 后分别启动,浏览器访问 http://127.0.0.1:8090,查看 Eureka Server 的界面是否正常显示多个 Provider 源服务
  2. 启动 Ribbon 客户端应用,浏览器访问 http://127.0.0.1:8080/add?a=10&b=5,若正常返回计算结果,说明整个项目运行成功
  3. 提示:当存在多个服务提供者时,Ribbon 默认会使用轮询的方式访问源服务,此外 Ribbon 对服务实例节点的增减也能动态感知

Ribbon 负载均衡策略

内置的七种负载均衡策略

Ribbon 按照不同的需求,已经提供 7 种实现了 IRule 接口的实现类,包含了常用的负载均衡策略,默认的策略是轮询策略。内置的策略能够适用大部分负载均衡需求的应用场景,若有更复杂的需求,可以自己实现 IRule 接口。

ribbon-rule

策略类命名描述实现说明
RandomRule 随机策略随机选择可用的服务器在 index 上随机,选择 index 对应位置的服务器
RoundRobinRule 轮询策略按顺序循环选择服务器轮询 index,选择 index 对应位置的服务器
RetryRule 重试策略对选定的负载均衡策略机上重试机制在一个配置时间段内当选择服务器不成功,则一直尝试使用 subRule 的方式选择一个可用的服务器
BestAvailableRule 最低并发策略选择一个并发请求量最少的服务器逐个考察服务器,如果服务器的断路器打开,则忽略,再选择其他并发连接数最低的服务器
AvailabilityFilteringRule 可用过滤策略过滤掉一直连接失败并被标记为 circuit tipped 的服务器,过滤掉那些高并发连接的服务器(active connections 超过配置的阀值)使用一个 AvailabilityPredicate 来包含过滤服务器的逻辑,其实就是检查 status 里记录的各个服务器的运行状态
WeightedResponseTimeRule 响应时间加权策略根据服务器的响应时间分配权重。响应时间越长,权重越低,被选择到的概率越低;响应时间越短,权重越高,被选择到的概率就越高。这个策略很贴切,综合了各种因素,如:网络、磁盘、CPU 等,这些因素都直接影响着响应时间一个后台线程定期的从 status 里面读取评价响应时间,为每个服务器计算一个权重;其中权重的计算也比较简单,responsetime 减去每个服务器自己平均的 responsetime 就是服务器的权重。当刚开始运行,没有形成 status 时,使用轮询策略选择服务器
ZoneAvoidanceRule 区域权衡策略综合判断服务器所在区域的性能和服务器的可用性来轮询选择服务器,并且判断一个 Zone 的运行性能是否可用,剔除不可用的 Zone 中的所有服务器使用 ZoneAvoidancePredicate 和 AvailabilityPredicate 来判断是否选择某个服务器,前一个判断判定一个 zone 的运行性能是否可用,剔除不可用的 zone 的所有服务器,AvailabilityPredicate 则用于过滤掉连接数过多的服务器

全局负载均衡策略的配置

使用 Ribbon 的时候若想要全局更改负载均衡策略,此时只需要增加一个配置类,加上之后凡是通过 Ribbon 的请求都会按照配置的策略来进行负载均衡

1
2
3
4
5
6
7
8
@Configuration
public class RuleConfiguration {

@Bean
public IRule customRule(){
return new RandomRule();
}
}

基于注解的自定义策略配置

若想针对某一个源服务设置其特有的负载均衡策略,可以通过使用 @RibbonClient 注解来实现。

创建 @AvoidScan 注解类,只有一个空的声明

1
2
3
public @interface AvoidScan {

}

配置类里注入针对 Ribbon 客户端的配置管理器(非必须),添加 @AvoidScan 注解

1
2
3
4
5
6
7
8
9
10
11
12
@AvoidScan
@Configuration
public class RuleConfiguration {

@Autowired
IClientConfig config;

@Bean
public IRule customRule(IClientConfig config){
return new RandomRule();
}
}

启动主类加上 @RibbonClient 注解,配置内容表示对 provider 服务使用的策略是通过 RuleConfiguration 类所配置的。此外这里使用 @ComponentScan 注解的意思是让 Spring 不去扫描被 @AvoidScan 注解标记的配置类,因为这里的策略配置是希望对单个源服务生效的,所以不能应用于全局。若不想使用 @AvoidScan 注解,只要保证 RuleConfiguration 类 不被 @ComponentScan 注解扫描到就行,简单的做法可以将 RuleConfiguration 类不存放在启动主类(RibbonLoadBalanceApplication)所在的包及其子包下。特别注意:无论是在 Java 代码中还是 YML 配置文件中,只要引用到服务名称的地方,都需要使用大写英文字符的服务名称,否则会找不到对应的服务提供者,导致策略的配置不生效

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableDiscoveryClient
@RibbonClient(name = "PROVIDER", configuration = RuleConfiguration.class)
@ComponentScan(excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = {AvoidScan.class})})
public class RibbonLoadBalanceApplication {

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

若想对多个源服务指定对应的负载均衡策略,可以使用 @RibbonClients 注解

1
2
3
4
5
6
@RibbonClients(value = {
@RibbonClient(name = "PROVIDER-USER", configuration = UserRuleConfiguration.class),
@RibbonClient(name = "PROVIDER-DEPT", configuration = DeptRuleConfiguration.class)})
public class RibbonLoadBalanceApplication {

}

基于配置文件的自定义策略配置

可以使用配置文件来对源服务的负载均衡策略进行配置,其基本语法是 <service name>.ribbon.*,使用它几乎可以不用写注解形式的任何配置代码,下述配置是对 provider 服务使用随机策略

1
2
3
PROVIDER:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

区域权衡策略的配置

Ribbon 默认实现了区域权衡策略,因此可以通过 Eureka 实例的元数据配置来实现区域化的实例配置方案。Ribbon 会优先访问与客户端处于一个 Zone 中的服务端实例,只有当被选中的 Zone 中没有可用的服务端实例的时候才会访问其他 Zone 中的服务端实例。因此通过 zone 属性的定义,将处于不同机房的实例配置成不同的区域值,配合实际部署的物理结构,可以有效地设计出针对区域性故障的容错集群。而实现的方式非常简单,只需在 Eureka 的服务实例的元数据中增加 zone 参考来指定自己所在的区域,下述内容是配置在 Eureka 的服务实例中:

1
2
3
4
eureka:
instance:
metadata-map:
zone: shanghai

Ribbon 配置实战

Ribbon 超时与重试

使用 HTTP 发起请求,免不了遇到极端环境,此时对调用进行时限控制以及时限之后的重试尤为重要。在 Spring Cloud 的 Brixtion 版本中,对于重试机制的实现需要开发者自行扩展实现,而从 Camden SR2 版本开始,Spring Cloud 整合了 Spring Retry 来增加 RestTemplate 的重试能力,只需要通过简单的配置,原来那些通过 RestTemplate 实现的服务访问就会自动根据配置来实现重试机制。注意,Finchley 版中 Ribbon 的重试机制默认是开启的,只需要添加针对超时时间与重试策略的配置即可。下述配置是对 provider 服务配置超时与重试相关参数:

1
2
3
4
5
6
7
8
PROVIDER:
ribbon:
ConnectTimeout: 3000
ReadTimeout: 60000
MaxAutoRetries: 1 #对第一次请求的服务的重试次数
MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包括第一个服务)
OkToRetryOnAllOperations: true
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

Ribbon 的饥饿加载

Ribbon 在进行客户端负载均衡的时候,并不是启动时就加载上下文的,而是在实际请求的时候才去创建,因此这个特性往往会让第一次调用显得疲软乏力,严重的时候会引起调用超时。此时可用通过指定 Ribbon 具体的客户端的名称来开启饥饿加载,即在启动的时候便加载所有配置项的应用程序上下文。

1
2
3
4
ribbon:
eager-load:
enabled: true
clients: PROVIDER-USER, PROVIDER-DEPT, PROVIDER-ORDER

基于配置文件自定义 Ribbon 客户端

Ribbon 在 1.2.0 版本之后,支持使用配置文件来定制 Ribbon 客户端,其实质就是使用配置文件来指定一些默认加载类,从而更改 Ribbon 客户端的默认行为方式,并且这种方法的优先级是最高的,优先级高于使用注解 @RibbonClient 指定的配置和 Java 源码中加载的相关 Bean,具体配置规则如下:

ribbon-config-file

下述配置,表示是对 provider 服务使用随机策略

1
2
3
PROVIDER:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

Ribbon 脱离 Eureka 的使用

在默认情况下,Ribbon 客户端会从 Eureka 注册中心读取服务注册信息列表,来达到动态负载均衡的目的。若不想从 Eureka 注册中心读取服务注册列表,可以配置 Ribbon 客户端使用指定的源服务地址,让 Ribbon 脱离 Eureka 使用,配置实例如下:

1
2
3
4
5
6
7
ribbon:
eureka:
enabled: false #禁用Eureka的功能

PROVIDER:
ribbon:
listOfServers: http://127.0.0.1:7000, http://127.0.0.1:7001 #指定provider服务的服务地址

Ribbon 进阶

核心接口

ribbon-interface