天天看点

Spring Cloud Netflix 负载均衡组件—Ribbon 高级应用

上一部分,我们使用Ribbon通过Sping 提供的RestTemplate了解了Ribbon负载均衡的使用,那么是如何通过RestTemplate实现客户端的负载均衡的呢?本篇我们将详细分析Ribbon是如何使用RestTemplate进行负载均衡的,并且会介绍Ribbon的一些自定义的配置。

在Spring-Web中,我们使用RestTemplate调用远程服务时,不涉及负载均衡,URI只需要是IP+端口即可,而在Ribbon中则是服务名,因此Ribbon需要根据服务名获取要调用的服务的IP +端口地址。这里用到了Spring-Web提供的RestTemplate的拦截器ClientHttpRequestInterceptor,在请求之前对请求进行拦截,可以改变要请求的地址。如下类图为ClientHttpRequestInterceptor的具体实现

Spring Cloud Netflix 负载均衡组件—Ribbon 高级应用

在上面的类中其中LoadBalanceInterceptor和RetryLoadBalanceInterceptor为Ribbon为我们提供的两个拦截器,一个用于负载均衡,一个用于请求重试,我们可以先看源码:LoadBalanceInterceptor。

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
//Ribbon客户端
    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(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
//获取请求的URL
        URI originalUri = request.getURI();
//获取请求的服务名称
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
//使用Ribbon执行调用
        return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
    }
}
           

 通过上面的代码我们看出请求最终交给LoadBalanceClient实例去执行,LoadBalanceClient是Ribbon提供的负载均衡客户端,很多重要的逻辑都在这里处理。如下为LoadBalanceClient的类图:

Spring Cloud Netflix 负载均衡组件—Ribbon 高级应用

我们以RibbonLoadBalancerClient为例讲解,在LoadBalanceInterceptor中调用了LoadBalanceClient的execute方法,如下为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);
    }
}
           

 在上面的代码中我们根据serviceId获取一个负载均衡器,然后根据负载均衡器获取一个Server,后续的请求会请求到该Server,Server实例中包含主机和端口字段。我们所要关注的就是通过负载均衡器获取Server的过程。如下为ILoadBalancer的实现:

Spring Cloud Netflix 负载均衡组件—Ribbon 高级应用

 如上所示为ILoadBalancer的所有实现类,其中NoOpLoadBalancer为不需要负载均衡,这里不做讲解。然后我们看ILoadBalancer的其他实现。如上所示为ILoadBalancer的所有实现类,其中NoOpLoadBalancer为不需要负载均衡,这里不做讲解。然后我们看ILoadBalancer的其他实现。AbstractLoadBalancer是ILoadBalancer接口的抽象实现。在该抽象类中定义了一个关于服务实例的分组枚举类 ServerGroup, 它包含以下三种不同类型。ALL: 所有服务实例;STATUS_UP: 正常服务的实例;STATUS_NOT_UP: 停止服务的实例。我们选择其中的BaseLoadBalancer查看源码,它是其他负均衡组件的基础实现。源码如下所示:

public Server chooseServer(Object key) {
        if (this.counter == null) {
            this.counter = this.createCounter();
        }、
        this.counter.increment();
        if (this.rule == null) {
            return null;
        } else {
            try {
                //通负载均衡规则,返回一个地址
                return this.rule.choose(key);
            } catch (Exception var3) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", new Object[]{this.name, key, var3});
                return null;
            }
        }
    }
           

上面的代码也是选择一个地址,但是负载均衡组件不是自己去选择而是交给一个接口为IRule的负载均衡规则去选择一个地址,它默认的负载均衡规则为RoundRobinRule模式。如下为Ribbon提供的负载均衡规则实现:

Spring Cloud Netflix 负载均衡组件—Ribbon 高级应用

上面的类图是IRule负载均衡策略的实现,下面将会上面常用的实现进行简单的讲解。下面的介绍仅仅简单的介绍了一下实现原理,详细实现可以自行阅读源码。

Random Rule :该策略实现了从服务实例清单中随机选择一个服务实例的功能。从具体的实现上看,它会使用传入的负载均衡器来获得可用实例列表upList和所有实例列表allList, 并通过rand.nextint(serverCoun七)函数来获取 一个 随机数,并将该随机数作为 upList 的索引值来返回具体实例。

