Spring Cloud Eureka(服务注册与发现) + Ribbon(负载均衡器)

启动Spring 项目同时设置相关命令行参数

java -jar demo.jar --server.port=8889 --spring.profiles.active=test 

1.spring-boot-starter-actuator介绍

先设置 management.security.enabled=false   否则下面执行无效

1.1.应有配置类
应用配置类:获取应用程序中加载的应用、环境变量、自动化配置报告等于spring boot密切相关的配置类信息

/autoconfig 自动化配置 https://blog.csdn.net/xiaojia1100/article/details/52861553
/beans 获取应用上下文中创建的所有的Bean
/configprops 获取应用中配置的属性信息
/env 获取应用所有可用的环境属性报告。包括环境变量、JVM属性、应用的配置属性、命令行中的参数
/mappings 返回所有Spring MVC的控制器映射关系报告。
/info 返回一些应用自定义的信息,默认返回一个空的JSOn内容,我们可以在application.properties配置文件中用过info前缀来设置一些属性

1.2.度量类指标类
度量类指标类: 获取应用程序中用于监控的度量指标,比如内存信息、线程池信息、HTTP请求统计等

/metrics 返回当前应用的各类重要度量指标,可以通过/metrics/{name}接口来获取更细粒度的度量信息
/health 用来获取应用的各类健康指标信息,自定义健康监测需实现接口or.springframework.boot.actuate.health.HealthIndicator
/dump 用来暴露程序运行中的线程信息,采用java.lang.ThreadMCBean.dumpAllThreads方法返回所有含有同步信息的活动线程详情
/trace 返回基本的HTTP追踪信息

1.3.操作控制类
操作控制类: 提供了对应用关闭等操作类功能,需要通过属性来配置开启操作

/shutdown 配置: endpoints.shutdown.enabled=true 关闭远程应用,post请求

2.Eurake

2.1分类
服务端: 服务注册中心
客户端: 主要处理服务的注册与发现:服务提供者和服务消费者

客户端

 org.springframework.cloud.netflix.eureka.EurekaClientConfigBean,该类是加载eureka client的配置文件的内容,从该类中可以看到一些默认的设置

Eureka client 的配置主要分为以下几个方面:

  • 服务注册相关的配置信息,包括服务注册中心的地址、服务获取的时间间隔、可用区域等
  • 服务实例相关的配置信息,包括服务实例名称、ip地址、端口、健康监测路径等。
  • 其他常见配置说明(见下面)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

服务端

Eureka Server 更类似于一个现成的产品,大多数情况下,我们不需要修改它的配置信息。具体默认配置可查看配置类

 org.springframework.cloud.netflix.eureka.EurekaServerConfigBean,该类是加载eureka server的配置文件的内容,从该类中可以看到一些默认的设置

Eureka Server配置主要分为以下几个方面:

1.服务实例类配置,这些配置都以eureka.instance为前缀。

 通过org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean的源码查看。

i. 元数据:
 它是Eureka客户端向服务注册中心发送注册请求时,用来描述自身服务信息的对象,其中包含了一些标准化的元数据,比如服务名称、实例名称、实例IP、实例端口等用于服务治理的重要信息;以及一些用于负载均衡策略或是其他特殊用途的自定义元数据信息。

元数据分为两大类:

  • 原生元数据(标准化元数据)
  • 自定义元数据

 元数据可以通过eureka.instance.<properties>=<value>的格式对标准化元数据进行配置。其中<properties>就是EurekaInstanceConfigBean对象中的成员变量。而对于自定义元数据,可以通过eureka.instance.metadataMap.<key>=<value>的格式来进行配置,比如

eureka.instance.metadataMap.zone=shanghai

常见的元数据配置

A. 实例名配置

它是区分同一服务不同实例的唯一标识。在Spring Cloud配置中,针对同一主机启动多实例的情况,对实例名的默认命名做了合理的扩展,它采用了如下默认规则:

${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}

对于实例名的命名规则,我们可以通过eureka.instance.instanceId参数来配置。

B.端点配置
主要是一些URL的配置信息。比喻spring-boot-actuator模块的/info端点和/health端点等。大多数情况下,我们并不需要修改这几个URL的配置,但是在一些特殊的情况下,比如为应用设置了context-path,这时,所有的spring-boot-actuator模块的监控端点都会增加一个前缀。所以我们要做类似如下的配置

management.context-path=/hello

