微服务架构-实现技术之具体实现工具与框架5:Spring Cloud Feign与Ribbon原理与注意事项

微服务架构-实现技术之具体实现工具与框架5:Spring Cloud Feign与Ribbon原理与注意事项目录一、SpringCloudFeign概述与工作原理解读(一)服务间调用的几种方式(二)Feign概述二、FeignClent注解剖析+SpringCloudFeign基本功能配置解读(一)@FeignClient注解剖析(二)SpringCloudFeign基本功能配置(三)Feign请求超时问题方法一方法二方法三三、SpringC…

大家好,又见面了,我是你们的朋友全栈君。

目录

一、Spring Cloud Feign概述与工作原理解读

(一)服务间调用的几种方式

(二)Feign 概述

二、FeignClent注解剖析+Spring Cloud Feign基本功能配置解读

(一)@FeignClient 注解剖析

(二)Spring Cloud Feign基本功能配置

(三)Feign请求超时问题

方法一

方法二

方法三

三、Spring Cloud Ribbon概述与核心工作原理

(一)Ribbon与负载均衡

(二)Ribbon核心工作原理

1.Ribbon 服务配置方式

2.和Feign的集成模式

(三)LoadBalancer–负载均衡器的核心

1.负载均衡器的内部基本实现原理

2.如何维护Server列表?(新增、更新、删除)

3.负载均衡器如何维护服务实例的状态?

4.如何从服务列表中挑选一个合适的服务实例?

(1)服务实例容器:ServerList的维护

(2) 服务实例列表过滤器ServerListFilter

(3)LoadBalancer选择服务实例 的流程

四、Spring Cloud Ribbon源码解读

注:以上所有只做理论性的总结与分析,相关实战代码会在后面的博客中和github中逐步增加。

参考书籍、文献和资料:


一、Spring Cloud Feign概述与工作原理解读

(一)服务间调用的几种方式

使用Spring Cloud开发微服务时,在服务消费者调用服务提供者时,底层通过HTTP Client 的方式访问。但实际上在服务调用时,有主要以下来实现:

使用JDK原生的URLConnection;

Apache提供的HTTP Client;

Netty提供的异步HTTP Client;

Spring提供的RestTemplate。

Spring Cloud的Spring Cloud Open Feign相对是最方便与最优雅的,使Feign支持Spring MVC注解的同时并整合了Ribbon。

(二)Feign 概述

Feign 是一个声明式的 Web Service 客户端。它的出现使开发 Web Service 客户端变得很简单。使用 Feign 只需要创建一个接口加上对应的注解,比如:@FeignClient 注解。 Feign 有可插拔的注解,包括 Feign 注解和 AX-RS 注解。Feign 也支持编码器和解码器,Spring Cloud Open Feign 对 Feign 进行增强支持 Spring Mvc 注解,可以像 Spring Web 一样使用 HttpMessageConverters 等。

Feign 是一种声明式、模板化的 HTTP 客户端。在 Spring Cloud 中使用 Feign,可以做到使用 HTTP 请求访问远程服务,就像调用本地方法一样的,开发者完全感知不到这是在调用远程方法,更感知不到在访问 HTTP 请求。接下来介绍一下 Feign 的特性,具体如下:

  • 可插拔的注解支持,包括 Feign 注解和AX-RS注解。
  • 支持可插拔的 HTTP 编码器和解码器。
  • 支持 Hystrix 和它的 Fallback。
  • 支持 Ribbon 的负载均衡。
  • 支持 HTTP 请求和响应的压缩。Feign 是一个声明式的 WebService 客户端,它的目的就是让 Web Service 调用更加简单。它整合了 Ribbon 和 Hystrix,从而不需要开发者针对 Feign 对其进行整合。Feign 还提供了 HTTP 请求的模板,通过编写简单的接口和注解,就可以定义好 HTTP 请求的参数、格式、地址等信息。Feign 会完全代理 HTTP 的请求,在使用过程中我们只需要依赖注入 Bean,然后调用对应的方法传递参数即可。

