SpringCloud学习笔记(4)—— 客户端负载均衡:Spring Cloud Ribbon

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

客户端负载均衡

  • 硬件负载均衡:主要通过服务器节点之间安装专门用于负载均衡的设备,比如F5
  • 软件负载均衡:通过在服务器上安装一些具有均衡负载功能或模块的软件来完成请求分发工作,比如Nginx

客户端负载均衡和服务端负载均衡最大的不同点在于服务清单所存储的位置。客户端负载均衡中,所有客户端节点都要维护自己要访问的服务端清单,这些服务端的清单来自于服务注册中心,比如Eureka服务端。客户端负载均衡中心也需要心跳去维护服务端清单的健康,这个步骤需要与服务注册中心配合完成。

通过Spring Cloud Ribbon的封装,我们在微服务架构中使用客户端负载均衡调用:

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

RestTemplate 详解

之前我们演示了通过RestTemplate实现了最简单的服务访问,下面我们将详细介绍RestTemplate针对几种不同请求类型和参数类型的服务调用实现。

1.GET 请求

可以通过如下两个方法进行调用实现。

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

RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://USER-SERVICE/user?name={1}",String.class,"didi";
String body = responseEntity.getBody();

例如上面的例子,访问服务/user请求,最后一个参数didi会替换url中的{1}占位符,而返回的ResponseEntity对象中的body内容类型会根据第二个参数转换为String类型。
若我们希望返回的body是一个User对象类型,也可以:

RestTemplate restTemplate = new RestTemplate();
ResponseEntity<User> responseEntity = restTemplate.getForEntity("http://USER-SERVICE/user?name={1}",User.class,"didi");
User body = responseEntity.getBody();

getForEntity函数实际上提供了以下三种不同的重载形式.

  • getForEntity(String url,Class responseType,Object…urlVariables)
  • getForEntity(String url,Class responseType,Map urlVariables)
  • getForEntity(RUI url,Class responseType);

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

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

它也提供了三种不同的重载方法:

  • getForObject(String url,Class responseType,Object… urlVariables);
  • getForObject(String url,Class responseType,Map urlVariables);
  • getForObject(URI url,Class responseType);
2.POST、PUT、DELETE 类似GET请求。

源码分析

我们将探索下Ribbon是如何通过RestTemplate实现客户端负载均衡的。

查看@LoadBalanced的注解源码,里面写道:

/**
 * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
 * @author Spencer Gibb
 */

可知,该注解用来给RestTemplate做标记,以使用负载均衡的客户端(LoadBalancerClient)来配置它。

public interface LoadBalnacerClient{
    ServiceInstance choose(String serviceId);

    <T> T execute(String serviceId,LoadBalancerRequest<T> request) throws IOException;

    URI reconstructURI(ServiceInstance instance,URI original);
}

  • ServiceInstance choose(String serviceId): 根据传入的服务名serviceId,从负载均衡器中挑选一个对应服务的实例。
  • Texecute(StringserviceId,LoadBalancerRequest request) throws IOException:使用从负载均衡器中挑选出的服务实例来执行请求内容。
  • URI reconstructURI(ServiceInstance instance,URI original):为系统构建一个合适的host:port形式的URI。在分布式系统中,我们使用逻辑上的服务名称作为host来构建URI(替代服务实例的host:port形式)进行请求,比如http://myservice/path/to/service。在该操作的定义中,前者ServiceInstance对象是带有host和port的具体服务实例,而后者URI对象则是使用逻辑服务名定义为host的URI,而返回的URI内容则是通过ServiceInstance的服务实例详情拼接出的具体host:port形式的请求地址。

顺着LoadBanlancerClient 接口的所属包org.springframework.cloud.client.loadbalancer跟踪可以判断到LoadBalancerAutoConfiguration为实现客户端负载均衡器的自动化配置类,从LoadBalancerAutoCofiguration类头上的注解可以知道,Ribbon实现的负载均衡自动化配置需要满足下面两个条件:

  • @ConditionalOnClass(RestTemplate.class):RestTemplate类必须存在于当前工程的环境中。
  • @ConditionalOnBean(LoadBalancerClient.class):在Spring的Bean工程中必须有LoadBalancerClient的实现Bean。

在该自动化配置类中,主要做了三件事:

  • 创建了一个LoadBalancerInterceptor的Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。
  • 创建了一个RestTemplateCustomizer的Bean,用于给RestTemplate增加LoadBalancerInterceptor拦截器。
  • 维护了一个被@LoadBalanced注解修饰的RestTemplate对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加LoadBalancerInterceptor拦截器。

