文章目录
概述
Ribbon 是一个进程间通信的客户端库,并且已经在云环境下经过大量的生产测试。它提供了以下功能:
- 负载均衡
- 容错机制
- 支持 HTTP、TCP、UDP 等多种协议,并支持异步和响应式的调用方式
- 缓存与批处理
负载均衡实现方式
为了保证服务的可用性,我们会给每个服务部署多个实例,避免因为单个实例挂掉之后,导致整个服务不可用。并且,因为每个服务部署了多个实例,也提升了服务的承载能力,可以同时处理更多的请求。
不过此时需要考虑到需要将请求均衡合理的分配,保证每个服务实例的负载。一般来说,我们有两种方式实现服务的负载均衡,分别是客户端级别和服务端级别。
服务端级别的负载均衡,客户端通过外部的代理服务器,将请求转发到后端的多个服务。比较常见的有 Nginx 服务器,如下图所示:
客户端级别的负载均衡,客户端通过内嵌的“代理”,将请求转发到后端的多个服务。比较常见的有 Dubbo、Ribbon 框架提供的负载均衡功能,如下图所示:
相比来说,客户端级别的负载均衡可以有更好的性能,因为不需要多经过一层代理服务器。并且,服务端级别的负载均衡需要额外考虑代理服务的高可用,以及请求量较大时的负载压力。因此,在微服务场景下,一般采用客户端级别的负载均衡为主。
对于客户端的负载均衡来说,最好搭配注册中心一起使用。这样,服务实例的启动和关闭,可以向注册中心发起注册和取消注册,保证能够动态的通知到客户端。
本文,我们会使用 Ribbon 提供客户端级别的负载均衡,使用 Nacos 作为注册中心。整体架构图如下:
快速入门
本小节,我们来搭建一个 Spring Cloud Netflix Ribbon 组件的快速入门示例。步骤如下:
- 首先,搭建一个服务提供者
demo-provider
,启动 2 个实例,注册服务到 Nacos 中。 - 然后,搭建一个服务消费者
demo-consumer
,使用 Ribbon 进行负载均衡,调用服务提供者demo-provider
的 HTTP 接口。
搭建服务提供者
搭建服务消费者
在导入依赖过程中:我们没有主动引入 spring-cloud-netflix-ribbon
依赖,因为 spring-cloud-starter-alibaba-nacos-discovery
默认引入了它。如下图所示:
@SpringBootApplication
public class DemoConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(DemoConsumerApplication.class, args);
}
@Configuration
public class RestTemplateConfiguration {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@RestController
static class TestController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancerClient loadBalancerClient;
@GetMapping("/hello")
public String hello(String name) {
// 获得服务 `demo-provider` 的一个实例
ServiceInstance instance = loadBalancerClient.choose("demo-provider");
// 发起调用
String targetUrl = instance.getUri() + "/echo?name=" + name;
String response = restTemplate.getForObject(targetUrl, String.class);
// 返回结果
return "consumer:" + response;
}
@GetMapping("/hello02")
public String hello02(String name) {
// 直接使用 RestTemplate 调用服务 `demo-provider`
String targetUrl = "http://demo-provider/echo?name=" + name;
String response = restTemplate.getForObject(targetUrl, String.class);
// 返回结果
return "consumer:" + response;
}
}
}
-
@LoadBalanced
注解,声明 RestTemplate Bean 被配置使用 Spring Cloud LoadBalancerClient(负载均衡客户端),实现在请求目标服务时,能够进行负载均衡。 -
TestController 提供了
/hello
和/hello02
接口,都用于调用服务提供者的/demo
接口。代码略微有几行,我们来稍微解释下哈。loadBalancerClient
属性,LoadBalancerClient 对象,负载均衡客户端。稍后我们会使用它,从 Nacos 获取的服务demo-provider
的实例列表中,选择一个进行 HTTP 调用。 -
/hello
接口,使用 LoadBalancerClient 先选择服务demo-provider
的一个实例,在使用 RestTemplate 调用服务demo-provider
的/demo
接口。不过要注意,这里执行会报如下异常:java.lang.IllegalStateException: No instances available for 10.171.1.115
- 因为我们这里创建的 RestTemplate Bean 是添加了
@LoadBalanced
注解,它会把传入的"10.171.1.115"
当做一个服务,显然是找不到对应的服务实例,所以会报 IllegalStateException 异常。 - 解决办法也非常简单,再声明一个未使用
@LoadBalanced
注解的 RestTemplate Bean 即可,并使用它发起请求。
- 因为我们这里创建的 RestTemplate Bean 是添加了
-
/hello02
接口,直接使用 RestTemplate 调用服务demo-provider
,代码精简了。这里要注意,在使用@LoadBalanced
注解的 RestTemplate Bean 发起 HTTP 请求时,需要将原本准备传入的host:port
修改成服务名,例如这里我们传入了demo-provider
。虽然
/hello02
接口相比/hello
接口只精简了一行代码,但是它带来的不仅仅是表面所看到的。例如说,如果我们调用服务的一个实例失败时,想要重试另外一个示例,就存在了很大的差异。/hello02
接口的方式,可以自动通过 LoadBalancerClient 重新选择一个该服务的实例,再次发起调用。/hello
接口的方式,需要自己手动写逻辑,使用 LoadBalancerClient 重新选择一个该服务的实例,后交给 RestTemplate 再发起调用。
负载均衡规则
Ribbon 内置了 7 种负载均衡规则,如下图所示:
每个发负载均衡规则说明如下:
策略名 | 策略描述 | 实现说明 |
---|---|---|
RandomRule | 随机选择一个 server | 在 index 上随机,选择 index 对应位置的 Server |
RoundRobinRule | 轮询选择 server | 轮询 index,选择 index 对应位置的 server |
ZoneAvoidanceRule | 复合判断 server 所在区域的性能和 server 的可用性选择 server | 使用 ZoneAvoidancePredicate 和 AvailabilityPredicate 来判断是否选择某个 server。ZoneAvoidancePredicate 判断判定一个 zone 的运行性能是否可用,剔除不可用的 zone(的所有 server);AvailabilityPredicate 用于过滤掉连接数过多的 server。 |
BestAvailableRule | 选择一个最小并发请求的 server | 逐个考察 server,如果 server 被 tripped 了则忽略,在选择其中activeRequestsCount 最小的 server |
AvailabilityFilteringRule | 过滤掉那些因为一直连接失败的被标记为 circuit tripped 的后端 server,并过滤掉那些高并发的的后端 server(activeConnections 超过配置的阈值) | 使用一个 AvailabilityPredicate 来包含过滤 server 的逻辑,其实就就是检查 status 里记录的各个 server 的运行状态 |
WeightedResponseTimeRule | 根据 server 的响应时间分配一个 weight,响应时间越长,weight 越小,被选中的可能性越低 | 一个后台线程定期的从 status 里面读取评价响应时间,为每个 server 计算一个 weight。weight 的计算也比较简单,responseTime 减去每个 server 自己平均的 responseTime 是 server 的权重。当刚开始运行,没有形成 status 时,使用 RoundRobinRule 策略选择 server。 |
RetryRule | 对选定的负载均衡策略机上重试机制 | 在一个配置时间段内当选择 server 不成功,则一直尝试使用 subRule 的方式选择一个可用的 server |
默认情况下,Ribbon 采用 ZoneAvoidanceRule 规则。因为大多数公司是单机房,所以一般只有一个 zone,而 ZoneAvoidanceRule 在仅有一个 zone 的情况下,会退化成轮询的选择方式,所以会和 RoundRobinRule 规则类似。
不同框架或者组件,会采用不同的负载均衡规则,感兴趣的胖友可以阅读如下文章:
- Nginx:《Nginx 负载均衡配置》 或 《Nginx 负载均衡的 5 种策略》
- Dubbo:《Dubbo 官方文档 —— 负载均衡》
- Motan:《Dubbo 官方文档 —— 用户指南(负载均衡)》
- SOFARPC:《SOFARPC 官方文档 —— 负载均衡》
总的来说,经典的负载均衡规则可以整理如下:
- 轮询(Round Robin) or 加权轮询(Weighted Round Robin)
- 随机(Random) or 加权随机(Weighted Random)
- 源地址哈希(Hash) or 一致性哈希(ConsistentHash)
- 最少连接数(Least Connections)
- 最小响应时间(ResponseTime)
自定义Ribbon
例如说,不再使用默认的负载均衡规则 ZoneAvoidanceRule,而是使用随机负载均衡规则 RandomRule。
在自定义 Ribbon 配置的时候,会有全局和客户端两种级别。相比来说,客户端级别是更细粒度的配置。针对每个服务,Spring Cloud Netflix Ribbon 会创建一个 Ribbon 客户端,并且使用服务名作为 Ribbon 客户端的名字。
实现 Ribbon 自定义配置,可以通过配置文件和 Spring JavaConfig 两种方式。
配置文件方式
通过在配置文件中,添加 {clientName}.ribbon.{key}={value}
配置项,设置指定名字的 Ribbon 客户端的指定属性的值。如此,我们就可以实现 Ribbon 客户端级别的自定义配置。
暂时没有找到配置文件的方式,实现 Ribbon 全局级别的自定义配置。搜了官方文档,也暂时没找到,目前猜测 Spring Cloud Netflix Ribbon 暂时没有提供。
修改 application.yaml
配置文件,额外添加如下配置:
demo-provider:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则全类名
- 通过
demo-provider.ribbon.NFLoadBalancerRuleClassName
配置项,设置名字为demo-provider
的 Ribbon 客户端的负载均衡规则为随机负载均衡规则 RandomRule。
Spring JavaConfig方式
使用 Spring JavaConfig 的方式,可以实现 Ribbon 全局和 客户端两种级别的自定义配置。
// RibbonConfiguration.java
@Configuration
@RibbonClients(
value = {
@RibbonClient(name = "demo-provider", configuration = UserProviderRibbonClientConfiguration.class) // 客户端级别的配置
},
defaultConfiguration = DefaultRibbonClientConfiguration.class // 全局配置
)
public class RibbonConfiguration {
}
// DefaultRibbonClientConfiguration.java
@Configuration
public class DefaultRibbonClientConfiguration {
@Bean
public IRule ribbonDefaultRule() {
return new RoundRobinRule();
}
}
// UserProviderRibbonClientConfiguration.java
@Configuration
public class UserProviderRibbonClientConfiguration {
@Bean
@Primary
public IRule ribbonCustomRule() {
return new RandomRule();
}
}
-
对于
DefaultRibbonClientConfiguration
和UserProviderRibbonClientConfiguration
两个配置类,我们并没有和DemoConsumerApplication
启动类放在一个包路径下。因为,Spring Boot 项目默认扫描 DemoConsumerApplication 所在包以及子包下的所有 Bean 们。而
@Configuration
注解也是一种 Bean,也会被扫描到。如果将 DefaultRibbonClientConfiguration 和 UserProviderRibbonClientConfiguration 放在 DemoConsumerApplication 所在包或子包中,将会被 Spring Boot 所扫描到,导致整个项目的 Ribbon 客户端都使用相同的 Ribbon 配置,无论是否在@RibbonClients注解中指定了不同的配置。那么这样就无法到达 Ribbon 客户端级别的自定义配置的目的。因此,这里在根路径下又创建了
ribbon
包,并将DefaultRibbonClientConfiguration
、UserProviderRibbonClientConfiguration
放入其中,避免被 Spring Boot 所扫描到。 -
@RibbonClients
注解,通过defaultConfiguration
属性声明 Ribbon 全局级别的自定义配置,通过value
属性声明多个 Ribbon 客户端级别的自定义配置。 -
@RibbonClient
注解,声明一个 Ribbon 客户端级别的自定义配置,其中name
属性用于设置 Ribbon 客户端的名字。 -
DefaultRibbonClientConfiguration 和 UserProviderRibbonClientConfiguration 都创建了 IRule Bean,而 DefaultRibbonClientConfiguration 是在 Spring 父上下文生效,会和 UserProviderRibbonClientConfiguration 所在的 Spring 子上下文共享。这样就导致从 Spring 获取 IRule Bean 时,存在两个而不知道选择哪一个。因此,我们声明 UserProviderRibbonClientConfiguration 创建的 IRule Bean 为
@Primary
,优先使用它。
实践建议
- 对于 Ribbon 客户端级别的自定义配置,推荐使用配置文件的方式,简单方便好管理。在配置文件的方式无法满足的情况下,使用 Spring JavaConfig 的方式作为补充。不过绝大多数场景下,都基本不需要哈~
- 对于 Ribbon 全局级别的自定义配置,暂时只能使用 Spring JavaConfig 的方式
- 配置文件方式的优先级高于 Spring JavaConfig 方式,客户端级别的优先级高于全局级别
Nacos 自定义负载均衡规则
Spring Cloud Alibaba Nacos Discovery 组件,在和 Ribbon 集成时,提供了自定义负载均衡规则 NacosRule。规则如下:
- 第一步,获得健康的方服务实例列表
- 第二步,优先选择相同 Nacos 集群的服务实例列表,保证高性能。如果选择不到,则允许使用其它 Nacos 集群的服务实例列表,保证高可用
- 第三步,从服务实例列表按照权重进行随机,选择一个服务实例返回
NacosRule 源码如下:
public class NacosRule extends AbstractLoadBalancerRule { // 继承 AbstractLoadBalancerRule 抽象类
private static final Logger LOGGER = LoggerFactory.getLogger(NacosRule.class);
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public Server choose(Object key) {
try {
String clusterName = this.nacosDiscoveryProperties.getClusterName();
DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
String name = loadBalancer.getName();
// 第一步,获得健康的方服务实例列表
NamingService namingService = nacosDiscoveryProperties
.namingServiceInstance();
List<Instance> instances = namingService.selectInstances(name, true);
if (CollectionUtils.isEmpty(instances)) {
LOGGER.warn("no instance in service {}", name);
return null;
}
// 第二步,优先获得相同集群的服务实例
List<Instance> instancesToChoose = instances;
if (StringUtils.isNotBlank(clusterName)) {
// 优先选择相同 Nacos 集群的服务实例列表,保证高性能
List<Instance> sameClusterInstances = instances.stream()
.filter(instance -> Objects.equals(clusterName,
instance.getClusterName()))
.collect(Collectors.toList());
if (!CollectionUtils.isEmpty(sameClusterInstances)) {
instancesToChoose = sameClusterInstances;
// 如果选择不到,则允许使用其它 Nacos 集群的服务实例列表,保证高可用
} else {
LOGGER.warn("A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}", name, clusterName, instances);
}
}
// 第三步,从服务实例列表按照权重进行随机,选择一个服务实例返回
Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToChoose);
// 返回
return new NacosServer(instance);
} catch (Exception e) {
LOGGER.warn("NacosRule error", e);
return null;
}
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
}
修改 application.yaml
配置文件,额外添加如下配置:
demo-provider:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
- 通过
demo-provider.ribbon.NFLoadBalancerRuleClassName
配置项,设置名字为user-provider
的 Ribbon 客户端的负载均衡规则为 Nacos 自定义负载均衡规则 NacosRule。 - 通过配置文件中的
spring.cloud.nacos.discovery.weight
实现服务实例的权重
饥饿加载
默认配置下,Ribbon 客户端是在首次请求服务时,才创建该服务的对应的 Ribbon 客户端。
好处是项目在启动的时候,能够更加快速,因为 Ribbon 客户端创建时,需要从注册中心获取服务的实例列表,需要有网络请求的消耗。
坏处是首次请求服务时,因为需要 Ribbon 客户端的创建,会导致请求比较慢,严重情况下会导致请求超时。
因此,Spring Cloud Netflix Ribbon 提供了 ribbon.eager-load
配置项,允许我们在项目启动时,提前创建 Ribbon 客户端。翻译成中文就是**“饥饿加载”**。
ribbon:
# Ribbon 饥饿加载配置项,对应 RibbonEagerLoadProperties 配置类
eager-load:
enabled: true # 是否开启饥饿加载。默认为 false 不开启
clients: user-provider # 开启饥饿加载的 Ribbon 客户端名字。如果有多个,使用 , 逗号分隔。
-
在本地开发环境时,可能会频繁重启项目,为了项目启动更快,可以考虑关闭 Ribbon 饥饿加载。
-
在生产环境下,一定要开启 Ribbon 饥饿加载。
HTTP 客户端
在上述的示例中,Ribbon 只负责服务实例的选择,提供负载均衡的功能,而服务的 HTTP 调用则是交给 RestTemplate 来完成。实际上,Ribbon 也是提供 HTTP 调用功能的。
在 Spring Cloud Netflix Ribbon 中,提供了 3 种 HTTP 客户端。配置方式如下:
#ribbon:
# okhttp:
# enabled: true # 设置使用 OkHttp,对应 OkHttpRibbonConfiguration 配置类
# restclient:
# enabled: true # 设置使用 Jersey Client,对应 RestClientRibbonConfiguration 配置类
# httpclient:
# enabled: true # 设置使用 Apache HttpClient,对应 HttpClientRibbonConfiguration 配置类
整理表格如下:
配置项 | HTTP 组件 | 配置类 |
---|---|---|
ribbon.okhttp.enable | OkHttp | OkHttpRibbonConfiguration |
ribbon.restclient.enable | Jersey Client | RestClientRibbonConfiguration |
ribbon.httpclient.enable | Apache HttpClient | HttpClientRibbonConfiguration |
请求重试
一般情况下,我们在 HTTP 请求远程服务时,都能够正常返回。但是极端情况下,可能会存在请求失败的情况下,例如说:
- 请求的服务执行逻辑过久,导致超过请求的等待时间
- 请求的服务异常挂掉了,未从注册中心中移除,导致服务消费者还是会调用该服务
- 网络一个抖动,导致请求失败
此时,我们通过重试请求到当前服务实例或者其它服务实例,以获得请求的结果,实现更高的可用性。
在 Spring Cloud 中,提供 spring.cloud.loadbalancer.retry
配置项,通过设置为 true
,开启负载均衡的重试功能。
在项目中添加如下配置:
ribbon:
restclient:
enabled: true # 设置使用 Jersey Client,对应 RestClientRibbonConfiguration 配置类
demo-provider:
ribbon:
ConnectTimeout: 1000 # 请求的连接超时时间,单位:毫秒。默认为 1000
ReadTimeout: 1 # 请求的读取超时时间,单位:毫秒。默认为 1000
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试,默认为 false。
MaxAutoRetries: 0 # 对当前服务的重试次数,默认为 0 次。
MaxAutoRetriesNextServer: 1 # 重新选择服务实例的次数,默认为 1 次。注意,不包含第 1 次哈。
① 设置 ribbon.restclient.enable
配置项为 true
,因为我们通过 Ribbon 实现请求重试,所以需要使用 Ribbon 内置的 HTTP 客户端进行请求服务。
② ConnectTimeout
和 ReadTimeout
两个配置项,用于设置请求的连接和读取超时时间。
③ OkToRetryOnAllOperations
、MaxAutoRetries
、MaxAutoRetriesNextServer
三个配置项,设置请求重试相关的参数。
MaxAutoRetries
和 MaxAutoRetriesNextServer
两个配置项可能略微难以理解,艿艿再简单描述下。
- 第一步,在使用 Ribbon 选择一个服务实例后,如果请求失败,重试
MaxAutoRetries
请求直到成功。 - 第二步,如果经过
1 + MaxAutoRetries
次,请求一个服务实例还是失败,重新使用 Ribbon 选择一个新的服务实例,重复第一步的过程。最多可以重新选择MaxAutoRetriesNextServer
次新的服务实例。
也就是说,在服务实例足够的情况下,最多会发起 (1 + MaxAutoRetries) * (1 + MaxAutoRetriesNextServer)
请求。不过一般情况下,推荐设置 MaxAutoRetries
为 0,不重试当前实例。
在 ② 和 ③ 中的参数,如果想要全局级别的配置,可以添加到 ribbon
配置项下。例如说:
ribbon:
ConnectTimeout: 1000 # 请求的连接超时时间,单位:毫秒。默认为 1000
ReadTimeout: 1 # 请求的读取超时时间,单位:毫秒。默认为 1000
OkToRetryOnAllOperations: true # 是否对所有操作都进行重试,默认为 false。
MaxAutoRetries: 0 # 对当前服务的重试次数,默认为 0 次。
MaxAutoRetriesNextServer: 1 # 重新选择服务实例的次数,默认为 1 次。注意,不包含第 1 次哈。