(三)客户端负载均衡:Spring Cloud Ribbon--笔记

Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,而已让我们将面向服务的REST模板请求自动转换成客户端负载均衡的服务调用。它只是一个工具类框架,无需独立部署,但几乎存在于每一个Spring Cloud构建的微服务和基础设施中。微服务建的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括之后的Feign。

客户端负载均衡

客户端负载均衡和服务端负载均衡最大的不同点在于服务清单所存储的位置。客户端负载均衡中,所有尅兑换节点都要维护自己要访问的服务端清单,这些服务端的清单来自于服务注册中心,比如Eureka服务端。客户端负载均衡中心也需要心跳去维护服务端清单的健康,这个步骤需要与服务注册中心配合完成。在Spring Cloud实现的服务治理框架中,默认会创建针对各个服务治理框架的Ribbon自动化整合配置,比如Eureka中的org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,Consul中的org.springframework.cloud.consul.discovery.RibbonConsulAutoConfiguration。在实际使用的时候,我们通过查看这两个类的实现,已找到它们的配置详情来帮助我们更好地使用它。

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

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

这样就可以将服务提供者的高可用以及服务消费者的负载均衡调用一起实现了。

RestTemplate详解

RestTemplate对象会使用Ribbon的自动化配置,同时通过配置@LoadBalanced还能够开启客户端负载均衡。

GET请求

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

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

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

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

若我们希望返回的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(URI url,Class responseType);

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

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

当不需要关注请求响应除body外的其他内容时,该函数就非常好用,可以少一个从Response中获取body的步骤。它与getForEntity函数类似,也提供了三种不同的重载实现:

getForObject(String url,Class responseType,Object... urlVariables);

getForObject(String url,Class responseType,Map urlVariables);

getForObject(URI url,Class responseType);
POST请求

对POST请求可以通过三个方法进行调用。

第一种:postForEntity函数。

第二种:postForObject函数。

第三种:postForLocation函数。

除了GET和POST还实现了PUT和DELETE。

源码分析

分析下Ribbon是如何通过RestTemplate实现客户端负载均衡的。

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

LoadBalancerClient是Spring Cloud定义的一个接口:

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,从负载均衡器中挑选一个对应服务的实例。
  • T execute(String serviceId,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形式的请求地址。

顺着LoadBalancerClient接口所属包org.springframework.cloud.client.loadbalancer,我们对其内容进行整理,可以多出如下关系:

这里写图片描述

从类名可以初步判断LoadBalancerAutoConfiguration为实现客户端负载均衡器的自动化配置类。

@Configuration
@ConditionOnClass(RestTemplate.class)
@ConditionOnBean(LoadBalanceClient.class)
public class LoadBalancerAutoConfiguration{

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

    @Bean
    public SmartInitializingSingleton loadBalancedRestTemplateInitializer(final List<RestTemplateCustomizer> customizers){
        return new SmartInitializingSingleton(){

            @Override
            public void afterSingletonsInstantiated(){
                for(RestTemplate restTemplate : LoadBalancerAutoConfigutration.this.restTemplates){
                    for(RestTemplateCustomer customer : customizers){
                        customizer.customize(restTemplate);
                    }   
                }
            }
        }
    }

    @Bean
    @ConditionalOnMissingBean
    public RestTemplateCustomizer restTemplateCustomizer(
        final LoadBalancerInterceptor loadBalancerInterceptor){
        @Override
        public void customize(RestTemplate restTemplate){
            List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());
            restTemplate.setInterceptors(list);
        }
    }

    @Bean
    public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient){
        return new LoadBalancerInterceptor(loadBalancerClient);

    }
}

从LoadBalancerAutoCofiguration类头上的注解可以知道,Ribbon实现的负载均衡自动化配置需要满足下面两个条件:

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

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

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