(三)Feign 工作原理

  1. 在开发微服务应用时,我们会在主程序入口添加 @EnableFeignClients 注解开启对 Feign Client 扫描加载处理。根据 Feign Client 的开发规范,定义接口并加 @FeignClients 注解。
  2. 当程序启动时,会进行包扫描,扫描所有 @FeignClients 的注解的类,并将这些信息注入 Spring IOC 容器中。当定义的 Feign 接口中的方法被调用时,通过JDK的代理的方式,来生成具体的 RequestTemplate。当生成代理时,Feign 会为每个接口方法创建一个 RequetTemplate 对象,该对象封装了 HTTP 请求需要的全部信息,如请求参数名、请求方法等信息都是在这个过程中确定的。
  3. 然后由 RequestTemplate 生成 Request,然后把 Request 交给 Client 去处理,这里指的 Client 可以是 JDK 原生的 URLConnection、Apache 的 Http Client 也可以是 Okhttp。最后 Client 被封装到 LoadBalanceclient 类,这个类结合 Ribbon 负载均衡发起服务之间的调用。

二、FeignClent注解剖析+Spring Cloud Feign基本功能配置解读

(一)@FeignClient 注解剖析

FeignClient注解被@Target(ElementType.TYPE)修饰,表示FeignClient注解的作用目标在接口上。

声明接口之后,在代码中通过@Resource注入之后即可使用。@FeignClient标签的常用属性如下:

  • name:指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现
  • url: url一般用于调试,可以手动指定@FeignClient调用的地址
  • decode404:当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
  • configuration: Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract
  • fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
  • fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
  • path: 定义当前FeignClient的统一前缀

(二)Spring Cloud Feign基本功能配置

所谓的基本功能配置主要是指可以自定义Feign的配置,相关代码会在后续博客和github中更新。

  1. 日志配置(后续博客和github中更新)
  2. 契约配置(后续博客和github中更新)
  3. Basic认证配置(后续博客和github中更新)
  4. 超时时间配置(后续博客和github中更新)
  5. 客户端组件配置(后续博客和github中更新)
  6. GZIP压缩配置(后续博客和github中更新)
  7. 编码器解码器配置(后续博客和github中更新)
  8. Feign默认Client的替换配置(后续博客和github中更新)
  9. Feign返回图片流处理方式(后续博客和github中更新)
  10. Feign调用传递Token(后续博客和github中更新)
  11. venus-cloud-feign的设计和使用(后续博客和github中更新)

(三)Feign请求超时问题

Hystrix默认的超时时间是1秒,如果超过这个时间尚未响应,将会进入fallback代码。而首次请求往往会比较慢(因为Spring的懒加载机制,要实例化一些类),这个响应时间可能就大于1秒了
解决方案有三种,以feign为例。

方法一

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000
该配置是让Hystrix的超时时间改为5秒

方法二

hystrix.command.default.execution.timeout.enabled: false
该配置,用于禁用Hystrix的超时时间

方法三

feign.hystrix.enabled: false
该配置,用于索性禁用feign的hystrix。该做法除非一些特殊场景,不推荐使用。

三、Spring Cloud Ribbon概述与核心工作原理

Ribbon是2013年1月Netflix公司开发的一个组件,它在云服务体系中起着至关重要的作用,一直是Netflix最活跃的项目。

Pivotal公司将其整入了Spring Cloud生态,正式命名为Spring Cloud Ribbon作为微服务弹性扩展的组件,与其他组件结合发挥强大作用。(Pivotal中国研发中心于2013.04成立)

(一)Ribbon与负载均衡