eureka.instance.statusPageUrlPath=${management.context-path}/info
eureka.instance.healthCheckUrlPath=${management.context-path}/health

另外,有时为了安全考虑,也可以修改/info和/health端点的原始路径。这时我们也需要做一些特数据配置:

endpoints.info.path=/appinfo
endpoints.health.path=/checkhealth

eureka.instance.statusPageUrlPath=/${endpoints.info.path}
eureka.instance.healthCheckUrlPath=/${endpoints.health.path}

由于Eureka的服务注册中心默认以HTTP的方式来访问和暴露这些端点,因此当客户端应有以HTTPS的方式来暴露服务和监控端点时,相对路径的配置方式就无法满足需求了。 所以Spring Cloud Eureka还提供了绝对路径的配置参数,如下:

eureka.instance.statusPageUrlPath=https://${eureka.instance.hostname}/info
eureka.instance.healthCheckUrlPath=https://${eureka.instance.hostname}/health
eureka.instance.homePageUrl=https://${eureka.instance.hostname}

C.健康检测

默认情况下,Eureka中各个服务实例的健康监测并不是通过spring-boot-actuator模块的/health端点来实现的,而是依靠客户端心跳的方式来保持服务实例的存活。默认的心跳实现方式可以有效检查客户端进程是否正常运作,但却无法保证客户端应有能够正常提供服务。在Spring Cloud Eureka中,我们可以通过简单配置,把Eureka客户端的健康检测交给spring-boot-actuator模块的/health端点,以实现更加全面的健康状态维护。

详细配置如下:

  • 在pom.xml中添加spring-boot-actuator模块的依赖
  • 在application.properties中增加参数配置eureka.client.healthcheck.enabled=true
  • 如果客户端的/health端点路径做了一些特殊处理,请参考上面介绍端点配置时的方法进行配置,让服务注册中心可以正确访问到健康监测端点。

D.其他配置

这些参数均以eureka.instance为前缀
在这里插入图片描述
在上面的这些配置中,除了前三个配置参数在需要的时候可以做一些调整,其他的参数配置大多数情况下不需要进行配置,使用默认值即可。

通信协议

默认情况下,Eureka使用Jersey和XStream配合JSON作为Server与Client之间的 通信协议。你也可以选择自己实现的协议来替代。
在这里插入图片描述
在这里插入图片描述

3.Ribbon

在Spring Cloud的服务治理框架中,默认会创建针对各个服务治理框架的Ribbon自动化整合配置,比如Eureka的
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration, Consul中的org.springframework.cloud.consul.discovery.RibbonConsulAutoConfiguration。在实际使用的时候,我么通过查看这两个类的实现,以找到他们的配置详情来帮助我们更好地使用它。

通过Spring Cloud Ribbon的封装,我们再微服务架构中使用客户端负载均衡调用非常简单,只需要如下两步:

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

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

GET请求

在RestTemplate中,对GET请求可以通过如下两个方法实现:

第一种: getForEntity函数。该函数返回的是ResponseEntity,该对象事Spring对HTTP请求响应的封装。比如HttpStatus、HttpHeaders以及泛型类型的请求对象。而返回的ResponseEntity对象中的body内容类型会根据第二个参数装换位String类型

RestTemplate restTemplate = new RestTemplate ();
ResponseEntity<User> responseEntity = restTemplate.getForEntity("http://USER_SERVICE/user?name={name}", User.class, ""didi");
User body = responseEntity .getBody();
  • getForEntity(String url, Class responseType, Object … urlVariables):
    GET请求的参数绑定通常使用url中拼接的方式(不适用RESTful方式?),比如http://USER_SERVICE/user?name=didi,但更好的方法是在url中使用占位符配合urlVariables参数实现GET请求的参数绑定。可以这样:
 getForEntity("http://USER_SERVICE/user?name={1}", String.class, "didi")

需要注意的是,由于urlVariables参数是一个数组,所以它的顺序会对应url中占位符定义的数字顺序。

  • getForEntity(String url, Class responseType, Map urlVariables)
RestTemplate restTemplate = new RestTemplate ();
Map<String, String> params = new HashMap<>();
params.put("name", "data");
ResponseEntity<String> responseEntity = resetTemplate.getForEntity("http://USER_SERVICE/user?name={name}", String.class, params);
  • getForEntity(URI url, Class responseType) : 该方法用URI对象代替url和urlVariables参数来指定访问地址和参数绑定。
RestTemplate restTemplate = new RestTemplate ();
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
		"http://USER_SERVICE/user?name={name}")
		.build()
		.expand("dodo")
		.encode();
