Spring Cloud详解(三)Ribbon负载均衡原理

Ribbon 是一个客户端负载均衡器,赋予了应用一些支配 HTTP 与 TCP 行为的能力,由此可以得知,这里的客户端负载均衡也是进程内负载均衡的一种。 Ribbon 在 SpringCloud 生态内的不可缺少的组件,没有了 Ribbon,服务就不能横向扩展。Feign、Zuul 已经集成了 Ribbon。

1. Ribbon负载均衡策略

Ribbon 中提供了七种负载均衡策略

策略类命名描述
RandomRule随机策略随机选择 Server
RoundRobinRule轮询策略按照顺序循环选择 Server
RetryRule重试策略在一个配置时间段内,当选择的 Server 不成功,则一直尝试选择一个可用的 Server
BestAvailableRule最低并发策略逐个考察 Server,如果 Server 的断路器被打开,则忽略,在不被忽略的 Server 中选择并发连接最低的 Server
AvailabilityFilteringRule可用过滤测试过滤掉一直连接失败,并被标记未 circuit tripped(即不可用) 的 Server,过滤掉高并发的 Server
ResponseTimeWeightedRule响应时间加权策略根据 Server 的响应时间分配权重,响应时间越长,权重越低,被选择到的几率就越低
ZoneAvoidanceRule区域权衡策略综合判断 Server 所在区域的性能和 Server 的可用性轮询选择 Server,并判定一个 AWS Zone 的运行性能是否可用,剔除不可用的 Zone 中的所有 Server

Ribbon 默认的负载均衡策略是轮询策略

2. Ribbon配置

2.1 负载均衡策略配置

可以在配置文件中配置每个服务负载均衡策略

{instance-id}: # instance-id 即被调用服务名称
    ribbon:
         NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
    #    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #配置规则 随机
    #    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #配置规则 轮询
    #    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule #配置规则 重试
    #    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule #配置规则 响应时间权重
    #    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.BestAvailableRule #配置规则 最空闲连接策略

2.2 超时与重试配置

{instance-id}: # instance-id 指的是被调用者的服务名称
  ribbon:
    ConnectTimeout: 30000 # 链接超时时间
    ReadTimeout: 30000 # 读超时时间
    MaxAutoRetries: 1 # 对第一次请求的服务的重试次数
    MaxAutoRetriesNextServer: 1 # 要重试的下一个服务的最大数量(不包括第一个服务)
    OkToRetryOnAllOperations: true # 是否对 连接超时、读超时、写超时 都进行重试

2.3 饥饿加载

Ribbon 在进行负载均衡时,并不是启动时就加载上线文,而是在实际的请求发送时,才去请求上下文信息,获取被调用者的 ip、端口,这种方式在网络环境较差时,往往会使得第一次引起超时,导致调用失败。此时需要指定 Ribbon 客户端,进行饥饿加载,即:在启动时就加载好上下文。

ribbon:
  eager-load:
    enabled: true
    clients: spring-cloid-ribbon-provider

此时启动 consumer,会看到控制打印信息如下:

Client: spring-cloid-ribbon-provider instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=spring-cloid-ribbon-provider,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
Using serverListUpdater PollingServerListUpdater
DynamicServerListLoadBalancer for client spring-cloid-ribbon-provider initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=spring-cloid-ribbon-provider,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@79e7188e

可以看到启动时就加载了 spring-cloud-ribbon-provider,并绑定了LoadBalancer。

2.4 常用配置

配置项说明
{instance-id}:ribbon.NFLoadBalancerClassName指负载均衡器类路径
{instance-id}:ribbon:NFLoadBalancerRuleClassName指定负载均衡算法类路径
{instance-id}:ribbom:NFLoadBalancerPingClassName指定检测服务存活的类路径
{instance-id}:ribbon:NIWSServerListClassName指定获取服务列表的实现类路径
{instance-id}:ribbon:NIWSServerListFilterClassName指定服务的 Filter 实现类路径

3. 工作原理

3.1 Ribbon核心接口

接口描述默认实现类
IClientConfig定义 Ribbon 中管理配置的接口DefaultClientConfigImpl
IRule定义 Ribbon 中负载均衡策略的接口RoundRobinRule
IPing定义定期 ping 服务检查可用性的接口DummyPing
ServerList<Server>定义获取服务列表方法的接口ConfigurationBasedServerList
ServerListFilter<Server>定义特定期望获取服务列表方法的接口ZonePreferenceServerListFilter
ILoadBalanacer定义负载均衡选择服务的核心方法的接口BaseLoadBalancer
ServerListUpdater为 DynamicServerListLoadBalancer 定义动态更新服务列表的接口PollingServerListUpdater