负载均衡在业界有不少分类:(基本可见https://blog.csdn.net/xiaofeng10330111/article/details/85682513

最常见的有软负载和硬负载,代表产品为nginx和F5.

另外一组分类为集中式负载和进程内负载,即服务端负载均衡和客户端负载均衡。这种分类下,nginx和F5都为集中式负载,Ribbon为进程内负载。

Ribbon是Spring Cloud框架中相当核心的模块,负责着服务负载调用,Ribbon也可以脱离SpringCloud单独使用。
另外Ribbon是客户端的负载均衡框架,即每个客户端上,独立维护着自身的调用信息统计,相互隔离;也就是说:Ribbon的负载均衡表现在各个机器上变现并不完全一致
Ribbon 也是整个组件框架中最复杂的一环,控制流程上为了保证服务的高可用性,有很多比较细节的参数控制,在使用的过程中,需要深入理清每个环节的处理机制,这样在问题定位上会高效很多。

(二)Ribbon核心工作原理                      

微服务架构-实现技术之具体实现工具与框架5:Spring Cloud Feign与Ribbon原理与注意事项

Spring Cloud集成模式下的Ribbon有以下几个特征:

1.Ribbon 服务配置方式

每一个服务配置都有一个Spring ApplicationContext上下文,用于加载各自服务的实例。
比如,当前Spring Cloud 系统内,有如下几个服务:

服务名称 角色 依赖服务
order  订单模块 user
user 用户模块
mobile-bff 移动端BFF order,user

mobile-bff服务在实际使用中,会用到order和user模块,那么在mobile-bff服务的Spring上下文中,会为order 和user 分别创建一个子ApplicationContext,用于加载各自服务模块的配置。也就是说,各个客户端的配置相互独立,彼此不收影响

2.和Feign的集成模式

在使用Feign作为客户端时,最终请求会转发成 http://<服务名称>/<relative-path-to-service>的格式,通过LoadBalancerFeignClient, 提取出服务标识<服务名称>,然后根据服务名称在上下文中查找对应服务的负载均衡器FeignLoadBalancer,负载均衡器负责根据既有的服务实例的统计信息,挑选出最合适的服务实例。

(三)LoadBalancer–负载均衡器的核心

LoadBalancer 的职能主要有三个:

  • 维护Sever列表的数量(新增、更新、删除等)
  • 维护Server列表的状态(状态更新)
  • 当请求Server实例时,能否返回最合适的Server实例

1.负载均衡器的内部基本实现原理    

微服务架构-实现技术之具体实现工具与框架5:Spring Cloud Feign与Ribbon原理与注意事项

 

微服务架构-实现技术之具体实现工具与框架5:Spring Cloud Feign与Ribbon原理与注意事项

 

  • Server  

Server 作为服务实例的表示,会记录服务实例的相关信息,如:服务地址,所属zone,服务名称,实例ID等    

  • ServerList    

维护着一组Server实例列表,在应用运行的过程中,Ribbon通过ServerList中的服务实例供负载均衡器选择。ServerList维护列表可能在运行的过程中动态改变   

  • ServerStats    

作为对应Server 的运行情况统计,一般是服务调用过程中的Server平均响应时间,累计请求失败计数,熔断时间控制等。一个ServerStats实例唯一对应一个Server实例    

  • LoadBalancerStats    

作为 ServerStats实例列表的容器,统一维护    

  • ServerListUpdater    

负载均衡器通过ServerListUpdater来更新ServerList,比如实现一个定时任务,每隔一段时间获取最新的Server实例列表 

  • Pinger    

服务状态检验器,负责维护ServerList列表中的服务状态注意:Pinger仅仅负责Server的状态,没有能力决定是否删除    

  • PingerStrategy    

定义以何种方式还检验服务是否有效,比如是按照顺序的方式还是并行的方式    

  • IPing    

Ping,检验服务是否可用的方法,常见的是通过HTTP,或者TCP/IP的方式看服务有无认为正常的请求    

2.如何维护Server列表?(新增、更新、删除)             

微服务架构-实现技术之具体实现工具与框架5:Spring Cloud Feign与Ribbon原理与注意事项

Server列表的维护从实现方法上分为两类:

  • 基于配置的服务列表

这种方式一般是通过配置文件,静态地配置服务器列表,这种方式相对而言比较简单,但并不是意味着在机器运行的时候就一直不变。netflix 在做Spring cloud 套件时,使用了分布式配置框架netflix archaius ,archaius 框架有一个特点是会动态的监控配置文件的变化,将变化刷新到各个应用上。也就是说,当我们在不关闭服务的情况下,如果修改了基于配置的服务列表时, 服务列表可以直接刷新

  • 结合服务发现组件(如Eureka)的服务注册信息动态维护服务列表

基于Spring Cloud框架下,服务注册和发现是一个分布式服务集群必不可少的一个组件,它负责维护不同的服务实例(注册、续约、取消注册)。

3.负载均衡器如何维护服务实例的状态?

Ribbon负载均衡器将服务实例的状态维护托交给Pinger、 PingerStrategyIPing 来维护,具体交互模式如下所示:              

微服务架构-实现技术之具体实现工具与框架5:Spring Cloud Feign与Ribbon原理与注意事项

4.如何从服务列表中挑选一个合适的服务实例?

(1)服务实例容器:ServerList的维护

负载均衡器通过 ServerList来统一维护服务实例,具体模式如上图,基础的接口定义如下:

/**
 * Interface that defines the methods sed to obtain the List of Servers
 * @author stonse
 *
 * @param <T>
 */
public interface ServerList<T extends Server> {
    //获取初始化的服务列表
    public List<T> getInitialListOfServers();
    
    /**
     * Return updated list of servers. This is called say every 30 secs
     * (configurable) by the Loadbalancer's Ping cycle
     * 获取更新后的的服务列表
     */
    public List<T> getUpdatedListOfServers();   

}

   在Ribbon的实现中,在ServerList(负责维护服务实例,并使用ServerListFilter过滤器过滤出符合要求的服务实例列表List<Server>)中,维护着Server的实例,并返回最新的List<Server>集合,供LoadBalancer使用   。               

微服务架构-实现技术之具体实现工具与框架5:Spring Cloud Feign与Ribbon原理与注意事项

(2) 服务实例列表过滤器ServerListFilter

传入一个服务实例列表,过滤出满足过滤条件的服务列表

public interface ServerListFilter<T extends Server> {
    public List<T> getFilteredListOfServers(List<T> servers);
}
  • Ribbon 的默认ServerListFilter实现1:ZoneAffinityServerListFilter

Ribbon默认采取了区域优先的过滤策略,即当Server列表中,过滤出和当前实例所在的区域(zone)一致的server列表

  • Ribbon 的ServerListFilter实现2:ZonePreferenceServerListFilter

ZonePreferenceServerListFilter 集成自 ZoneAffinityServerListFilter,在此基础上做了拓展,在 返回结果的基础上,再过滤出和本地服务相同区域(zone)的服务列表。

当指定了当前服务的所在Zone,并且 ZoneAffinityServerListFilter 没有起到过滤效果时,ZonePreferenceServerListFilter会返回当前Zone的Server列表。

  • Ribbon 的ServerListFilter实现3:ServerListSubsetFilter

这个过滤器作用于当Server数量列表特别庞大时(比如有上百个Server实例),这时,长时间保持Http链接也不太合适,可以适当地保留部分服务,舍弃其中一些服务,这样可使释放没必要的链接。
此过滤器也是继承自 ZoneAffinityServerListFilter,在此基础上做了拓展,在实际使用中不太常见。

(3)LoadBalancer选择服务实例 的流程

LoadBalancer的核心功能是根据负载情况,从服务列表中挑选最合适的服务实例。LoadBalancer内部采用了如下图所示的组件完成:

微服务架构-实现技术之具体实现工具与框架5:Spring Cloud Feign与Ribbon原理与注意事项

LoadBalancer 选择服务实例的流程

  • 通过ServerList获取当前可用的服务实例列表;
  • 通过ServerListFilter将步骤1 得到的服务列表进行一次过滤,返回满足过滤器条件的服务实例列表;
  • 应用Rule规则,结合服务实例的统计信息,返回满足规则的某一个服务实例;

通过上述的流程可以看到,实际上,在服务实例列表选择的过程中,有两次过滤的机会:第一次是首先通过ServerListFilter过滤器,另外一次是用过IRule 的选择规则进行过滤。

四、Spring Cloud Ribbon源码解读

既然是在restTemplate加了@LoadBalanced注解,那就进去这个注解里面看下吧。

/**
 * 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 {
}

从注释中可以知道,这个注解是用来给RestTemplate做标记,以使用负载均衡客户端(LoadBalancerClient)来配置它。所以,我们在生成的RestTemplate的bean上添加这么一个注解,这个bean就会配置LoadBalancerClient。LoadBalancerClient的代码如下:

/**
 * Represents a client side load balancer
 * @author Spencer Gibb
 */
public interface LoadBalancerClient {
    /**
     * Choose a ServiceInstance from the LoadBalancer for the specified service
     * @param serviceId the service id to look up the LoadBalancer
     * @return a ServiceInstance that matches the serviceId
     */
    ServiceInstance choose(String serviceId);

    /**
     * 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;

    /**
     * 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是一个接口,里面有三个方法。 
第一个,ServiceInstance choose(String serviceId);从方法名上就可以看出,是根据传入的serviceId(服务名),从负载均衡器中选择一个服务实例,服务实例通过ServiceInstance类来表示。 
第二个,execute方法,使用从负载均衡器中选择的服务实例来执行请求内容。 
第三个,URI reconstructURI(ServiceInstance instance, URI original);方法,是重新构建一个URI的,还记得我们在代码中,通过RestTemplate请求服务时,写的是服务名吧,这个方法就会把这个请求的URI进行转换,返回host+port,通过host+port的形式去请求服务。 
从工程中搜索LoadBalancerClient接口的实现类,可以找到RibbonLoadBalancerClient类实现了这个接口,并且实现了接口中定义的方法。

再梳理一下逻辑,我们在RestTemplate上添加了@LoadBalanced注解,RibbonLoadBalancerClient就会配置到这个RestTemplate实例上。

在LoadBalancerClient接口的同一个包路径下,还会看到另一个LoadBalancerAutoConfiguration类,看名字就感觉这是一个自动配置LoadBalancer的,进去这个类看一下。

/**
 * Auto configuration for Ribbon (client side load balancing).
 *
 * @author Spencer Gibb
 * @author Dave Syer
 */
@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.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 : LoadBalancerAutoConfiguration.this.restTemplates) {
                    for (RestTemplateCustomizer customizer : customizers) {
                        customizer.customize(restTemplate);
                    }
                }
            }
        };
    }