接下来看下LoadBalancerInterceptor拦截器时如何将一个普通的RestTemplate变成客户端负载均衡的:

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor{
    private LoadBalancerClient loadBalancer;

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer){
        this.loadBalancer = loadBalancer;
    }

    @Override
    public ClientHttpResponse intercept(final HttpRequest request,final byte[] body,final ClientHttpRequestExecution execution) throws IOException{
    final URI originalUri = request.getURI();
    String serviceName = originalUri.getHost();
    return this.loadBalancer.execute(serviceName,new LoadBalancerRequest<ClientHttpResponse>(){
            @Override
            public ClientHttpResponse apply(final ServiceInstance instance) throws Exception{
                HttpRequest serviceRequest = new ServiceRequestWSrapper(request,instance);
                return execution.execute(serviceRequest,body);
            }
        });
    }

    private class ServiceRequestWrapper extends HttpRequestWrapper{
        private final ServiceInstance instance;

        public ServiceRequestWrapper(HttpRequest,request,ServiceInstance instance){
            super(request);
            this.instance = instance;
        }

        @Override
        public URI getURI(){
            URI uri = LoadBalancerInterceptor.this.loadBalancer.reconstructURI(this.instance,getRequest().getURI());
            return uri;
        }
    }
}

通过源码以及之前的自动化配置类,我们可以看到在拦截器中注入了LoadBalancerClient的实现。当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会被LoadBalancerInterceptor类的intercept函数所拦截。由于我们在使用RestTemplate时采用了服务名作为host,所以直接从HttpRequest的URI对象中通过getHost()就可以拿到服务名,然后调用execute函数去根据服务名来选择实例并发起实际的请求。

到这里,LoadBalancerClient还只是一个抽象的负载均衡器接口,所以我们还需要找到它的具体类来进一步进行分析。通过查看Ribbon的源码,可以很容易在org.springframework.cloud.netflix.ribbon包下找到对应的实现类RibbonLoadBalancerClient。

public <T> T execute(String serviceId,LoadBalancerRequest<T> request) throws IOException{
    ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
    Server server = getServer(loadBalancer);
    if(server == null){
        throw new IllegalStateException("No instances available for "+serviceId);
    }
    RibbonServer ribbonServer = new RibbonServer(serviceId,server,isSecure(server,serviceId),serverIntrospector(serviceId).getMetadata(server));
    RibbonLoadBalanceContext context = this.clientFactory.getLoadBalancerContext(serviceId);
    RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context,server);

    try{
        T returnVal = request.apply(ribbonServer);
        statsRecorder.recordStats(returnVal);
        return returnVal;
    }catch(IOException ex){
        statsRecorder.recordStats(ex);
        throw ex;
    }catch(Exception ex){
        statsRecorder.recordStats(ex);
        ReflectionUtils.rethrowRuntimeException(ex);
    }
    return null;
}

在execute函数实现中,第一步做的就是getServer根据传入的服务名serviceId去获取具体的服务实例:

protected Server getServer(ILoadBalancer loadBalancer){
    if(loadBalancer == null){
        return null;
    }
    return loadBalancer.chooseServer("default");
}

通过getServer的实现,可以看到这里获取具体服务实例的时候并没有使用LoadBalancerClient接口中的choose函数,而是使用了Netfilx Ribbon自身的ILoadBalancer接口中定义的chooseServer函数。

ILoadBalancer接口:

public interface ILoadBalancer{
    public void addServers(List<Server> new Servers);
    public Server chooseServer(Object key);
    public void markServerDown(Server server);
    public List<Server> getReachableServers();
    public List<Server> getAllServers();
}

可以看到,在该接口中定义了一个客户端负载均衡器需要的一系列抽象操作:

  • addServers: 向负载均衡器中维护的实例列表增加服务实例。
  • chooseServer:通过某种策略,从负载均衡器中挑选出一个具体的服务实例。
  • markServerDown:用来通知和标识负载均衡器中某个具体实例已经停止服务,不然负载均衡器在下一次获取服务实例清单前都会认为服务实例均是正常的。
  • getReachableServers:获取当前正常服务的实例列表;
  • getAllServers:获取所有已知的服务实例列表,包括正常服务和停止服务的实例。

在该接口定义中涉及的Server对象定义是一个传统的服务端节点,在该类中存储了服务端节点的一些元数据信息,包括host、port以及一节部署信息等。