URI uri= uriComponents.toUri();
ResponseEntity<String> responseEntity = resetTemplate.getForEntity(uri, String.class);

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

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

它与getForEntity函数类似,也提供了三种不同的重载实现。

  • getForObject(String url, Class responseType, Object … urlVariables)
  • getForObject (String url, Class responseType, Map urlVariables)
  • getForObject (URI url, Class responseType)

POST请求
在RestTemplate中,对POST请求时可以通过如下三个方法进行调用实现。

第一种:postForEntity函数。

RestTemplate restTemplate = new RestTemplate ();
User user = new User("didi", 30);
ResponseEntity<String> responseEntity = restTemplate .getForEntity("http://USER_SERVICE/user", user, String.class);
String body = responseEntity.getBody();

postForEntity函数实现了三站不同的重载方法。

  • postForEntity(String url, Object request, Class responseType, Object … urlVariables)
  • postForEntity(String url, Object request, Class responseType, Map urlVariables)
  • postForEntity(URI url, Object request, Class responseType)

request参数,该参数可以是一个普通的对象,也可以是一个HttpEntity对象,如果是一个普通对象,而非HttpEntity对象的时候,RestTemplate会将请求对象转换为一个HttpEntity对象来处理,其中Object就是request的类型,request内容不视作完整的body来处理;而如果request是一个HttpEntity对象,那么就会被当作一个完整的HTTP请求对象来处理。这个request中不仅包含了body的内容,也包含了header的内容。

RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
headers.setContentType(type);
headers.add("Accept", MediaType.APPLICATION_JSON.toString());

JSONObject jsonObj = JSONObject.fromObject(params);

HttpEntity<String> formEntity = new HttpEntity<String>(jsonObj.toString(), headers);

String result = restTemplate.postForObject(url, formEntity, String.class);

第二种:postForObject函数。和postForEntity函数类似,它的作用是简化postForEntity的后续处理:

RestTemplate restTemplate = new RestTemplate ();
User user = new User("didi", 30);
String result= restTemplate .postForObject("http://USER_SERVICE/user", user, String.class);

postForObject函数也实心了三种不同的重载方法:

  • postForObject(String url, Object request, Class responseType, Object … urlVariables)
  • postForObject(String url, Object request, Class responseType, Map urlVariables)
  • postForObject(URI url, Object request, Class responseType)

第三种: postForLocation函数。该方法实现了以post请求提交资源,并返回新资源的URI,比如下面的例子:

User user = new User("didi", 40);
URI responseURI = restTemplate.postForLocation("http://USER_SERVICE/user", user);

postForLocation函数实现了三种不同的重载方法:

  • postForLocation(String url, Object request, Object … urlVariables);
  • postForLocation(String url, Object request,Map urlVariables);
  • postForLocation(URI url, Object request);

源码分析:

通过搜索LoadBalancerClient可以发现,这是Spring Cloud中定义的一个接口:

package org.springframework.cloud.client.loadbalancer;
import org.springframework.cloud.client.ServiceInstance;

public interface ServiceInstanceChooser {
    ServiceInstance choose(String var1);
}

package org.springframework.cloud.client.loadbalancer;

import java.io.IOException;
import java.net.URI;
import org.springframework.cloud.client.ServiceInstance;

public interface LoadBalancerClient extends ServiceInstanceChooser {
    <T> T execute(String var1, LoadBalancerRequest<T> var2) throws IOException;

    <T> T execute(String var1, ServiceInstance var2, LoadBalancerRequest<T> var3) throws IOException;

    URI reconstructURI(ServiceInstance var1, URI var2);
}

说明

  • ServiceInstance choose(String var1) 根据传入的服务名serviceId,从负载均衡器中挑选一个对应的服务实例。
  • T execute(String var1, LoadBalancerRequest var2): 使用从负载均衡器中挑选出的服务实例执行请求内容
  • URI reconstructURI(ServiceInstance var1, URI var2) 为系统构建一个合适的host:post形式的URI。在分布式系统中,我们使用逻辑上的服务名称作为host来构建URI(代替服务实例的host:post形式)进行请求,而返回的URI内容则是通过ServiceInstance的服务实例详情拼接出的具体host:post形式的请求地址。