@Bean
    @ConditionalOnMissingBean
    public RestTemplateCustomizer restTemplateCustomizer(
            final LoadBalancerInterceptor loadBalancerInterceptor) {
        return new RestTemplateCustomizer() {
            @Override
            public void customize(RestTemplate restTemplate) {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                        restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            }
        };
    }

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

}

注释中说明这个类是为Ribbon做自动配置的,类上的@Configuration说明这是一个配置类,在当前项目中存在RestTemplate类、并且存在LoadBalancerClient接口的实现类时,就满足了自动化配置的条件。 
在LoadBalancerAutoConfiguration类中,创建了一个LoadBalancerInterceptor拦截器,还维护了一个被@LoadBalanced修饰的RestTemplate列表,在初始化的时候,会为每个restTemplate实例添加LoadBalancerInterceptor拦截器。 
我们自己实现的项目,就定义了RestTemplate的一个对象,并且引入了spring-cloud相关的包,存在RibbonLoadBalancerClient作为LoadBalancerClient的实现类,所以,满足自动化配置的条件。接下来就看下,在restTemplate实例添加的LoadBalancerInterceptor拦截器的逻辑。

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 ServiceRequestWrapper(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;
        }

    }

}

由于在自动配置类中,对restTemplate实例添加了LoadBalancerInterceptor拦截器,所以,当用restTemplate发送http请求时,就会执行这个拦截器的intercept方法。 
intercept方法中,会根据request.getURI(),获取请求的uri,再获取host,我们在发送http请求的时候,是用的服务名作为host,所以,这里就会拿到服务名,再调用具体LoadBalancerClient实例的execute方法,发送请求。 
LoadBalancerClient的实现类为RibbonLoadBalancerClient,最终的负载均衡请求由它来执行,所以,还需要再梳理下RibbonLoadBalancerClient的逻辑。