对于该接口的实现,可以看到BaseLoadBalancer类实现了基础的负载均衡,而DynamicServerListLoadBalancer和ZoneAwareLoadBalancer在负载均衡的策略上做出了一些功能的扩展。

这里写图片描述

在整合Ribbon的时候Spring Cloud默认采用了那种具体实现呢?我们通过 RibbonClientConfiguration配置类,可以知道在整合时默认采用了ZoneAwareLoadBalancer来实现负载均衡器。

@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config,ServerList<Server> serverList,ServerListFilter<Server> serverlistFilter,IRule rule,IPing ping){
    ZoneAwareLoadBalancer<Server> balancer = LoadBalancerBuilder.newBuilder().withClientConfig(config).withRule(rule).withPing(ping).withServerListFilter(serverListFilter).withDynamicServerList(serverList).buildDynamicServerListLoadBalancer();
    return balancer;
}

再回到RibbonLoadBalancerClient的execute函数逻辑,在通过ZoneAwareLoadBalancer的chooseServer函数获取了负载均衡策略分配到的服务实例对象Server之后,将其内容包装成RibbonServer对象(该对象除了存储了服务实例的信息之外,还增加了服务名serverId、是否需要使用HTTPS等其他信息),然后使用该对象再回调LoadBalancerInterceptor请求拦截器中LoadBalancerRequest的apply(final ServiceInstance instance)函数,向一个实际的具体服务实例发起请求,从而实现一开始以服务名为host的URI请求到host:port形式的实际方法地址的转换。

在apply(final ServiceInstance instance)函数中传入的ServiceInstance接口对象是对服务实例的抽象定义。在该接口中暴露了服务治理系统中每个服务实例需要提供的一些基本信息,比如serviceId、host、port等,:

public interface ServiceInstance{
    String getServiceId();
    String getHost();
    int getPort();
    boolean isSecure();
    URI getUri();
    Map<String,String> getMetadata();
}

上面提到的具体包装Server服务实例的RibbonServer对象就是ServieInstance接口的实现,可以看到它除了包含Server对象之外,还存储了服务名、是否使用HTTPS标识以及一个Map类型的元数据集合。

protected static class RibbonServer implements ServiceInstance{
    private final String serviceId;
    private final Server server;
    private final boolean secure;
    private Map<String,String> metadata;

    protected RibbonServer(String serviceId,Server server){
        this(serviceId,server,false,Collection.<String,String>emptyMap());
    }

    protected RibbonServer(String serviceId,Server server,boolean secure,Map<String,String> metadata){
        this.serviceId = serviceId;
        this.server = server;
        this.secure = secure;
        this.metadata = metadata;
    }
    ......
}

那么apply(final ServiceInstance instance)函数在接收到了具体ServiceInstance实例后,是如何通过LoadBalancerClient接口中的reconstructURI操作来组织具体请求地址呢?

@Override
public ClientHttpResponse apply() throws Exception{
    HttpRequest serviceRequest = new ServiceRequestWrapper(rquest,instance);
    return execution.execute(serviceRequest,body);
}

从apply的实现中,可以看到它具体执行的时候,还传入了ServiceRequestWrapper对象,该对象继承了HttpRequestWrapper并重写了getURI函数,重写后的getURI通过调用LoadBalancerClient接口的reconstructURI函数来重新构建一个URI来进行访问。

private class ServiceRequestWrapper extends HttpRequestWrapper{
    private final ServiceInstance instance;
    ...

    public URI getURI(){
        URI uri = LoadBalancerInterceptor.this.loadBalancer.reconstructURI(this.instance,getRequest().getURI());
        return uri;
    }

}

在LoadBalancerInterceptor拦截器中,ClientHttpRequestExecution的实例具体执行execution.execute(serviceRequest,body)时,会调用InterceptingClientHttpRequest下InterceptingRequestExecution类的execute函数:

public ClientHttpResponse execute(HttpRequest request,byte[] body) throws IOException{
    if(this.iterator.hasNext()){
        ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
        return nextInterceptor.intercept(request,body,this);
    }else{
        ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(),request.getMethod());
        delegate.getHeaders().putAll(request.getHeaders());
        if(body.length>0){
            StreamUtils.copy(body,delegate.getBody());
        }
        return delegate.execute();
    }
}

