spring-cloud-ribbon负载均衡组件

Ribbon简介:                 

  Spring Cloud Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。 通过 Spring Cloud 的封装, 可以让我们轻松地将面向服务的 REST 模板请求自动转换成客户端负载均衡的服务调用。 Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、 配置中心、 API 网关那样需要独立部署, 但是它几乎存在于每一个Spring Cloud 构建的微服务和基础设施中。 因为微服务间的调用,API 网关的请求转发等内容实际上都是通过Ribbon 来实现的,包括后续我们将要介绍的 Feign, 它也是基于 Ribbon实现的工具。  

  在微服务架构中,业务都会被拆分成一个独立的服务,服务与服务的通讯是基于http Restful的。Spring Cloud有两种服务调用方式,一种是ribbon+restTemplate,另一种是feign。  我们通常所说的负载均衡都指的是服务端负载均衡, 其中分为硬件负载均衡和软件负载均衡。 硬件负载均衡主要通过在服务器节点之间安装专门用于负载均衡的设备,比如F5等;而软件负载均衡则是通过在服务器上安装 一些具有均衡负载功能或模块的软件来完成请求分发工作, 比如Nginx等。 不论采用硬件负载均衡还是软件负载均衡,硬件负载均衡的设备或是软件负载均衡的软件模块都会维护一个下挂可用的服务端清单,通过心跳检测来剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。

  当客户端发送请求到负载均衡设备的时候, 该设备按某种算法(比如线性轮询、 按权重负载、 按流量负载等)从维护的可用服务端清单中取出 一台服务端的地址, 然后进行转发。而客户端负载均衡和服务端负载均衡最大的不同点在千上面所提到的服务清单所存储的位置。 在客户端负载均衡中, 所有客户端节点都维护着自己要访问的服务端清单, 而这些服务端的清单来自于服务注册中心,比如上一章我们介绍的Eureka服务端。同服务端负载均衡的架构类似, 在客户端负载均衡中也需要心跳去维护服务端清单的健康性, 只是这个步骤需要与服务注册中心配合完成。所有微服务汇集到了 Eureka 之中,这样所有的微服务汇集到了Eureka之中,而客户端的调用也应该通过Eureka完成,这种调用可以通过Ribbon技术来实现。Ribbon是一个服务调用组件,并且是一个客户端实现负载均衡的处理的组件。

  如上图,当配置了负载均衡的客户端对服务端发情请求时,会根据Eureka服务中已经注册的并且处于活动状态的服务,结合自己配置里的Robbin对应的负载均衡算法进行负载分流。常见的轮询、随机等算法,当然也可以自定义算法。

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

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

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

Ribbon客户端的实现:

1.添加pom依赖

<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.SR3</spring-cloud.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <!-- SpringCloud 所有子项目 版本集中管理. 统一所有SpringCloud依赖项目的版本依赖-->
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
            <version>1.4.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin><!-- SpringBoot 项目打jar包的Maven插件 -->
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

2. 配置文件application.yml

server:
  port: 9001

spring:
  application:
    name: ribbon-server #服务注册到Eureka上使用的名称

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:7001/eureka/,http://localhost:7002/eureka/
  instance:
    instance-id: ribbon-server-9001

3.主启动类注解

@SpringBootApplication
@EnableDiscoveryClient
public class DeptConsumer8002_App 
{
    public static void main( String[] args )
    {
    	SpringApplication.run(DeptConsumer8002_App.class, args);
    }
}

4.负载规则配置

@Configuration
public class ConfigBean {

    @Bean
    @LoadBalanced // ribbon是客户端 的负载均衡工具
    //默认算法是轮询算法 核心组件IRule
    public RestTemplate getRestTemplate() {

        return new RestTemplate();
    }
}

5.Controller

@RestController
public class RibbonController {