继续跟踪源码可以大致理清楚Spring Cloud Ribbon中实现客户端负载均衡的基本脉络,了解它是如何通过LoadBalancerInterceptor拦截器对RestTemplate的请求进行拦截,并利用Spring Cloud的负载均衡器LoadBalancerClient将以逻辑服务名为host的URI转换成具体的服务实例地址的过程。同时通过分析LoadBalancerClient的Ribbon实现RibbonLoadBalancerClient,可以知道在使用Ribbon实现负载均衡器的时候,实际使用的还是Ribbon中定义的ILoadBalancer接口的实现,自动化配置会采用ZoneAwareLoadBalancer的实例来实现客户端负载均衡。

负载均衡器

虽然Spring Cloud 中定义了LoadBalancerClient作为负载均衡器的通用接口,并且针对Ribbon实现了RibbonLoadBalancerClient,但是它在具体体实现客户端负载均衡时,是通过Ribbon的ILoadBalancer接口实现的。下面我们根据ILoadBalancer接口的实现类逐个看看它是如何实现负载均衡的。

  • AbstractLoadBalancer
  • BaseLoadBalancer
  • DynamicServerListLoadBalancer

###负载均衡策略
Ribbon中实现了非常多的选择策略(负载均衡中的服务实例选择策略),其中包括的RoundRobinRule和ZoneAvoidanceRule。

配置详解

1.自动化配置

由于Ribbon中定义的每一个接口都有多种不同的策略实现,同时这些接口之间又有一定的依赖关系,使得Ribbon开发者很难上手。Spring Cloud Ribbon中的自动化配置能够解决这种问题。就能自动化构架下面这些接口的实现。

  • IClientConfig:Ribbon的客户端配置
  • IRule:Ribbon的负载均衡策略
  • IPing:Ribbon的实例检查策略
  • ServerList:服务实例清单的维护机制
  • ServerListFilter:服务实例清单过滤机制
  • ILoadBalancer:负载均衡器

通过自动化配置的实现,可以轻松地实现客户端负载均衡。同时,针对一些个性化需求,也可以方面替换上面的默认实现。只需要在Spring Boot应用中创建对应的实现实例就能覆盖这些默认的配置实现。比如下面,由于创建了PingUrl实例,所以默认的NoOpPing就不会被创建。

@Configuration
public class MyRibbonCconfiguration{
    @Bean
    public IPing ribbonPing(IClientConfig config){
        return new PingUrl();
    }
}

另外,也可以通过使用@RibbonClient注解来实现更细粒度的客户端配置,比如前面的代码实现了为hello-service服务使用HelloServiceConfiguration中的配置。

@Configuration
@RibbonClient(name="hello-service",configuration=HelloServiceConfiguration.class)
public class RibbonConfiguration{
}

Camden版本对RibbonClient配置的优化

上面介绍在Brixton版本中对RibbonClient的IPing、IRule等接口实现进行个性化定制的方法,主要通过独立创建一个Configuration类来定义IPing、IRule等接口的具体实现Bean,然后在创建RibbonClient时制定要使用的具体Configuration类来覆盖自动化配置的默认实现。虽然这种方式已经能够实现个性化的定义,但是当有大量这类配置的时候,对于各个RibbonClient的制定配置信息都将分散在这些配置类的注解定义中,这使得管理和修改都变得非常不方便。所以,在Camden版本中,Sping Cloud Ribbon对RibbonClient定义个性化配置的方法做了进一步优化。可以直接通过.ribbon.=的形式进行配置。别入我们要实现与上面例子一样的配置(将hello-service服务客户端的IPing接口实现替换为PingUrl),只需在application.preperties配置中增加如下内容:

hello-service.ribbon.NFLoadBalancePingClassName=com.netflix.loadbalancer.PingUrl

其中hello-service为服务名,NFLoadBalancerPingClassName参数用来置顶具体的IPIng接口实现类。

在Camden版本中我们可以通过配置的方式,更加方便地为RibbonClient指定ILoadBalancer、IPing、IRule、ServerList和ServerListFilter的定制化实现。

参数配置

对于Ribbon的参数配置通常有两种方式:全局配置以及指定客户端配置。

全局配置方式很简单,只需ribbon.=格式进行配置即可。
其中,代表了Ribbon客户端配置的参数名,则代表了对应参数的值。比如。
我们可以向下面这样全局配置Ribbon创建连接的超时时间:
ribbon.ConnetcTimeout=250