整体架构
在这里插入图片描述

从类的命名上可初步判断LoadBalancerAutoConfiguration为实现客户端负载均衡器的自动化配置类,源码:


package org.springframework.cloud.client.loadbalancer;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancedBackOffPolicyFactory.NoBackOffPolicyFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryListenerFactory.DefaultRetryListenerFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicyFactory.NeverRetryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.RestTemplate;

@Configuration
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnBean({LoadBalancerClient.class})
@EnableConfigurationProperties({LoadBalancerRetryProperties.class})
public class LoadBalancerAutoConfiguration {
    @LoadBalanced
    @Autowired(
        required = false
    )
    private List<RestTemplate> restTemplates = Collections.emptyList();
    @Autowired(
        required = false
    )
    private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

    public LoadBalancerAutoConfiguration() {
    }

    @Bean
    public SmartInitializingSingleton loadBalancedRestTemplateInitializer(final List<RestTemplateCustomizer> customizers) {
        return new SmartInitializingSingleton() {
            public void afterSingletonsInstantiated() {
                Iterator var1 = LoadBalancerAutoConfiguration.this.restTemplates.iterator();

                while(var1.hasNext()) {
                    RestTemplate restTemplate = (RestTemplate)var1.next();
                    Iterator var3 = customizers.iterator();

                    while(var3.hasNext()) {
                        RestTemplateCustomizer customizer = (RestTemplateCustomizer)var3.next();
                        customizer.customize(restTemplate);
                    }
                }

            }
        };
    }

    @Bean
    @ConditionalOnMissingBean
    public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
        return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
    }

    @Configuration
    @ConditionalOnClass({RetryTemplate.class})
    public static class RetryInterceptorAutoConfiguration {
        public RetryInterceptorAutoConfiguration() {
        }

        @Bean
        @ConditionalOnMissingBean
        public RetryLoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRetryProperties properties, LoadBalancedRetryPolicyFactory lbRetryPolicyFactory, LoadBalancerRequestFactory requestFactory, LoadBalancedBackOffPolicyFactory backOffPolicyFactory, LoadBalancedRetryListenerFactory retryListenerFactory) {
            return new RetryLoadBalancerInterceptor(loadBalancerClient, properties, lbRetryPolicyFactory, requestFactory, backOffPolicyFactory, retryListenerFactory);
        }

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

    @Configuration
    @ConditionalOnClass({RetryTemplate.class})
    public static class RetryAutoConfiguration {
        public RetryAutoConfiguration() {
        }

        @Bean
        @ConditionalOnMissingBean
        public RetryTemplate retryTemplate() {
            RetryTemplate template = new RetryTemplate();
            template.setThrowLastExceptionOnExhausted(true);
            return template;
        }

        @Bean
        @ConditionalOnMissingBean
        public LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory() {
            return new NeverRetryFactory();
        }

        @Bean
        @ConditionalOnMissingBean
        public LoadBalancedBackOffPolicyFactory loadBalancedBackOffPolicyFactory() {
            return new NoBackOffPolicyFactory();
        }

        @Bean
        @ConditionalOnMissingBean
        public LoadBalancedRetryListenerFactory loadBalancedRetryListenerFactory() {
            return new DefaultRetryListenerFactory();
        }
    }

    @Configuration
    @ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
    static class LoadBalancerInterceptorConfig {
        LoadBalancerInterceptorConfig() {
        }

        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }

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

从上面可以看出,Ribbon实现的自动负载均衡器自动化配置需要满足下面两个条件:

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

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

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

Spring框架提供了很多@Condition给我们用
@ConditionalOnBean(仅仅在当前上下文中存在某个对象时,才会实例化一个Bean)
@ConditionalOnClass(某个class位于类路径上,才会实例化一个Bean)
@ConditionalOnExpression(当表达式为true的时候,才会实例化一个Bean)
@ConditionalOnMissingBean(仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean)。
@ConditionalOnMissingClass(某个class类路径上不存在的时候,才会实例化一个Bean)
@ConditionalOnNotWebApplication(不是web应用)

另一种解释
@ConditionalOnClass:该注解的参数对应的类必须存在,否则不解析该注解修饰的配置类;
@ConditionalOnMissingBean:该注解表示,如果存在它修饰的类的bean,则不需要再创建这个bean;可以给该注解传入参数例如@ConditionOnMissingBean(name = “example”),这个表示如果name为“example”的bean存在,这该注解修饰的代码块不执行。