3.2 Ribbon的运行原理

Ribbon 实现负载均衡,基本用法是注入一个 RestTemplate,并在 RestTemplate 上使用 @LoadBalanced,才能使 RestTemplate 具备负载均衡能力。

3.2.1 @LoadBalanced

这个注解标注,一个RestTemplate使用LoadBalancerClient。

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

/**
 * Represents a client side load balancer
 * @author Spencer Gibb
 */
public interface LoadBalancerClient extends ServiceInstanceChooser {

    <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;

    <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
    
    URI reconstructURI(ServiceInstance instance, URI original);
}

LoadBalancerClient又扩展了ServiceInstanceChooser接口

public interface ServiceInstanceChooser {

    ServiceInstance choose(String serviceId);
}

说明:

  • ServiceInstance choose(String serviceId):根据 serviceId,结合负载均衡器,选择一个服务实例
  • <T> T execute(String serviceId, LoadBalancerRequest<T> request):使用 LoadBalancer 的 serviceInstance 为置顶的服务执行请求
  • <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request):使用来自 LoadBalancer 的 ServiceInstance 为指定的服务执行请求,是上一个方法的重载,是上一个方法的细节实现
  • URI reconstructURI(ServiceInstance instance, URI original):使用主机 ip、port 构建特定的 URI,供 RIbbon 内部使用。Ribbon 使用服务名称的 URI 作为host。如:http://instance-id/path/to/service

3.2.2 LoadBalancer 初始化

LoadBalancerAutoConfiguration 是 Ribbon 负载均衡初始化加载类,启动的关键核心代码如下:

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public LoadBalancerRequestFactory loadBalancerRequestFactory(
            LoadBalancerClient loadBalancerClient) {
        return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
    }

    @Configuration
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    static class LoadBalancerInterceptorConfig {
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(
                LoadBalancerClient loadBalancerClient,
                LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }

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

可以看到,在类注解上,@ConditionalOnClass(RestTemplate.class)@ConditionalOnBean(LoadBalancerClient.class),必须在当前工程下有 RestTemplate 的实例、必须已经初始化了 LoadBalancerClient 的实现类,才会加载 LoadBalancer 的自动装配。

其中 LoadBalancerRequestFactory 用于创建 LoadBalancerRequest,以供 LoadBalancerInterceptor 使用(在低版本没有),LoadBalancerInterceptorConfig 中维护了 LoadBalancerInterceptorRestTemplateCustomizer 的实例。

  • LoadBalancerInterceptor:拦截每一次 HTTP 请求,将请求绑定进 Ribbon 负载均衡的生命周期
  • RestTemplateCustomizer:为每个 RestTemplate 绑定 LoadBalancerInterceptor 拦截器

3.2.3 LoadBalancerInterceptor

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

LoadBalancerInterceptor 利用 ClientHttpRequestInterceptor 对每次 HTTP 请求进行拦截,这个类是 Spring 中维护的请求拦截器。可以看到,拦截的请求使用了 LoadBalancerClient 的 execute 方法处理请求(由于 RestTemplate 中使用服务名当做 host,所以此时 getHosts() 获取到的服务名),LoadBalancerClient 只有一个实现类: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));

    return execute(serviceId, ribbonServer, request);
}

可以看到,源码中首先获取一个 LoadBalancer,再去获取一个 Server,那么,这个 Server 就是具体服务实例的封装了。既然 Server 是一个具体的服务实例,那么, getServer(loadBalancer) 就是发生负载均衡的地方。

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

查看 chooseServer 方法具体实现(BaseLoadBalancer):

public Server chooseServer(Object key) {
    if (counter == null) {
        counter = createCounter();
    }
    counter.increment();
    if (rule == null) {
        return null;
    } else {
        try {
            return rule.choose(key);
        } catch (Exception e) {
            logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
            return null;
        }
    }
}

rule.choose(key) 的中的 rule,就是 IRule,而 IRule 就是 Ribbon 的负载均衡策略。由此可以证明,HTTP 请求域负载均衡策略关联起来了。

3.2.4 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();    
}

IRule 中一共定义了 3 个方法,实现类实现 choose 方法,会加入具体的负载均衡策略逻辑,另外两个方法与 ILoadBalancer 关联起来。
在调用过程中,Ribbon 通过 ILoadBalancer 关联 IRule,ILoadBalancer 的 chooseServer 方法会转为为调用 IRRule 的 choose 方法,抽象类 AbstractLoadBalancerRule 实现了这两个方法,从而将 ILoadBalancer 与 IRule 关联起来。

 

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值