SpringCloud Netflix负载均衡 Ribbon讲解

概述

Ribbon 是一个进程间通信的客户端库,并且已经在云环境下经过大量的生产测试。它提供了以下功能:

  • 负载均衡
  • 容错机制
  • 支持 HTTP、TCP、UDP 等多种协议,并支持异步和响应式的调用方式
  • 缓存与批处理

负载均衡实现方式

为了保证服务的可用性,我们会给每个服务部署多个实例,避免因为单个实例挂掉之后,导致整个服务不可用。并且,因为每个服务部署了多个实例,也提升了服务的承载能力,可以同时处理更多的请求。

不过此时需要考虑到需要将请求均衡合理的分配,保证每个服务实例的负载。一般来说,我们有两种方式实现服务的负载均衡,分别是客户端级别服务端级别

服务端级别的负载均衡,客户端通过外部的代理服务器,将请求转发到后端的多个服务。比较常见的有 Nginx 服务器,如下图所示:

服务端级别

客户端级别的负载均衡,客户端通过内嵌的“代理”,将请求转发到后端的多个服务。比较常见的有 Dubbo、Ribbon 框架提供的负载均衡功能,如下图所示:

客户端级别

相比来说,客户端级别的负载均衡可以有更好的性能,因为不需要多经过一层代理服务器。并且,服务端级别的负载均衡需要额外考虑代理服务的高可用,以及请求量较大时的负载压力。因此,在微服务场景下,一般采用客户端级别的负载均衡为主。

对于客户端的负载均衡来说,最好搭配注册中心一起使用。这样,服务实例的启动和关闭,可以向注册中心发起注册和取消注册,保证能够动态的通知到客户端。

本文,我们会使用 Ribbon 提供客户端级别的负载均衡,使用 Nacos 作为注册中心。整体架构图如下:

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 即可,并使用它发起请求。
  • /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 规则类似。

不同框架或者组件,会采用不同的负载均衡规则,感兴趣的胖友可以阅读如下文章:

总的来说,经典的负载均衡规则可以整理如下:

  • 轮询(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();
    }
}
  • 对于 DefaultRibbonClientConfigurationUserProviderRibbonClientConfiguration 两个配置类,我们并没有和 DemoConsumerApplication 启动类放在一个包路径下。

    因为,Spring Boot 项目默认扫描 DemoConsumerApplication 所在包以及子包下的所有 Bean 们。而 @Configuration 注解也是一种 Bean,也会被扫描到。如果将 DefaultRibbonClientConfiguration 和 UserProviderRibbonClientConfiguration 放在 DemoConsumerApplication 所在包或子包中,将会被 Spring Boot 所扫描到,导致整个项目的 Ribbon 客户端都使用相同的 Ribbon 配置,无论是否在@RibbonClients注解中指定了不同的配置。那么这样就无法到达 Ribbon 客户端级别的自定义配置的目的

    因此,这里在根路径下又创建了 ribbon 包,并将 DefaultRibbonClientConfigurationUserProviderRibbonClientConfiguration 放入其中,避免被 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.enableOkHttpOkHttpRibbonConfiguration
ribbon.restclient.enableJersey ClientRestClientRibbonConfiguration
ribbon.httpclient.enableApache HttpClientHttpClientRibbonConfiguration

请求重试

一般情况下,我们在 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 客户端进行请求服务。

ConnectTimeoutReadTimeout 两个配置项,用于设置请求的连接和读取超时时间。

OkToRetryOnAllOperationsMaxAutoRetriesMaxAutoRetriesNextServer 三个配置项,设置请求重试相关的参数。

MaxAutoRetriesMaxAutoRetriesNextServer 两个配置项可能略微难以理解,艿艿再简单描述下。

  • 第一步,在使用 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 次哈。

Ribbon 主要组件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值