接下来,看看LoadBalancerInterceptor拦截器是如何将一个RestTemplate变成客户端负载均衡的。

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
    private LoadBalancerClient  loadBalancer;
    private LoadBalancerRequestFactory requestFactory;

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

    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }

    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
    }
}

通过上面的分析可知,源码中注入了LoadBalancerClient 。当一个被@LoadBalanced注解修饰的RestTemplate对象向外Http请求时,会被LoadBalancerInterceptor类的intercept函数拦截,通过从HttpRequest的URI中的getHost()就可以拿到服务名,然后调用execute函数去根据服务名来选择实例并发起实际的请求。

接下来继续分析LoadBalancerClient 的实现类RibbonLoadBalancerClient。

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

public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
        Server server = null;
        if (serviceInstance instanceof RibbonLoadBalancerClient.RibbonServer) {
            server = ((RibbonLoadBalancerClient.RibbonServer)serviceInstance).getServer();
        }

        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        } else {
            RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId);
            RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);

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

通过上面可知,首先通过getServer根据传入的服务名serviceId去获得具体的服务实例:

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

我们先来看一下loadBalancer接口:

public interface ILoadBalancer {

	/**
	 * Initial list of servers.
	 * This API also serves to add additional ones at a later time
	 * The same logical server (host:port) could essentially be added multiple times
	 * (helpful in cases where you want to give more "weightage" perhaps ..)
	 * 
	 * @param newServers new servers to add
	 */
	public void addServers(List<Server> newServers);
	
	/**
	 * Choose a server from load balancer.
	 * 
	 * @param key An object that the load balancer may use to determine which server to return. null if 
	 *         the load balancer does not use this parameter.
	 * @return server chosen
	 */
	public Server chooseServer(Object key);
	
	/**
	 * To be called by the clients of the load balancer to notify that a Server is down
	 * else, the LB will think its still Alive until the next Ping cycle - potentially
	 * (assuming that the LB Impl does a ping)
	 * 
	 * @param server Server to mark as down
	 */
	public void markServerDown(Server server);
	
	/**
	 * @deprecated 2016-01-20 This method is deprecated in favor of the
	 * cleaner {@link #getReachableServers} (equivalent to availableOnly=true)
	 * and {@link #getAllServers} API (equivalent to availableOnly=false).
	 *
	 * Get the current list of servers.
	 *
	 * @param availableOnly if true, only live and available servers should be returned
	 */
	@Deprecated
	public List<Server> getServerList(boolean availableOnly);

	/**
	 * @return Only the servers that are up and reachable.
     */
    public List<Server> getReachableServers();

    /**
     * @return All known servers, both reachable and unreachable.
     */
	public List<Server> getAllServers();
}

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

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

通过进一步分析源码可知。BaseLoadBalancer类实现了基础的负载均衡,而DynamicServerListLoadBalancer和ZoneAwareLoadBalancer在负载均衡的策略上做了一些功能的扩展
在这里插入图片描述

通过分析RibbonClientConfiguration配置类,可以知道在整合时默认采用了ZoneAwareLoadBalancer来实现负载均衡器。

@Bean
    @ConditionalOnMissingBean
    public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
        return (ILoadBalancer)(this.propertiesFactory.isSet(ILoadBalancer.class, this.name) ? (ILoadBalancer)this.propertiesFactory.get(ILoadBalancer.class, config, this.name) : new ZoneAwareLoadBalancer(config, rule, ping, serverList, serverListFilter, serverListUpdater));
    }

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

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

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

ZoneAwareLoadBalancer
ZoneAwareLoadBalancer是对DynamicServerListLoadBalancer的扩展,在DynamicServerListLoadBalancer中并没有重写选择具体服务实例的chooseServer函数,所以它依然采用在BaseLoadBalancer中实现的算法。使用RoundRobinRule规则,以线性轮询的方式来选择调用服务的实例,该算法实现简单并没有区域(Zone)的概念,所以它会把所有实例视为一个Zone下的节点来看待,这样就会周期性地跨区域(Zone)访问的情况,由于跨区域会产生更高的延迟,这些实例主要以防止区域性故障实现高可用为目的而不能作为常规访问的实例,所以在多区域部署的情况下就会有一定的性能问题,而该负载均衡器则可以避免这样的问题。

负载均衡策略
在这里插入图片描述