可以看到,在创建请求的时候requestFactory.createRequest(request.getURI(),request.getMethod());这里的的request.getURI()会调用之前介绍的ServiceRequestWrapper对象中重写的getURI函数。此时,它就会使用RibbonLoadBalancerClent中实现的reconstructURI来组织具体请求的服务实例地址。

public URI reconstructURI(ServiceInstace instance,URI original){
    String serviceId = instance.getServiceId();
    RibbonLoadBalancerContext context  = this.clientFactory.getLoadBalancerContext(serviceId);
    Server server = new Server(instance.getHost(),instance.getPort());
    boolean secure = isSecure(server,serviceId);
    URI uri = original;
    if(secure){
        uri = UriConponentsBuilder.fromUri(uri).sheme("https").bulid().toUri();
    }
    return context.reconstructURIWithServer(server,uri);

}

从reconstructURI函数中可以看到,它通过ServiceInstance实例对象的serviceId,从SpringClientFactory类的clientFactory对象中获取对应serviceId的负载均衡器的上下文RibbonLoadBalancerContext对象。然后根据SrviceInstance中的信息来构建具体服务实例信息的Server对象,并使用RibbonLoadBalancerContext对象的reconstructURIWithServer函数来构建服务实例的URI。

为了帮助理解,简单介绍下上面提到的SpringClientFactory和RibbonLoadBalancerContext。

  • SpringClientFactory类是一个用来创建客户端负载均衡器的工厂类,该工厂类会为每一个不同名的Ribbon客户端生成不同的Spring上下文。
  • RibbonLoadBalanceContext类时LoadBalancerContext的子类,该类用于存储一些被负载均衡器使用的上下文内容和API操作(reconstructURIWithServer就是其中之一)。

从reconstructURIWithServer的实现中我们可以看到,它通reconstructURI的定义类似。只是reconstructURI的第一个保存具体服务实例的参数使用了Spring Cloud定义的ServiceInstance,而reconstructURIWithServer中使用了Netflix中定义的Server,所以在RibbonLoadBalancerClient实现reconstructURI的时候,做了一次转换,使用ServiceInstance的host和port信息构建了一个Server对象来给reconstructURIWithServer使用。从reconstructURIWithServer的实现逻辑中,可以看到,它从Server对象中获取host和port信息,然后根据以服务名为host的URI对象original中获取其他请求信息,将两者内容进行拼接整合,形成最终要访问的服务实例的具体地址。

另外,从RibbonLoadBalancerClient的execute函数逻辑中,我们还能看到在回调拦截器中,执行具体的请求之后,Ribbon还通过RibbonStatsRecorder对象对服务的请求进行了跟踪记录。

分析到这,已经可以大致理清楚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

AbstractLoadBalancer是ILoadBalancer接口的抽象实现。在该抽象类中定义了一个关于服务实例的分组枚举类ServerGroup,它包含三种不同类型。

  • ALL :所有服务实例。
  • SATUS_UP:正常服务的实例。
  • SATUS_NOT_UP:停止服务的实例。

另外,还实现了一个chooseServer()函数,该函数通过调用接口中的chooseServer(Object key)实现,其中参数key为null,表示在选择具体服务实例时忽略key的条件判断。
最后,还定义了两个抽象函数。

  • getServerList(ServerGroup serverGroup):定义了根据分组类型来获取不同的服务实例的列表。
  • getLoadBalancerStats对象呗永磊存储负载均衡器中各个服务实例当前的属性和统计信息。
public abstract class AbstractLoadBalancer implements ILoadBalancer{
    public enum ServerGroup{
        ALL,STATUS_UP,STATUS_NOT_UP
    }

    public Server chooseServer(){
        return chooseServer(null);
    }

    public abstract List<Server> getServerList(ServerGroup serverGroup);

    public abstract LoadBalancerStats getLoadBalanverStats();
}

BaseLoadBalancer