指定客户端的配置方式采用.ribbon.=的格式进行配置。其中,和的含义同全局配置相同,而代表了客户端的名称,如上文中我们在@RibbonClient中指定的名称,也可以将它理解为是一个服务名。为了方便理解这种配置方式,举一个例子:假设,有一个服务消费者通过RestTemplate来访问hello-service服务的/hello接口,这是会这样调用restTemplate.getForEntity(“http://hello-service/hello“,String.class).getBody();如果没有服务治理框架的帮助,我们需要为该客户端指定具体的实例清单,可以指定服务名来做详细的配置,
hello-service.ribbon.listOfServers=localhost:8001,localhost:8002,localhost:8003

与Eureka结合

当在Spring Cloud的应用中同时引入Spring Cloud Ribbon和Spring Cloud Eureka依赖时,会触发Eureka中实现的对Ribbon的自动化配置。这时ServerList的维护机制实现将被com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList的实例所覆盖,该实现会将服务清单列表交给Eureka的服务治理机制来进行维护;IPing的实现将被com.netflix.niws.loadbalancer.NIWSDiscoveryPing的实例所覆盖,该实现也将实例检查的任务交给了服务治理框架来进行维护。默认情况下,用于获取实例请求的ServerLsit接口实现将采用Spring Cloud Eureka中封装的org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList,其目的是为了让实例维护策略更加通用,所以将使用物理元数据来进行负载均衡,而不是使用原生的AWS AMI元数据。

由于Spring Cloud Ribbon默认实现了区域亲和策略,所以,可以通过Eureka实例的元数据配置来实现区域化的实例配置方案。比如,可以将处于不同机房的实例配置成不同区域值,以作为跨区域的容错机制实现。而实现的方式非常简单,只需要在服务实例的元数据中增加zone参数来指定自己所在的区域:

eureka.instance.metadataMap.zone=shanghai

在Spring Cloud Ribbon与Spring Cloud Enreka结合的工程中,我们也可以通过参数配置的方式来禁用Eureka对Ribbon服务实例的维护实现。只需要在配置中加入日下参数,这时我们对于服务实例的维护就又回到了.ribbon.listOfServers参数配置的方式来实现了。

ribbon.eureka.enabled=false

重试机制

由于Spring Cloud Eureka实现的服务治理机制强调了CAP原理中的AP,即可用性与可靠性,它与ZooKeeper这类强调CP(一致性、可靠性)的服务治理框架最大的区别就是,Eureka为了实现更高的服务可用性,牺牲了一定的一致性,在极端情况下它宁愿接受故障实例也不要丢掉“健康”实例,比如,当服务注册中心的网络繁盛故障断开时,由于所有的服务实例无法维持持续心跳,在强调AP的服务治理中会把所有服务实例都踢出掉,而Eureka则会因为超过85%的实例丢失心跳二回触发保护机制,注册中心江湖保留此时的所有节点,以实现服务间依然可以进行互相调用的场景,即使其中有部分故障节点,但这样做可以继续保障大多数服务正常消费。

由于Spring Cloud Eureka在可用性与一致性上的取舍,不论是由于出发了保护机制还是服务剔除的延迟,引起服务调用故障实例的时候,我们还是新网能能够增强对这类问题的容错。所以,我们在实现服务调用的时候通常会加入一些重试机制。从Camden SR2版本开始,Spring Cloud整合了Spring Retry来增强RestTemplate的重试能力,对于开发者来说只需通过简单的配置,原来那些通过RestTemplate实现的服务访问就会自动根据配置来实现重试策略。

以之前对hello-service服务的调用为例,可以在配置文件中增加如下内容:

spring.cloud.loadbalancer.retry.enabled=true
hystrix.command.default.execution.isolation.thread.timeoutInMillseconds=10000

hello-service.ribbon.ConnetTimeout=250
hello-service.ribbon.ReadTimeout=1000
hello-service.ribbion.OkToRetryOnAllOperations=true
hello-service.ribbon.MaxAuto.RetriesNextServer=2
hello-service.ribbon.MaxAutoRetries=1

说明:

spring.cloud.loadbalancer.retry.enabled:该参数用来开启重试机制,它默认是关闭的。
hystrix.command.default.execution.isolation.thread.timeoutInMillseconds:断路器的超时时间需要大于Ribbon的超时时间,不然不会触发重试。
hello-service.ribbon.ConnectTimeout:请求连接的超时时间。
hello-service.ribbon.ReadTimeout:请求处理的超时时间。
hello-service.ribbon.OkToRetryOnAllOperations:对所有操作请求都进行重试。
hello-service.ribbon.MaxAutoRetriesNextServer:切换实例的重试次数。
hello-service.ribbon.MaxAutoRetries:对当前实例的重试次数。
根据如上配置,当访问到故障请求的时候,它会载尝试访问一次当前实例次数(次数由MaxAutoRetries配置),如果不行,就换一个实例进行访问,如果还不行,再换一次实例访问(更换次数由MaxAutoRetriesNextServer配置),如果依然不行,返回失败信息。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值