AbstractLoadBalancerRule

定义了负载均衡器ILoadBalancer对象,该对象能够在具体实现选择服务策略时,获取到一些负载均衡器中维护的信息来作为分配依据,并以此来设计一些算法来针对特定场景的高效策略。

public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {

    private ILoadBalancer lb;
        
    @Override
    public void setLoadBalancer(ILoadBalancer lb){
        this.lb = lb;
    }
    
    @Override
    public ILoadBalancer getLoadBalancer(){
        return lb;
    }      
}

RandomRule
该策略实现了从服务清单中随机选择一个服务实例的功能。

public Server choose(ILoadBalancer lb, Object key) {
    if (lb == null) {
        return null;
    }
    Server server = null;

    while (server == null) {
        if (Thread.interrupted()) {
            return null;
        }
        List<Server> upList = lb.getReachableServers();
        List<Server> allList = lb.getAllServers();

        int serverCount = allList.size();
        if (serverCount == 0) {
            /*
             * No servers. End regardless of pass, because subsequent passes
             * only get more restrictive.
             */
            return null;
        }

        int index = rand.nextInt(serverCount);
        server = upList.get(index);

        if (server == null) {
            /*
             * The only time this should happen is if the server list were
             * somehow trimmed. This is a transient condition. Retry after
             * yielding.
             */
            Thread.yield();
            continue;
        }

        if (server.isAlive()) {
            return (server);
        }

        // Shouldn't actually happen.. but must be transient or a bug.
        server = null;
        Thread.yield();
    }

    return server;

}

RoundRobinRule
该策略实现了按照线性轮询的方式依次选择每个服务实例的功能。

public Server choose(ILoadBalancer lb, Object key) {
    if (lb == null) {
        log.warn("no load balancer");
        return null;
    }

    Server server = null;
    int count = 0;
    while (server == null && count++ < 10) {
        List<Server> reachableServers = lb.getReachableServers();
        List<Server> allServers = lb.getAllServers();
        int upCount = reachableServers.size();
        int serverCount = allServers.size();

        if ((upCount == 0) || (serverCount == 0)) {
            log.warn("No up servers available from load balancer: " + lb);
            return null;
        }

        int nextServerIndex = incrementAndGetModulo(serverCount);
        server = allServers.get(nextServerIndex);

        if (server == null) {
            /* Transient. */
            Thread.yield();
            continue;
        }

        if (server.isAlive() && (server.isReadyToServe())) {
            return (server);
        }

        // Next.
        server = null;
    }

    if (count >= 10) {
        log.warn("No available alive servers after 10 tries from load balancer: "
                + lb);
    }
    return server;
}

private int incrementAndGetModulo(int modulo) {
   for (;;) {
       int current = nextServerCyclicCounter.get();
       int next = (current + 1) % modulo;
       if (nextServerCyclicCounter.compareAndSet(current, next))
           return next;
   }
}

RetryRule
该策略实现了一个具备重试机制的实例选择功能。其内部还定义了一个IRule对象,默认使用RoundRobinRule实例。在choose方法中则实现了对内部定义的策略进行反复尝试的策略,若期间能够选择到具体的服务实例就返回,若选择不到就根据设置的尝试结束时间为阀值(maxRetryMillis参数定义的值+choose方法开始执行是的时间戳),当超过该值后就返回null

public void setMaxRetryMillis(long maxRetryMillis) {
	if (maxRetryMillis > 0) {
		this.maxRetryMillis = maxRetryMillis;
	} else {
		this.maxRetryMillis = 500;
	}
}

public Server choose(ILoadBalancer lb, Object key) {
	long requestTime = System.currentTimeMillis();
	long deadline = requestTime + maxRetryMillis;

	Server answer = null;

	answer = subRule.choose(key);

	if (((answer == null) || (!answer.isAlive()))
			&& (System.currentTimeMillis() < deadline)) {

		InterruptTask task = new InterruptTask(deadline
				- System.currentTimeMillis());

		while (!Thread.interrupted()) {
			answer = subRule.choose(key);

			if (((answer == null) || (!answer.isAlive()))
					&& (System.currentTimeMillis() < deadline)) {
				/* pause and retry hoping it's transient */
				Thread.yield();
			} else {
				break;
			}
		}

		task.cancel();
	}

	if ((answer == null) || (!answer.isAlive())) {
		return null;
	} else {
		return answer;
	}
}