BaseLoadBalancer类是Ribbon负载均衡器的基础实现类,在该类中定义了很多关于负载均衡器相关的基础内容。

  • 定义并维护了两个存储服务实例Server对象的列表。一个用户存储所有服务实例的清单,一个用户存储正常服务的实例清单。
@Monitor(name=PREFIX + "AllServerList",type=DataSourceType.INFORMATIONAL)
protected volatile List<Server> allServerList = Collections.synchronizedList(new ArrayList<Server>());
@Monitor(name=PREFIX + "UpServerList",type=DataSurceType.INFORMATIONAL)
protected volatile List<Server> upServerList = Collections.synchronizedList(new ArrayList<Server>());
  • 定义了之前我们提到的用来存储负载均衡器各服务实例属性和统计信息的LoadBalancerStats对象。
  • 定义了检查服务实例是否正常服务的IPing对象,在BaseLoadBalancer中默认为null,需要在构造时注入它的具体实现。
  • 定义了检查服务实例操作的执行策略对象IPingStrategy,在BaseLoadBalancer中默认使用了该类中定义的静态内部类SerialPingStrategy实现。
  • 定义了负载均衡的处理规则IRule对象。从BaseLoadBalancer中chooseServer(Object key)的实现源码,可以知道,负载均衡实际将服务实例选择人物委托给了IRule实例中的choose函数来实现。在这里,默认初始化了RoundRobinRule为IRule的实现对象。RouleRobinRule实现了最基本且常用的线性负载均衡规则。
  • 启动ping任务。
  • 实现了ILoadBalancer接口定义的负载均衡器应具备的一系列操作。

DynamicServerListLoadBalancer

DynamicServerListLoadBalancer类继承于BaseLoadBalancer类,它是对基础负载均衡器的扩展。在该负载均衡器中,实现了服务清单在运行期的动态更新功能;同时,它还具备了对服务实例清单的过滤功能,也就是说,我们可以通过过滤器来选择性获取一批服务实例清单。

负载均衡策略

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

这里写图片描述

配置详解

自动化配置

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

  • IClientConfig:Ribbon的客户端配置,默认采用com.netflix.client.config.DefaultClientConfigImpl实现。
  • IRule:Ribbon的负载均衡策略,默认采用com.netflix.loadbalancer.ZoneAvoidanceRule实,该策略能够在多区域环境下选出最佳区域的实例进行访问。
  • IPing:Ribbon的实例检查策略,默认采用com.netflix.loadbalancer.NoOpPing实现,该检查策略是一个特殊的实现,实际上它并不会检查实例是否可用,而是始终返回true,默认认为所有服务实例都是可用的。
  • ServerList:服务实例清单的维护机制,默认采用com.netflix.loadbalancer.ConfigurationBasedServerList实现。
  • ServerListFilter:服务实例清单过滤机制,默认采用org.springframework.cloud.netflix.ribbon.ZonePrefenceServerListFilter实现,该策略能够优先过滤出与请求调用方处于同区域的服务实例。
  • ILoadBalancer:负载均衡器,默认采用com.netflix.loadbalancer.ZoneAwareLoadBalancer实现,它具备了区域感知的能力。

  • 上面这些自动化配置内容仅在没有引入Spring Cloud Enreka等服务治理框架时如此,在同时引入Eureka和Ribbon依赖时,自动化配置会有一些不同。

通过自动化配置的实现,可以轻松地实现客户端负载均衡。同时,针对一些个性化需求,也可以方面替换上面的默认实现。只需要在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.<key>=<value>格式进行配置即可。
    其中,代表了Ribbon客户端配置的参数名,则代表了对应参数的值。比如。
    我们可以向下面这样全局配置Ribbon创建连接的超时时间:
ribbon.ConnetcTimeout=250
  • 指定客户端的配置方式采用<client>.ribbon.<key>=<value>的格式进行配置。其中,和的含义同全局配置相同,而代表了客户端的名称,如上文中我们在@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服务实例的维护实现。只需要在配置中加入日下参数,这时我们对于服务实例的维护就又回到了<client>.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配置),如果依然不行,返回失败信息。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值