    //  private static final String REST_URL_PREFIX="http://localhost:8001"; 单机版
    //集群的时候  需要配置该服务在eureka里注册的名字
    private static final String REST_URL_PREFIX="http://cloud-provider";

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping(value ="/hello")
    public String get() {
        return restTemplate.getForObject(REST_URL_PREFIX+"/hello", String.class);
    }
    //消费端可以调用服务发现
    @RequestMapping(value ="/discovery")
    public Object discovery() {
     // restTemplate 对Get、Post、Put、Delete四种请求类型和参数类型的调用均有实现,可以自己试试
        return restTemplate.getForObject(REST_URL_PREFIX+"/discovery", Object.class);
    }
}

  启动 ribbon-server 应用后, 我们可以在 Eureka 信息面板中看到, 当前除了cloud-provider之外, 还多了我们实现的 RIBBON-SERVER服务。当我们访问对应的 /hello或者/discovery 就能看到对应的负载均衡策略所转发获取回来的响应。

  如此便配置好了客户端负载均衡,Robbin的默认负载均衡算法是轮询算法,如果我们需要配置其他的算法规则   则需要在容器中注入我们需要的bean 例如:

@Bean
public IRule myRule() { // 负载均衡算法。
		
	return new RandomRule();//随机
}

  以下是自带的一些负载均衡算法:具体的逻辑查看对应的类的choose方法

  类图如下:

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

  可根据自己的需求寻找合适的配置。如果其中没有我们需要的算法,那么我们可以重新自己的负载算法。

自定义负载均衡策略:

  自定义类需要实现 IRule 接口,我们先来看一下该接口

public interface IRule{
    /*
     * choose one alive server from lb.allServers or
     * lb.upServers according to key
     * 
     * @return choosen Server object. NULL is returned if none
     *  server is available 
     */

    public Server choose(Object key);
    
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}

  该接口很简单,其核心方法 choose(Object key) 才是执行算法逻辑的核心。其中cloud为我们提供了IRule接口的抽象子类AbstractLoadBalancerRule ,因此我们只需要继承该类,并且重写choose方法既可以达到我们需要的算法逻辑。我们拿轮询算法作为例子,依然使用轮询算法,但是我们让他每个服务调用5次后再进行轮询。代码如下:

public class MyRule_ZY extends AbstractLoadBalancerRule {

    private int total = 0;
    private int currentIndex = 0;

    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) {
                return null;
            }
            //自定义的一些算法
            if (total < 5) {
                server = upList.get(currentIndex);
                total++;
            } else {
                total = 0;
                currentIndex++;
                if (currentIndex >= upList.size()) {
                    currentIndex = 0;
                }
            }
            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;

    }

    @Override
    public Server choose(Object key) {
        return choose(getLoadBalancer(), key);
    }

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
        // TODO Auto-generated method stub

    }
}

  此刻需要修改主启动类

@EnableEurekaClient
@SpringBootApplication
//自定义负载均衡算法 自定义配置类不能跟主启动类一个包或在子包下面
//name: 表示对哪个服务采用自定义算法
//configuration:负载算法类
@RibbonClient(name="cloud-provider" ,configuration= MyRule_ZY.class)
public class RibbonApp {
    private final static Logger log = LoggerFactory.getLogger(RibbonApp.class);

    public static void main(String[] args) {
        SpringApplication.run(RibbonApp.class,args);
        log.info("服务启动成功");

    }
}

  这样即可使用自己的负载均衡算法。

源码分析:

  这里的 RestTemplate 是 org.springframework.web.client 包下面的。他不是spring 自己提供的嘛?跟Ribbon的客户端负载均衡又有什么关系呢?

  首先, 回顾 一 下之前的消费者示例: 我们是如何实现客户端负载均衡的?仔细观察 一下之前的实现代码, 可以发现在 消费者的例子中, 可能就@LoadBalanced这个 注解是之前没有接触过的,并且从命名上来看也与负载均衡相关 。我们不妨以此为线索来看看Spring Cloud Ribbon的源码实现。

   从@LoadBalanced注解 源码的注释中可以知道, 该 注解用来给RestTemplate做标记, 这个从源码注释中也看到了:

/**
 * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
 * @author Spencer Gibb
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

  以使用负载均衡的客户端(LoadBalancerClient)来配置它。通过搜索LoadBalancerClient可以发现, 这是SpringCloud中定义的一个接口:

public interface LoadBalancerClient extends ServiceInstanceChooser {

    /**根据传入的 服 务名serviceld, 从负载均衡器中挑选 一 个对应服务的实例 。
     * execute request using a ServiceInstance from the LoadBalancer for the specified
     * service
     * @param serviceId the service id to look up the LoadBalancer
     * @param request allows implementations to execute pre and post actions such as
     * incrementing metrics
     * @return the result of the LoadBalancerRequest callback on the selected
     * ServiceInstance
     */
    <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;

    /**使用从负载均衡器中挑选出的服务实例来执行请求内容 。
     * execute request using a ServiceInstance from the LoadBalancer for the specified
     * service
     * @param serviceId the service id to look up the LoadBalancer
     * @param serviceInstance the service to execute the request to
     * @param request allows implementations to execute pre and post actions such as
     * incrementing metrics
     * @return the result of the LoadBalancerRequest callback on the selected
     * ServiceInstance
     */
    <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;

    /**为系统构建一 个合适的host:post形式的URI。
     * Create a proper URI with a real host and port for systems to utilize.
     * Some systems use a URI with the logical serivce name as the host,
     * such as http://myservice/path/to/service.  This will replace the
     * service name with the host:port from the ServiceInstance.
     * @param instance
     * @param original a URI with the host as a logical service name
     * @return a reconstructed URI
     */
    URI reconstructURI(ServiceInstance instance, URI original);
}

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

  从类的命名上可初步判断 LoadBalancerAutoConfiguration 为实现客户端负载均衡器的自动化配置类。 通过查看源码, 我们可以验证这 一 点假设:

@Configuration
//RestTemplate 类必须存在当前工程的环境中。
@ConditionalOnClass(RestTemplate.class)
//在Spring 的Bean工程中必须有LoadBalancerClie江的实现 Bean。
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
  .........
}

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

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

  接下来, 我们看看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) {
        // for backwards compatibility
        this(loadBalancer, new LoadBalancerRequestFactory(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();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    }
}

  通过源码以及之前的自动化配置类, 我们可以看到在拦截器中注入了 LoadBalancerClient的实现。 当一 个被 @LoadBalanced注解修饰的 RestTemplate 对象向外发起 HTTP 请求时, 会被 LoadBalancerinterceptor 类的 intercept 函数所拦截。 由于我们在使用 RestTemplate 时采用了服务名作为 host, 所以直接从 HttpRequest 的URI对象中通过 getHost ()就可以拿到服务名,然后调用 execute 函数去根据服务名来选择实例并发起实际的请求。分析到这里, LoadBalancerClient 还只是 一 个抽象的负载均衡器接口, 所以我们还需要找到它的具体实现类来进 一 步进行分析。 通过查看Ribbon的源码, 可以很容易地在org.springframework.cloud.netflix.ribbon 包下找到对应的实现类 Ribbon­LoadBalancerClient:我们着重看 excute方法

public class RibbonLoadBalancerClient implements LoadBalancerClient {
  // .........
  // 这个是获取服务实例
    @Override
    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));

        return execute(serviceId, ribbonServer, request);
    }
   // 这个是执行客户请求
    @Override
    public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
        Server server = null;
        if(serviceInstance instanceof RibbonServer) {
            server = ((RibbonServer)serviceInstance).getServer();
        }
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        }

        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 and rethrow so RestTemplate behaves correctly
        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"); // TODO: better handling of key
}

  通过 getServer 函数的实现源码,我们可以看到这里获取具体服务实例的时候并没有使用 LoadBalancerClient 接口中的 choose 函数, 而是使用了 Netflix Ribbon 自身的 ILoadBalancer 接口中定义的 chooseServer 函数。我们先来认识 一 下这个 ILoadBalancer 接口,在该接口中定义了一 个客户端负载均衡器需要的 一 系列抽象操作:

public interface ILoadBalancer {
  //向负载均衡器中维护的实例列表增加服务实例。
    void addServers(List<Server> var1);
  //通过某种策略, 从负载均衡器中挑选出一 个具体的服务实例。
    Server chooseServer(Object var1);
  //用来通知和标识负载均衡器中某个具体实例已经停止服务, 
  //不然负载均衡器在下 一 次获取服务实例清单前都会认为服务实例均是正常服务的。
  void markServerDown(Server var1);
  //
    /** @deprecated */
    @Deprecated
    List<Server> getServerList(boolean var1);
  //获取当前正常服务的实例列表。
    List<Server> getReachableServers();
  //获取所有已知的服务实例列表, 包括正常服务和停止服务的实例。
    List<Server> getAllServers();
}

  在该接口定义中涉及的 Server 对象定义是 一 个传统的服务端节点, 在该类中存储了服务端节点的 一 些元数据信息, 包括 host、 port 以及 一 些部署信息等。而对于该接口的实现,我们整理出如下图所示的结构。可以看到, BaseLoadBalancer 类实现了基础的负载均衡器,而 DynarnicServerListLoaclBalancer 和 ZoneAwareLoaclBalancer在负载均衡的策略上做了 一 些功能的扩展。

 

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

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

  下面, 我们再回到 RibbonLoadBalancerClient 的 execute 函数逻辑, 在通过ZoneAwareLoadBalancer 的 chooseServer 函数获取了负载均衡策略分配到的服务实例对象 Server 之后, 将其内容包装成RibbonServer 对象(该对象除了存储了服务实例的信息之外, 还增加了服务名 serviceId、 是否需要使用 HTTPS 等其他信息), 然后使用该对象再回调 LoadBalancerinterceptor 请求拦截器中 LoadBalancerRequest的 apply(final Serviceinstance instance) 函数, 向一 个实际的具体服务实例发起请求,从而实现 一 开始以服务名为 host 的 URI 请求到 host:post 形式的实际访问地址的转换。在 apply(final Serviceinstance instance) 函数中传入的 Serviceinstance接口对象是对服务实例的抽象定义。 在该接口中暴露了服务治理系统中每个服务实例需要提供的 一 些基本信息, 比如 serviceld 、 host 、 port 等, 具体定义如下:

public interface ServiceInstance {

    /**
     * @return the service id as registered.
     */
    String getServiceId();

    /**
     * @return the hostname of the registered ServiceInstance
     */
    String getHost();

    /**
     * @return the port of the registered ServiceInstance
     */
    int getPort();

    /**
     * @return if the port of the registered ServiceInstance is https or not
     */
    boolean isSecure();

    /**
     * @return the service uri address
     */
    URI getUri();

    /**
     * @return the key value pair metadata associated with the service instance
     */
    Map<String, String> getMetadata();

    /**
     * @return the scheme of the instance
     */
    default String getScheme() {
        return null;
    }
}

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

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

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

        public 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 接口中的 reconstructUR 工操作来组织具体请求地址的呢?

@Override
public ListenableFuture<ClientHttpResponse> apply(final ServiceInstance instance)
  throws Exception {
  HttpRequest serviceRequest = new ServiceRequestWrapper(request,
          instance, loadBalancer);
  return execution.executeAsync(serviceRequest, body);
}

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

public class ServiceRequestWrapper extends HttpRequestWrapper {
    private final ServiceInstance instance;
    private final LoadBalancerClient loadBalancer;

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

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

  在 LoadBalancerinterceptor 拦截器中,AsyncRequestExecution 的实例具体执行 execution.executeAsync(serviceRequest, body) 时, 会调用InterceptingAsyncClientHttpRequest下 AsyncRequestExecution类的 executeAysnc 函数,具体实现如下:

private class AsyncRequestExecution implements AsyncClientHttpRequestExecution {