WeightedResponseTimeRule
该策略是对RoundRobinRule的扩展,增加了根据实例的运行情况来计数权重,并根据权重来挑选实例。该策略实例化的时候在内部创建了一个定时任务,每过30s便去统计一下各个实例的权重。

ClientConfigEnableRoundRobinRule
该策略较为特殊,一般不直接使用它。该策略内部定义了一个RoundRobinRule策略,choose函数的实现也是使用了RoundRobinRule的线下轮询机制。一般使用方法:继承该策略,默认的choose方法实现了线性轮询机制,在子类中做一些高级策略时通常可能会存在一些无法实施的情况,那么就可以用父类的实现作为备选。

BestAvailableRule
该策略继承ClientConfigEnableRoundRobinRule,在实现中它注入了负载均衡器的统计对象LoadBalancerStats,同时在具体的choose算法中利用LoadBalancerStats保存的实例统计信息来满足要求的实例。它通过遍历负载均衡器中维护的所有服务实例,会过滤掉故障的实例,并找出并发请求数最少的一个,所以该策略的特性是可选出最空闲的实例

PredicateBasedRule
抽象策略,继承了ClientConfigEnableRoundRobinRule,基于Predicate实现的策略,Predicate是Google Guava Collection工具对集合进行过滤的条件接口,策略:先过滤清单,在轮询选择

AvailableFilteringRule
继承自PredicationBasedRule

ZoneAvoidanceRule
继承自PredicationBasedRule

配置详解

在引入Spring Cloud Ribbon依赖后,就能够自动化构建下面这些接口的实现:

1.自动化配置

  • IClientConfig:Ribbon的客户端配置,默认采用DefaultClientConfigImpl实现。

  • IRule:默认采用ZoneAvoidanceRule实现,该策略能够在多区域环境下选出最佳区域的实例。

  • IPing:默认采用DummyPing实现,该检查策略是一个特殊的实现,实际上并不会检查实例是否可用,而是始终返回true,默认认为所有实例都可用。

  • ServerList:服务实例清单的维护机制,默认采用ConfigurationBasedServerList实现。

  • ServerlistFilter:服务实例清单过滤机制,默认采用ZonePreferenceServerListFilter,该策略能够优先过滤出于请求方处于同区域的服务实例。

  • ILoadBalancer:负载均衡器,默认采用ZoneAwareLoadBalancer,它具备区域感知的能力。

上面这些自动化配置仅在没有引入Spring Cloud Eureka等服务治理框架时如此

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

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

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

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

2.Camden版本对RibbonClient配置的优化

在Camden版本中,可以直接通过下面的形式进行配置

<clientName>.ribbon.<key> = <value>

比如我们要实现与上面的例子一样的配置(将hello-service服务客户端的IPing接口替换为PingUrl),只需在application.properties配置中增加下面的内容

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

hello-service为服务名,NFLoadBalancerPingClassName参数用来指定具体的IPing接口实现类。

在Camden版本,Spring Cloud Ribbon新增了PropertiesFactory类来动态为RibbonClient创建这些接口实现。

public class PropertiesFactory {
    @Autowired
    private Environment environment;
 
    private Map<Class, String> classToProperty = new HashMap<>();
    //在Camden版本可以通过配置的方式,更加方便地为RibbonClient指定
    //ILoadBalancer、IPing、IRule、ServerList、ServerListFilter的定制化实现
    public PropertiesFactory() {
        //配置ILoadBalancer接口的实现
        classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName");
        //配置IPing接口的实现
        classToProperty.put(IPing.class, "NFLoadBalancerPingClassName");
        //配置IRule接口的实现
        classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName");
        //配置ServerList接口的实现
        classToProperty.put(ServerList.class, "NIWSServerListClassName");
        //配置ServerListFilter接口的实现
        classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName");
    }
 
    public boolean isSet(Class clazz, String name) {
        return StringUtils.hasText(getClassName(clazz, name));
    }
 
    public String getClassName(Class clazz, String name) {
        if (this.classToProperty.containsKey(clazz)) {
            String classNameProperty = this.classToProperty.get(clazz);
            String className = environment.getProperty(name + "." + NAMESPACE + "." + classNameProperty);
            return className;
        }
        return null;
    }
 