先看下RibbonLoadBalancerClient中的execute方法:

@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));

    RibbonLoadBalancerContext context = this.clientFactory
            .getLoadBalancerContext(serviceId);
    RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);

    try {
        T returnVal = request.apply(ribbonServer);
        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;
}

服务名作为serviceId字段传进来,先通过getLoadBalancer获取loadBalancer,再根据loadBalancer获取server,下面是getServer的代码:

protected Server getServer(ILoadBalancer loadBalancer) {
    if (loadBalancer == null) {
        return null;
    }
    return loadBalancer.chooseServer("default"); // TODO: better handling of key
}

如果loadBalancer为空,就直接返回空,否则就调用loadBalancer的chooseServer方法,获取相应的server。 
看一下ILoadBalancer是一个接口,里面声明了一系列负载均衡实现的方法:

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

这里面还有一个getServerList方法,不过已经标记为Deprecated,所以就没有列出。 
这些方法名比较直观,很容易就能猜出是干啥的,addServers是用来添加一个server集合,chooseServer是选择一个server,markServerDown用来标记某个服务下线,getReachableServers获取可用的Server集合,getAllServers是获取所有的server集合。 
ILoadBalancer有很多实现,那具体是用的哪个类呢,可以通过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;
}

ZoneAwareLoadBalancer从名字中可以看出来,这个负载均衡器和zone是有关系的。下面看下ZoneAwareLoadBalancer中的chooseServer方法:

@Override
public Server chooseServer(Object key) {
    if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
        logger.debug("Zone aware logic disabled or there is only one zone");
        return super.chooseServer(key);
    }
    Server server = null;
    try {
        LoadBalancerStats lbStats = getLoadBalancerStats();
        Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
        logger.debug("Zone snapshots: {}", zoneSnapshot);
        if (triggeringLoad == null) {
            triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
                    "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
        }

        if (triggeringBlackoutPercentage == null) {
            triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
                    "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
        }
        Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
        logger.debug("Available zones: {}", availableZones);
        if (availableZones != null &&  availableZones.size() < zoneSnapshot.keySet().size()) {
            String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
            logger.debug("Zone chosen: {}", zone);
            if (zone != null) {
                BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
                server = zoneLoadBalancer.chooseServer(key);
            }
        }
    } catch (Throwable e) {
        logger.error("Unexpected exception when choosing server using zone aware logic", e);
    }
    if (server != null) {
        return server;
    } else {
        logger.debug("Zone avoidance logic is not invoked.");
        return super.chooseServer(key);
    }
}

