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 关联起来。

发布了8 篇原创文章 · 获赞 0 · 访问量 7272

猜你喜欢

转载自blog.csdn.net/fedorafrog/article/details/104156757