    @SuppressWarnings("unchecked")
    public <C> C get(Class<C> clazz, IClientConfig config, String name) {
        String className = getClassName(clazz, name);
        if (StringUtils.hasText(className)) {
            try {
                Class<?> toInstantiate = Class.forName(className);
                return (C) SpringClientFactory.instantiateWithConfig(toInstantiate, config);
            } catch (ClassNotFoundException e) {
                throw new IllegalArgumentException("Unknown class to load "+className+" for class " + clazz + " named " + name);
            }
        }
        return null;
    }
}

3.参数配置

对于Ribbon的参数配置,主要有两种,全局配置以及指定客户端配置

i.全局配置

1 格式

ribbon.<key>=<value>

代表了Ribbon客户端配置的参数名,则代表了对应参数的值。

2 举例

ribbon.ConnectTimeout=250

3 说明

全局配置可以作为默认值进行设置,当指定客户端配置相应key值时,将覆盖全局配置的内容。

ii.指定客户端配置

1 格式

<client>.ribbon.<key>=<value>

<key>和<value>的含义同全局配置相同,而<client>代表了客户端的名称。

2 举例

hello-service.ribbon.listofServers=localhost:8001,localhost:8002,localhost:8003

3 配置查看方法

对于Ribbon参宿的key以及value类型的定义,可以通过查看下面的类来获得更为详细的配置内容。

com.netflix.client.config.CommonClientConfigKey

4.与Eureka结合

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

在与Spring cloud Eureka结合使用的时候,不需要再去指定类似的hello-service.ribbon.listOfServers的参数来指定具体的服务实例清单,因为Eureka将会为我们维护所有服务的实例清单,而对于Ribbon的参数配置,我们依然可以采用之前的两种配置方式来实现。

此外,由于spring Cloud Ribbon默认实现了区域亲和策略,所以,可以通过Eureka实例的元数据配置来实现区域化的实例配置方案。比如可以将不同机房的实例配置成不同的区域值,作为跨区域的容器机制实现。而实现也非常简单,只需要服务实例的元数据中增加zone参数来指定自己所在的区域,比如:
eureka.instance.metadataMap.zone=shanghai
在Spring Cloud Ribbon与Spring Cloud Eureka结合的工程中,我们可以通过参数禁用Eureka对Ribbon服务实例的维护实现。这时又需要自己去维护服务实例列表了。

ribbon.eureka.enabled=false.

5.重试机制

由于Spring Cloud Eureka实现的服务治理机制强调了cap原理的ap机制(即可用性和可靠性),与zookeeper这类强调cp(一致性,可靠性)服务质量框架最大的区别就是,Eureka为了实现更高的服务可用性,牺牲了一定的一致性,在极端情况下宁愿接受故障实例也不要丢弃"健康"实例。

比如说,当服务注册中心的网络发生故障断开时候,由于所有的服务实例无法维护续约心跳,在强调ap的服务治理中将会把所有服务实例剔除掉,而Eureka则会因为超过85%的实例丢失心跳而触发保护机制,注册中心将会保留此时的所有节点,以实现服务间依然可以进行互相调用的场景,即使其中有部分故障节点,但这样做可以继续保障大多数服务的正常消费。

在Camden版本,整合了spring retry来增强RestTemplate的重试能力,对于我们开发者来说,只需要简单配置,即可完成重试策略。

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

hello-service.ribbon.ConnectTimeout=250
hello-service.ribbon.ReadTimeout=1000
hello-service.ribbon.OkToRetryOnAllOperations=true
hello-service.ribbon.MaxAutoRetriesNextServer=2
hello-service.ribbon.maxAutoRetries=1

spring.cloud.loadbalancer.retry.enabled:该参数用来开启重试机制,它默认是关闭的。

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds:断路器的超时时间需要大于Ribbon的超时时间,不然不会触发重试。

user-service.ribbon.ConnectTimeout:请求连接超时时间。
user-service.ribbon.ReadTimeout:请求处理的超时时间
user-service.ribbon.OkToRetryOnAllOperations:对所有操作请求都进行重试。
user-service.ribbon.MaxAutoRetriesNextServer:切换实例的重试次数。
user-service.ribbon.maxAutoRetries:对当前实例的重试次数。

根据以上配置,当访问到故障请求的时候,它会再尝试访问一次当前实例(次数由maxAutoRetries配置),如果不行,就换一个实例进行访问,如果还是不行,再换一个实例访问(更换次数由MaxAutoRetriesNextServer配置),如果依然不行,返回失败。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值