这个方法会根据server的zone和可用性来选择具体的实例,返回一个Server对象。

通过ZoneAwareLoadBalancer选择具体的Server之后,再包装成RibbonServer对象,之前返回的server是该对象中的一个字段,除此之外,还有服务名serviceId,是否需要使用https等信息。最后,通过LoadBalancerRequest的apply方法,向具体的server发请求,从而实现了负载均衡。 
下面是apply方法的定义:

public interface LoadBalancerRequest<T> {
    public T apply(ServiceInstance instance) throws Exception;
}

在请求时,传入的ribbonServer对象,被当成ServiceInstance类型的对象进行接收。ServiceInstance是一个接口,定义了服务治理系统中,每个实例需要提供的信息,比如serviceId,host,port等。 
LoadBalancerRequest是一个接口,最终会通过实现类的apply方法去执行,实现类是在LoadBalancerInterceptor中调用RibbonLoadBalancerClient的execute方法时,传进来的一个匿名类,可以通过查看LoadBalancerInterceptor的代码看到。 
创建LoadBalancerRequest匿名类的时候,就重写了apply方法,apply方法中,还新建了一个ServiceRequestWrapper的内部类,这个类中,就重写了getURI方法,getURI方法会调用loadBalancer的reconstructURI方法来构建uri。

看到这里,已经可以大体知道Ribbon实现负载均衡的流程了,我们在RestTemplate上添加注解,就会有LoadBalancerClient的对象来配置它,也就是RibbonLoadBalancerClient。同时,LoadBalancerAutoConfiguration会进行配置,创建一个LoadBalancerInterceptor,并且拿到我们声明的所有restTemplate,在这些restTemplate中添加LoadBalancerInterceptor拦截器。 
当通过restTemplate发送请求时,就会经过这个拦截器,在拦截器中,就会调用RibbonLoadBalancerClient中的方法,获取到根据服务名,通过负载均衡方法获取到服务实例,然后去请求这个实例。 
上面说的这些,是如何对请求进行负载均衡的,但是还有个问题,我们请求的实例,是从Eureka Server上获取到的,那这个实例列表是如何获取的呢?怎么保证这个实例列表中的实例是可用的呢?

在RibbonLoadBalancerClient选择实例的时候,是通过ILoadBalancer的实现类根据负载均衡算法选择服务实例的,也就是ZoneAwareLoadBalancer的chooseServer中的逻辑,那就在这里找线索。查看ZoneAwareLoadBalancer的继承关系,可以看到如下图所示。 

微服务架构-实现技术之具体实现工具与框架5:Spring Cloud Feign与Ribbon原理与注意事项

可以看到,最上面是ILoadBalancer接口,AbstractLoadBalancer类继承了这个接口,BaseLoadBalancer继承了AbstractLoadBalancer类,DynamicServerListLoadBalancer继承了BaseLoadBalancer,ZoneAwareLoadBalancer继承了DynamicServerListLoadBalancer。

ILoadBalancer接口的代码已经看过了,现在看下AbstractLoadBalancer的代码:

public abstract class AbstractLoadBalancer implements ILoadBalancer {

    public enum ServerGroup{
        ALL,
        STATUS_UP,
        STATUS_NOT_UP        
    }

    /**
     * delegate to {@link #chooseServer(Object)} with parameter null.
     */
    public Server chooseServer() {
        return chooseServer(null);
    }

    /**
     * List of servers that this Loadbalancer knows about
     * 
     * @param serverGroup Servers grouped by status, e.g., {@link ServerGroup#STATUS_UP}
     */
    public abstract List<Server> getServerList(ServerGroup serverGroup);

    /**
     * Obtain LoadBalancer related Statistics
     */
    public abstract LoadBalancerStats getLoadBalancerStats();    
}

这是一个抽象类,里面加了一个枚举,增加了两个抽象方法。定义的chooseServer方法。

下面再看BaseLoadBalancer类,BaseLoadBalancer类就算是负载均衡器的一个基础实现类,在里面可以看到定义了两个list:

@Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> allServerList = Collections
        .synchronizedList(new ArrayList<Server>());
@Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL)
protected volatile List<Server> upServerList = Collections
        .synchronizedList(new ArrayList<Server>());