RoundRobinRule:该策略实现了按照线性轮询的方式依次选择每个服务实例的功能。 它的具体实现如下,RandomRule 非常类似。 除了循环条件不同外, 就是从可用列表中获取所谓的逻辑不同。 从循环条件中, 它增加了一个 count计数变量, 该变量会在每次循环之后累加, 也就是说, 如果一直选择不到 server 超过 10 次, 那么就会结束尝试, 并打印一个警告信息 No available alive servers after 10 tries from load balancer: ...。而线性轮询的实现则是通过Atomicinteger nextServerCyclicCounter对象实现,每次进行实例选择时通过调用 incrementAndGetModulo 函数实现递增。

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

Weighted ResponseTimeRul:该策略是对 RoundRobinRule 的扩展, 增加了根据实例的运行情况来计算权重,并根据权重来挑选实例, 以达到更优的分配效果,具体实现这里不在解释,可以查看源码自行了解。

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

PredicateBasedRule:这是一个抽象策略它也继承了 ClientConfigEnabledRoundRobinRule, 它是一个基千 Predicate 实现的策略 Predicate 是 Google Guava Collection 工具对集合进行过滤的条件接口。先通过子类中实现的 Predicate 逻辑来过滤一部分服务实例, 然后再以线性轮询的方式从过滤后的实例清单中选出一个。具体实现可以查看源码

AvailabilityFilteringRul:该策略继承自上面介绍的抽象策略 PredicateBasedRule, 所以它也继承了“先过滤清单,再轮询选择”的基本处理逻辑,其中过滤条件使用了 AvaliabilityPredicate。

ZoneAvoidanceRule 它也是PredicateBasedRule的具体实现类。它使用了 CompositePredicate 来进行服务实例清单的过滤。这是一个组合过滤条件,在其构造函数中,它以ZoneAvoidancePredicate 为主过滤条件,AvailabilityPredicate为次过滤条件初始化了组合过滤条件的实例。

Ribbon配置

前面对Ribbon的负载均衡策略做了简单的讲解,介绍了几种负载均衡策略,而作为Ribbon的配置,自然也是对负载均衡策略的配置,Ribbon提供了两种方式对Ribbon进行配置,一种是以代码的形式,一种是以配置文件的形式,在介绍之前我们先看一下要配制的有哪些内容:IRule: Ribbon 的负载均衡策略;IPing: Ribbon的实例检查策略;ILoadBalancer: 负载均衡器;IClientConfig:Ribbon 的 客户端配置;ServerList<Server>: 服务实例清单的维护机制; ServerListFilter<Server>: 服务实例清单过滤机制。

一般我们需要配置的是IRule、IPing、和ILoadBalancer三个实例,我们只需要使用@Configuration注解类将实例注入即可。代码如下所示:

@Configuration
public class IRuleConfig {
    @Bean
    public IRule iRule() {
        return new RandomRule();
    }
}
           

如上代码所示,我们配置了一个全局的负载均衡策略,所有调用都通过该策略负载,如果我们想要为某个服务做专门的策略则可以使用@RibbonClient注解,代码如下所示:

@Configuration
@RibbonClient(name = "ribbon-provider",configuration = IRuleConfig.class)
public class RibbonProviderIRuleConfig {
}
           

除了使用代码进行配置,在Camden版本之后还支持以配置文件的形式进行配置,可以直接通 过<clientName>.ribbon.<key>=<value>的形式进行配置。比如我们要实现与上面例子一样的配置(将ribbon-provider 服务客户端的IPing 接口实现替换为 PingUrl), 只需在application.properties配置中增加下面的内容即可:

ribbin-provider.ribbon.NFLoadBalancerPingClassName=com.netf让x.loadbalancer.PingUrl
           

除了配置IPing之外,还可以配置IRule,ILoadBalancer,ServerList,ServerListFilter等,他们的key分别为NFLoadBalancerRuleClassName,NFLoadBalancerClassName;NIWSServerListClassName,NIWSServerListFilterClassName.

本篇介绍了Ribbon的核心:负载均衡,包括负载均衡策略IRule以及负载均衡的配置。并没有将负载均衡策略源码贴到博客,有兴趣的可以自行阅读阅读源码。

继续阅读