        private Iterator<AsyncClientHttpRequestInterceptor> iterator;

        public AsyncRequestExecution() {
            this.iterator = interceptors.iterator();
        }

        @Override
        public ListenableFuture<ClientHttpResponse> executeAsync(HttpRequest request, byte[] body)
                throws IOException {

            if (this.iterator.hasNext()) {
                AsyncClientHttpRequestInterceptor interceptor = this.iterator.next();
                return interceptor.intercept(request, body, this);
            }
            else {
                URI uri = request.getURI();
                HttpMethod method = request.getMethod();
                HttpHeaders headers = request.getHeaders();

                Assert.state(method != null, "No standard HTTP method");
                AsyncClientHttpRequest delegate = requestFactory.createAsyncRequest(uri, method);
                delegate.getHeaders().putAll(headers);
                if (body.length > 0) {
                    StreamUtils.copy(body, delegate.getBody());
                }

                return delegate.executeAsync();
            }
        }
}

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

@Override
public URI reconstructURI(ServiceInstance instance, URI original) {
        Assert.notNull(instance, "instance can not be null");
        String serviceId = instance.getServiceId();
      //获取对应service Id 的负载均衡器的上下文贮 bbonLoadBalancerContext 对象
        RibbonLoadBalancerContext context = this.clientFactory
                .getLoadBalancerContext(serviceId);
        URI uri;
        Server server;
     //构建具体服务实例信息的 Server 对象
        if (instance instanceof RibbonServer) {
            RibbonServer ribbonServer = (RibbonServer) instance;
            server = ribbonServer.getServer();
            uri = updateToSecureConnectionIfNeeded(original, ribbonServer);
        } else {
            server = new Server(instance.getScheme(), instance.getHost(), instance.getPort());
            IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);
            ServerIntrospector serverIntrospector = serverIntrospector(serviceId);
            uri = updateToSecureConnectionIfNeeded(original, clientConfig,
                    serverIntrospector, server);
        }//构建服务实例的 URI 。
        return context.reconstructURIWithServer(server, uri);
}

  简单介绍一 下上面提到的SpringClientFactory 和 RibbonLoad­BalancerContext:

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

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

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

参数配置:

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

   全局配置的方式很简单, 只需使用 ribbon. < key>= < value>格式进行配置即可。比如, 我们可以像下面这样全局配置Ribbon创建连接的超时时间:

ribbon.ConnectTimeout = 250

  指定客户端的配置方式 采用< client> .ribbon. < key>= < value>的格式进行配置。< client>代表了客户端的名称, 如上文中我们在@RibbonClient中指定的名称,也可以将它理解 为是一个服务名。

cloud-provider.ribbon.listOfServers = localhost:8001,localhost:8002, localhost:8003

  对于Ribbon 参数的key以及value类型的定义,可以通过查看com.netflix.client.config.CommonClientConfigKey类获得更为详细的配置 内容, 不进行详细介绍。

重试机制:

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

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

  以我们上文服务的调用为例, 可以在配置文件中增加如下内容:

spring.cloud.loadbalancer.retry.enabled=true//该参数用来开启重试机制
hystrix.command.default.execution.isolation.thread.timeoutinMilliseconds = 10000 //断路器的超时时间需要大于Ribbon的超时时间, 不然不会触发重试。
cloud-provider.ribbon.ConnectTimeout = 250 //请求连接的超时时间。
cloud-provider.ribbon.ReadTimeout = 1000  //请求处理的超时时间。
cloud-provider.ribbon.OkToRetryOnAllOperations = true //对所有操作请求都进行重试。
cloud-provider.ribbon.MaxAutoRetriesNextServer = 2 //切换实例的重试次数。
cloud-provider.ribbon.MaxAutoRetries = 1 //对当前实例的重试次数。

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

posted @ 2019-04-22 14:24  吴振照  阅读(1185)  评论(0编辑  收藏  举报