从名字上看,这就是维护所有服务的实例列表,和维护状态为up的实例列表。 
而且还可以看到BaseLoadBalancer中实现的ILoadBalancer接口中的方法,比如下面这两个,获取可用的服务列表,就会把upServerList返回,获取所有的服务列表,就会把allServerList返回。

@Override
public List<Server> getReachableServers() {
    return Collections.unmodifiableList(upServerList);
}

@Override
public List<Server> getAllServers() {
    return Collections.unmodifiableList(allServerList);
}

接下来,再看DynamicServerListLoadBalancer类。从类头上的注释可以知道,这个类可以动态的获取服务列表,并且利用filter对服务列表进行过滤。

在DynamicServerListLoadBalancer类中,能看到定义了一个ServerList类型的serverListImpl字段,ServerList是一个接口,里面有两个方法:

public interface ServerList<T extends Server> {

    public List<T> getInitialListOfServers();

    /**
     * Return updated list of servers. This is called say every 30 secs
     * (configurable) by the Loadbalancer's Ping cycle
     * 
     */
    public List<T> getUpdatedListOfServers();   

}

getInitialListOfServers是获取初始化的服务列表。 
getUpdatedListOfServers是获取更新的服务列表。 
ServerList有多个实现类,具体用的哪个呢,可以在EurekaRibbonClientConfiguration类中找到,这是Ribbon和Eureka结合的自动配置类,这里面有个方法:

@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config) {
    DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
            config);
    DomainExtractingServerList serverList = new DomainExtractingServerList(
            discoveryServerList, config, this.approximateZoneFromHostname);
    return serverList;
}

方法中先新建了一个DiscoveryEnabledNIWSServerList类型的对象,又把这个对象作为一个参数,创建了DomainExtractingServerList类型的对象,最终返回的是DomainExtractingServerList的实例对象。 
查看DomainExtractingServerList中重写的这两个方法,发现还是调用的DiscoveryEnabledNIWSServerList中的方法。然后,进到DiscoveryEnabledNIWSServerList类中,看这两个方法的定义:

@Override
public List<DiscoveryEnabledServer> getInitialListOfServers(){
    return obtainServersViaDiscovery();
}

@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
    return obtainServersViaDiscovery();
}

这两个方法,都是调用了obtainServersViaDiscovery这个方法:

private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
    List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();

    if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
        logger.warn("EurekaClient has not been initialized yet, returning an empty list");
        return new ArrayList<DiscoveryEnabledServer>();
    }

    EurekaClient eurekaClient = eurekaClientProvider.get();
    if (vipAddresses!=null){
        for (String vipAddress : vipAddresses.split(",")) {
            // if targetRegion is null, it will be interpreted as the same region of client
            List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
            for (InstanceInfo ii : listOfInstanceInfo) {
                if (ii.getStatus().equals(InstanceStatus.UP)) {

                    if(shouldUseOverridePort){
                        if(logger.isDebugEnabled()){
                            logger.debug("Overriding port on client name: " + clientName + " to " + overridePort);
                        }

                        // copy is necessary since the InstanceInfo builder just uses the original reference,
                        // and we don't want to corrupt the global eureka copy of the object which may be
                        // used by other clients in our system
                        InstanceInfo copy = new InstanceInfo(ii);

                        if(isSecure){
                            ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build();
                        }else{
                            ii = new InstanceInfo.Builder(copy).setPort(overridePort).build();
                        }
                    }
                    DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, isSecure, shouldUseIpAddr);
                    des.setZone(DiscoveryClient.getZone(ii));
                    serverList.add(des);
                }
            }
            if (serverList.size()>0 && prioritizeVipAddressBasedServers){
                break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers
            }
        }
    }
    return serverList;
}

在这个方法中,就是通过eurekaClient去注册中心获取服务,将状态为up的服务实例封装成DiscoveryEnabledServer对象,然后放入列表返回,这就是获取服务列表的流程。 
获取服务列表的流程知道了,那是如何触发去获取,如何更新服务列表的呢?再看DynamicServerListLoadBalancer类中的代码,有一段:

protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
    @Override
    public void doUpdate() {
        updateListOfServers();
    }
};

ServerListUpdater是一个接口,在DynamicServerListLoadBalancer的构造函数中,创建了一个PollingServerListUpdater类的对象,为ServerListUpdater字段赋值。进入PollingServerListUpdater类看一下:

@Override
public synchronized void start(final UpdateAction updateAction) {
    if (isActive.compareAndSet(false, true)) {
        final Runnable wrapperRunnable = new Runnable() {
            @Override
            public void run() {
                if (!isActive.get()) {
                    if (scheduledFuture != null) {
                        scheduledFuture.cancel(true);
                    }
                    return;
                }
                try {
                    updateAction.doUpdate();
                    lastUpdated = System.currentTimeMillis();
                } catch (Exception e) {
                    logger.warn("Failed one update cycle", e);
                }
            }
        };

        scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                wrapperRunnable,
                initialDelayMs,
                refreshIntervalMs,
                TimeUnit.MILLISECONDS
        );
    } else {
        logger.info("Already active, no-op");
    }
}

里面有个start方法,实现了Runnable接口,run方法里调用UpdateAction的doUpdate,之后再启动一个定时任务,执行这个方法。定时任务传入的两个时间参数:initialDelayMs和refreshIntervalMs,任务启动后一秒开始执行,并且每隔三十秒执行一次,用于刷新列表。

看到这里,就可以大体了解了,构造DynamicServerListLoadBalancer实例的时候,就会启动一个定时任务了,一开始先获取服务列表,之后每隔三十秒获取一次。负载均衡时,就是通过负载均衡算法在实例列表中选择一个,发送请求。

以上这就是Ribbon源码大体的流程。
 

注:以上所有只做理论性的总结与分析,相关实战代码会在后面的博客中和github中逐步增加。

参考书籍、文献和资料:

【1】郑天民. 微服务设计原理与架构. 北京:人民邮电出版社,2018.

【2】徐进,叶志远,钟尊发,蔡波斯等. 重新定义Spring Cloud. 北京:机械工业出版社. 2018.

【3】https://blog.csdn.net/wo18237095579/article/details/83343915.

【4】https://blog.csdn.net/a15514920226/article/details/78924483.

【5】https://blog.csdn.net/xiaofeng10330111/article/details/85682513.

【6】https://blog.csdn.net/luanlouis/article/details/83060310.

【7】https://blog.csdn.net/chayangdz/article/details/82177917.

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/131106.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

  • 五个步骤教你数据清洗_数据仓库ods层

    五个步骤教你数据清洗_数据仓库ods层关于ODS层是否做数据清洗一直是存在争议的,但有一点是可以确定的,对于比较重的清洗工作是要留到后面数仓的ETL过程中进行处理。但是,有这么一种情况:我们在长期的生产实际过程中,发现部分已知的数据问题的处理可以通过自动化的方式来处理,这种方式通常在数据入库之前,做额外的加工处理后再做入库操作。数据清洗的主要工作是处理那些不符合要求的数据,从而提升数据质量,比如一些常见的问题:错误的数据、重复的数据错误的数据这种错误通常是业务系统处理不够健全造成的,比如字符串数据后面有回车空格、日期格式不正确、日期

  • pycharm安装及配置教程_pycharm配置环境变量

    pycharm安装及配置教程_pycharm配置环境变量目录一、安装1.下载2.安装二、基本配置三、增加解释器四、字体大小调整五、常用快捷键一、安装1.下载pycharm下载地址:http://www.jetbrains.com/pych…

  • openGL研究钞四 : 关于颜色, 尺寸, 虚线, 多边形逆转, 空洞, 使用位图

    openGL研究钞四 : 关于颜色, 尺寸, 虚线, 多边形逆转, 空洞, 使用位图

  • SQL like 模糊查询

    SQL like 模糊查询SQL模糊查询在进行数据库查询时,有完整查询和模糊查询之分。一般模糊查询语句如下:SELECT字段FROM表WHERE某字段Like条件其中关于条件,SQL提供了四种匹配模式:1,% :表示任意0个或多个字符。可匹配任意类型和长度的字符,有些情况下若是中文,请使用两个百分号(%%)表示。

  • 基于Tess4j的图片识别

    基于Tess4j的图片识别Tess4J是对TesseractOCRAPI的JavaJNA封装。tesseract是跨平台的OCR(OpticalCharacterRecognition,光学字符识别)引擎,让开发者非常容易的集成OCR能力到他们自己的应用。通过强大的API从图片中识别和提取文本内容。Tess4J支持主流的图片格式,如TIFF,JPEG,GIF,PNG,BMP,andPDF。…

  • minicom的使用

    minicom的使用一、安装sudoapt-getinstallminicom二、配置运行$sudominicom-s便进入了minicom的配置界面,使用上下键选择Serialportsetup,回车。此时光标在“changewhichsetting”后面停留,它的上面有如下菜单:Serialportsetup[Enter]±————————-…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号