SpringCloud 源码系列(8)— 负载均衡Ribbon 之 核心原理

2,413 阅读16分钟

系列文章:

SpringCloud 源码系列(1)— 注册中心Eureka 之 启动初始化

SpringCloud 源码系列(2)— 注册中心Eureka 之 服务注册、续约

SpringCloud 源码系列(3)— 注册中心Eureka 之 抓取注册表

SpringCloud 源码系列(4)— 注册中心Eureka 之 服务下线、故障、自我保护机制

SpringCloud 源码系列(5)— 注册中心Eureka 之 EurekaServer集群

SpringCloud 源码系列(6)— 注册中心Eureka 之 总结篇

SpringCloud 源码系列(7)— 负载均衡Ribbon 之 RestTemplate

负载均衡器 ILoadBalancer

从上一篇 RestTemplate 负载均衡的原理中了解到,使 RestTemplate 具备负载均衡的能力,最重要的一个组件之一就是负载均衡器 ILoadBalancer,因为要用它来获取能调用的 Server,有了 Server 才能对原始带有服务名的 URI 进行重构。这节就来看下 Ribbon 负载均衡器 ILoadBalancer 是如何创建的以及如何通过它获取 Server。

RibbonLoadBalancerClient:

public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
    // 从 SpringClientFactory 中获取负载均衡器 ILoadBalancer
    ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
    // 利用负载均衡器获取 Server
    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);
}

SpringClientFactory与上下文

从上一篇文章已经了解到,RibbonLoadBalancerClient 执行负载均衡请求时,通过 SpringClientFactory 来获取 ILoadBalancer。从 getInstance 一步步进去,最后会进入到 NamedContextFactory 中,从 getContext 方法可以发现,不同的服务都会创建一个 AnnotationConfigApplicationContext,也就是一个应用上下文 ApplicationContext。也就是说每个服务都有自己的一个上下文环境,会绑定不同的 ILoadBalancer。

RibbonLoadBalancerClient:

protected ILoadBalancer getLoadBalancer(String serviceId) {
    return this.clientFactory.getLoadBalancer(serviceId);
}

SpringClientFactory:

public ILoadBalancer getLoadBalancer(String name) {
	// 获取 ILoadBalancer
    return getInstance(name, ILoadBalancer.class);
}

public <C> C getInstance(String name, Class<C> type) {
	// 从父类 NamedContextFactory 中获取
    C instance = super.getInstance(name, type);
    if (instance != null) {
        return instance;
    }
    IClientConfig config = getInstance(name, IClientConfig.class);
    return instantiateWithConfig(getContext(name), type, config);
}

NamedContextFactory:

/**
 * name - 服务名称
 * type - 要获取的 bean 的类型
*/ 
public <T> T getInstance(String name, Class<T> type) {
    // 根据名称获取
    AnnotationConfigApplicationContext context = getContext(name);
    if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, type).length > 0) {
        return context.getBean(type);
    }
    return null;
}

protected AnnotationConfigApplicationContext getContext(String name) {
    // contexts => Map<String, AnnotationConfigApplicationContext>
    if (!this.contexts.containsKey(name)) {
        synchronized (this.contexts) {
            if (!this.contexts.containsKey(name)) {
                this.contexts.put(name, createContext(name));
            }
        }
    }
    return this.contexts.get(name);
}

调试看下 AnnotationConfigApplicationContext 上下文,可以看到放入了与这个服务绑定的 ILoadBalancer、IClientConfig、RibbonLoadBalancerContext 等。

它这里为什么要每个服务都绑定一个 ApplicationContext 呢?因为服务实例列表可以有多个来源,比如可以从 eureka 注册中心获取、可以通过代码配置、可以通过配置文件配置,另外每个服务还可以有很多个性化的配置,有默认的配置、定制的全局配置、个别服务的特定配置等,它这样做就便于用户定制每个服务的负载均衡策略。

创建 ILoadBalancer

ILoadBalancer 的创建在哪呢?看 RibbonClientConfiguration,这个配置类提供了 ILoadBalancer 的默认创建方法,可以看到 ILoadBalancer 的默认实现类为 ZoneAwareLoadBalancer

public class RibbonClientConfiguration {
    public static final int DEFAULT_CONNECT_TIMEOUT = 1000;
    public static final int DEFAULT_READ_TIMEOUT = 1000;
    public static final boolean DEFAULT_GZIP_PAYLOAD = true;

    @RibbonClientName
    private String name = "client";
    @Autowired
    private PropertiesFactory propertiesFactory;

    @Bean
    @ConditionalOnMissingBean
    public IClientConfig ribbonClientConfig() {
        DefaultClientConfigImpl config = new DefaultClientConfigImpl();
        config.loadProperties(this.name);
        // 可以看到默认的连接超时和读取超时时间都是 1 秒
        config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
        config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
        config.set(CommonClientConfigKey.GZipPayload, DEFAULT_GZIP_PAYLOAD);
        return config;
    }

    @Bean
    @ConditionalOnMissingBean
    public IRule ribbonRule(IClientConfig config) {
        if (this.propertiesFactory.isSet(IRule.class, name)) {
            return this.propertiesFactory.get(IRule.class, config, name);
        }
        ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
        rule.initWithNiwsConfig(config);
        return rule;
    }

    @Bean
    @ConditionalOnMissingBean
    public IPing ribbonPing(IClientConfig config) {
        if (this.propertiesFactory.isSet(IPing.class, name)) {
            return this.propertiesFactory.get(IPing.class, config, name);
        }
        return new DummyPing();
    }

    @Bean
    @ConditionalOnMissingBean
    @SuppressWarnings("unchecked")
    public ServerList<Server> ribbonServerList(IClientConfig config) {
        if (this.propertiesFactory.isSet(ServerList.class, name)) {
            return this.propertiesFactory.get(ServerList.class, config, name);
        }
        ConfigurationBasedServerList serverList = new ConfigurationBasedServerList();
        serverList.initWithNiwsConfig(config);
        return serverList;
    }

    @Bean
    @ConditionalOnMissingBean
    public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
        return new PollingServerListUpdater(config);
    }

    @Bean
    @ConditionalOnMissingBean
    @SuppressWarnings("unchecked")
    public ServerListFilter<Server> ribbonServerListFilter(IClientConfig config) {
        if (this.propertiesFactory.isSet(ServerListFilter.class, name)) {
            return this.propertiesFactory.get(ServerListFilter.class, config, name);
        }
        ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
        filter.initWithNiwsConfig(config);
        return filter;
    }

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

可以看到创建 ILoadBalancer 需要 IClientConfig、ServerList<Server>、ServerListFilter<Server>、IRule、IPing、ServerListUpdater,其实这6个接口加上 ILoadBalancer 就是 Ribbon 的核心接口,它们共同定义了 Ribbon 的行为特性。

从 RibbonClientConfiguration 可以知道这7个核心接口以及默认实现类:

ILoadBalancer 选择 Server

获取到 ILoadBalancer 后,就要去获取 Server 了。从 getServer 方法可以看到,RibbonLoadBalancerClient 就是用 ILoadBalancer 来获取 Server。

protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
    if (loadBalancer == null) {
        return null;
    }
    // Use 'default' on a null hint, or just pass it on?
    return loadBalancer.chooseServer(hint != null ? hint : "default");
}

ILoadBalancer 的默认实现类是 ZoneAwareLoadBalancer,进入它的 chooseServer 方法内,如果只配置了一个 zone,就走父类的 chooseServer,否则从多个 zone 中去选择实例。

public Server chooseServer(Object key) {
    // ENABLED => ZoneAwareNIWSDiscoveryLoadBalancer.enabled 默认 true
    // AvailableZones 配置的只有一个 defaultZone
    if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
        logger.debug("Zone aware logic disabled or there is only one zone");
        // 走父类获取 Server 的逻辑
        return super.chooseServer(key);
    }

    // 多 zone 逻辑....
}

先看下 ZoneAwareLoadBalancer 的类继承结构,ZoneAwareLoadBalancer 的直接父类是 DynamicServerListLoadBalancer,DynamicServerListLoadBalancer 的父类又是 BaseLoadBalancer

ZoneAwareLoadBalancer 调用父类的 chooseServer 方法是在 BaseLoadBalancer 中的,进去可以看到,它主要是用 IRule 来选择实例,最终选择实例的策略就交给了 IRule 接口

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

IRule 轮询选择 Server

IRule 的默认实现类是 ZoneAvoidanceRule,先看下 ZoneAvoidanceRule 的继承结构,ZoneAvoidanceRule 的直接父类是 PredicateBasedRule

rule.choose 的逻辑在 PredicateBasedRule 中,getPredicate() 返回的是 ZoneAvoidanceRule 创建的一个组合断言 CompositePredicate,就是用这个断言来过滤出可用的 Server,并通过轮询的策略返回一个 Server。而 Server 列表的来源是通过 ILoadBalancer 的 getAllServers() 方法来获取的。

public Server choose(Object key) {
    ILoadBalancer lb = getLoadBalancer();
    // getPredicate() Server断言 => CompositePredicate
    // RoundRobin 轮询方式获取实例
    // 参数 servers => 通过 lb 负载均衡器获取所有 Server
    Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
    if (server.isPresent()) {
        return server.get();
    } else {
        return null;
    }
}

从这里也可以了解到 ILoadBalancer 和 IRule 的关系,ILoadBalancer 负责获取 Server 列表,而 IRule 是负责通过一定策略从 Server 列表中选出一个 Server

在初始化 ZoneAvoidanceRule 配置时,创建了 CompositePredicate,可以看到这个组合断言主要有两个断言,一个是断言 Server 的 zone 是否可用,一个断言 Server 本身是否可用,例如 Server 无法 ping 通。

public void initWithNiwsConfig(IClientConfig clientConfig) {
    // 断言 Server 的 zone 是否可用,只有一个 defaultZone 的情况下都是可用的
    ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this, clientConfig);
    // 断言 Server 是否可用
    AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this, clientConfig);
    // 封装组合断言
    compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate);
}

private CompositePredicate createCompositePredicate(ZoneAvoidancePredicate p1, AvailabilityPredicate p2) {
    // 建造者模式创建断言
    return CompositePredicate.withPredicates(p1, p2)
                         .addFallbackPredicate(p2)
                         .addFallbackPredicate(AbstractServerPredicate.alwaysTrue())
                         .build();
}

接着看选择Server的 chooseRoundRobinAfterFiltering,参数 servers 是通过 ILoadBalancer 获取的所有实例,可以看到它其实就是返回了 ILoadBalancer 在内存中缓存的服务所有 Server。这个 Server 从哪来的我们后面再来看。

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

先对所有实例通过断言过滤掉不可用的 Server,然后是通过轮询的方式获取一个 Server 返回。这就是默认配置下 ILoadBalancer(ZoneAwareLoadBalancer) 通过 IRule(ZoneAvoidanceRule) 选择 Server 的机制了,默认采用的是轮询的策略获取 Server。

public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
    // 断言获取可用的 Server
    List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
    
    // 通过取模的方式轮询 Server
    return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
}

public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
    if (loadBalancerKey == null) {
        return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate()));
    } else {
        List<Server> results = Lists.newArrayList();
        // 对每个 Server 断言
        for (Server server: servers) {
            if (this.apply(new PredicateKey(loadBalancerKey, server))) {
                results.add(server);
            }
        }
        return results;
    }
}

private int incrementAndGetModulo(int modulo) {
    for (;;) {
        int current = nextIndex.get();
        // 模运算取余数
        int next = (current + 1) % modulo;
        // CAS 更新 nextIndex
        if (nextIndex.compareAndSet(current, next) && current < modulo)
            return current;
    }
}

Ribbon 整合EurekaClient拉取Server列表

前面在通过 IRule 选择 Server 的时候,首先通过 lb.getAllServers() 获取了所有的 Server,那这些 Server 从哪里来的呢,这节就来看下。

ILoadBalancer 初始化

ILoadBalancer 的默认实现类是 ZoneAwareLoadBalancer,先从 ZoneAwareLoadBalancer 的构造方法进去看看都做了些什么事情。

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

可以看到,ZoneAwareLoadBalancer 直接调用了父类 DynamicServerListLoadBalancer 的构造方法,DynamicServerListLoadBalancer 先调用父类 BaseLoadBalancer 初始化,然后又做了一些剩余的初始化工作。

public ZoneAwareLoadBalancer(IClientConfig clientConfig, IRule rule,
                             IPing ping, ServerList<T> serverList, ServerListFilter<T> filter,
                             ServerListUpdater serverListUpdater) {
    // DynamicServerListLoadBalancer
    super(clientConfig, rule, ping, serverList, filter, serverListUpdater);
}

public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
                                     ServerList<T> serverList, ServerListFilter<T> filter,
                                     ServerListUpdater serverListUpdater) {
    // BaseLoadBalancer
    super(clientConfig, rule, ping);
    this.serverListImpl = serverList;
    this.filter = filter;
    this.serverListUpdater = serverListUpdater;
    if (filter instanceof AbstractServerListFilter) {
        ((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
    }
    // 剩余的一些初始化
    restOfInit(clientConfig);
}

public BaseLoadBalancer(IClientConfig config, IRule rule, IPing ping) {
    // createLoadBalancerStatsFromConfig => LoadBalancerStats 统计
    initWithConfig(config, rule, ping, createLoadBalancerStatsFromConfig(config));
}    

看 BaseLoadBalancer 的 initWithConfig,主要做了如下的初始化:

  • 设置 IPing 和 IRule,ping 的间隔时间是 30秒setPing 会启动一个后台定时任务,然后每隔30秒运行一次 PingTask 任务。
  • 设置了 ILoadBalancer 的 统计器 LoadBalancerStats,对 ILoadBalancer 的 Server 状态进行统计,比如连接失败、成功、熔断等信息。
  • 在启用 PrimeConnections 请求预热的情况下,创建 PrimeConnections 来预热客户端 与 Server 的链接。默认是关闭的。
  • 最后是注册了一些监控、开启请求预热。
void initWithConfig(IClientConfig clientConfig, IRule rule, IPing ping, LoadBalancerStats stats) {
    this.config = clientConfig;
    String clientName = clientConfig.getClientName();
    this.name = clientName;
    // ping 间隔时间,默认30秒
    int pingIntervalTime = Integer.parseInt(clientConfig.getProperty(CommonClientConfigKey.NFLoadBalancerPingInterval, Integer.parseInt("30")));
    // 没看到用的地方
    int maxTotalPingTime = Integer.parseInt(clientConfig.getProperty(CommonClientConfigKey.NFLoadBalancerMaxTotalPingTime, Integer.parseInt("2")));
    // 设置 ping 间隔时间,并重新设置了 ping 任务
    setPingInterval(pingIntervalTime);
    setMaxTotalPingTime(maxTotalPingTime);

    // 设置 IRule、IPing
    setRule(rule);
    setPing(ping);

    // PrimeConnections,请求预热,默认关闭
    // 作用主要用于解决那些部署环境(如读EC2)在实际使用实时请求之前,从防火墙连接/路径进行预热(比如先加白名单、初始化等等动作比较耗时,可以用它先去打通)。
    boolean enablePrimeConnections = clientConfig.get(CommonClientConfigKey.EnablePrimeConnections, DefaultClientConfigImpl.DEFAULT_ENABLE_PRIME_CONNECTIONS);
    if (enablePrimeConnections) {
        this.setEnablePrimingConnections(true);
        PrimeConnections primeConnections = new PrimeConnections(this.getName(), clientConfig);
        this.setPrimeConnections(primeConnections);
    }
    // 注册一些监控
    init();
}

protected void init() {
    Monitors.registerObject("LoadBalancer_" + name, this);
    // register the rule as it contains metric for available servers count
    Monitors.registerObject("Rule_" + name, this.getRule());
    // 默认关闭
    if (enablePrimingConnections && primeConnections != null) {
        primeConnections.primeConnections(getReachableServers());
    }
}

再看下 DynamicServerListLoadBalancer 的初始化,核心的初始化逻辑在 restOfInit 中,主要就是做了两件事情:

  • 开启动态更新 Server 的特性,比如实例上线、下线、故障等,要能够更新 ILoadBalancer 的 Server 列表。
  • 然后就全量更新一次本地的 Server 列表。
void restOfInit(IClientConfig clientConfig) {
    boolean primeConnection = this.isEnablePrimingConnections();
    // turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
    this.setEnablePrimingConnections(false);

    // 开启动态更新 Server 的特性
    enableAndInitLearnNewServersFeature();

    // 更新 Server 列表
    updateListOfServers();

    // 开启请求预热的情况下,对可用的 Server 进行预热
    if (primeConnection && this.getPrimeConnections() != null) {
        this.getPrimeConnections().primeConnections(getReachableServers());
    }
    this.setEnablePrimingConnections(primeConnection);
}

全量更新Server列表

先看下 updateListOfServers() 是如何更新 Server 列表的,进而看下 ILoadBalancer 是如何存储 Server 的。

  • 首先使用 ServerList 获取所有的 Server 列表,在 RibbonClientConfiguration 中配置的是 ConfigurationBasedServerList,但和 eureka 继承后,就不是 ConfigurationBasedServerList 了,这块下一节再来看。
  • 然后使用 ServerListFilter 对 Server 列表过滤,其默认实现类是 ZonePreferenceServerListFilter,它主要是过滤出当前 Zone(defaultZone)下的 Server。
  • 最后就是更新所有 Server 列表,先是设置 Server alive,然后调用父类(BaseLoadBalancer)的 setServersList 来更新Server列表,这说明 Server 是存储在 BaseLoadBalancer 里的。
public void updateListOfServers() {
    List<T> servers = new ArrayList<T>();
    if (serverListImpl != null) {
        // 从 ServerList 获取所有 Server 列表
        servers = serverListImpl.getUpdatedListOfServers();

        if (filter != null) {
            // 用 ServerListFilter 过滤 Server
            servers = filter.getFilteredListOfServers(servers);
        }
    }
    // 更新所有 Server 到本地缓存
    updateAllServerList(servers);
}

protected void updateAllServerList(List<T> ls) {
    if (serverListUpdateInProgress.compareAndSet(false, true)) {
        try {
            for (T s : ls) {
                s.setAlive(true); // 设置 Server alive
            }
            setServersList(ls);
            // 强制初始化 Ping
            super.forceQuickPing();
        } finally {
            serverListUpdateInProgress.set(false);
        }
    }
}

public void setServersList(List lsrv) {
    // BaseLoadBalancer
    super.setServersList(lsrv);

    // 将 Server 更新到 LoadBalancerStats 统计中 ....
}

接着看父类的 setServersList,可以看出,存储所有 Server 的数据结构 allServerList 是一个加了 synchronized 的线程安全的容器,setServersList 就是直接将得到的 Server 列表替换 allServerList


protected volatile List<Server> allServerList = Collections.synchronizedList(new ArrayList<Server>());

public void setServersList(List lsrv) {
    Lock writeLock = allServerLock.writeLock();
    ArrayList<Server> newServers = new ArrayList<Server>();
    // 加写锁
    writeLock.lock();
    try {
        // for 循环将 lsrv 中的 Server 转移到 allServers
        ArrayList<Server> allServers = new ArrayList<Server>();
        for (Object server : lsrv) {
            if (server instanceof String) {
            	// 一般就是静态配置中配置的 ip:port 的形式
                server = new Server((String) server);
            }
            if (server instanceof Server) {
            	// 从 EurekaClient 同步过来的 Server
                allServers.add((Server) server);
            } else {
                throw new IllegalArgumentException("Type String or Server expected, instead found:" + server.getClass());
            }
        }

        // 如果启用了服务预热,开始 Server 预热...

        // 直接替换
        allServerList = allServers;
        
        // Ping 判断 Server 存活状态
        if (canSkipPing()) {
            for (Server s : allServerList) {
                s.setAlive(true);
            }
            upServerList = allServerList;
        } else if (listChanged) {
            forceQuickPing();
        }
    } finally {
        // 释放写锁
        writeLock.unlock();
    }
}

前面 chooseRoundRobinAfterFiltering 获取所有 Server 时就是返回的这个 allServerList 列表。

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

Eureka Ribbon 客户端配置

获取 Server 的组件是 ServerList,RibbonClientConfiguration 中配置的默认实现类是 ConfigurationBasedServerList。ConfigurationBasedServerList 默认是从配置文件中获取,可以像下面这样配置服务实例地址,多个 Server 地址用逗号隔开。

demo-producer:
  ribbon:
    listOfServers: http://10.215.0.92:8010,http://10.215.0.92:8011

但是和 eureka-client 结合后,也就是引入 spring-cloud-starter-netflix-eureka-client 的客户端依赖,它会帮我们引入 spring-cloud-netflix-eureka-client 依赖,这个包中有一个 RibbonEurekaAutoConfiguration 自动化配置类,它通过 @RibbonClients 注解定义了全局的 Ribbon 客户端配置类 为 EurekaRibbonClientConfiguration

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@ConditionalOnRibbonAndEurekaEnabled
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@RibbonClients(defaultConfiguration = EurekaRibbonClientConfiguration.class)
public class RibbonEurekaAutoConfiguration {

}

进入 EurekaRibbonClientConfiguration 可以看到:

  • IPing 的默认实现类为 NIWSDiscoveryPing
  • ServerList 的默认实现类为 DomainExtractingServerList,但是 DomainExtractingServerList 在构造时又传入了一个类型为 DiscoveryEnabledNIWSServerList 的 ServerList。看名字大概也可以看出,DiscoveryEnabledNIWSServerList 就是从 EurekaClient 获取 Server 的组件。
@Configuration(proxyBeanMethods = false)
public class EurekaRibbonClientConfiguration {
    @Value("${ribbon.eureka.approximateZoneFromHostname:false}")
    private boolean approximateZoneFromHostname = false;

    @RibbonClientName
    private String serviceId = "client";
    @Autowired
    private PropertiesFactory propertiesFactory;

    @Bean
    @ConditionalOnMissingBean
    public IPing ribbonPing(IClientConfig config) {
        if (this.propertiesFactory.isSet(IPing.class, serviceId)) {
            return this.propertiesFactory.get(IPing.class, config, serviceId);
        }
        NIWSDiscoveryPing ping = new NIWSDiscoveryPing();
        ping.initWithNiwsConfig(config);
        return ping;
    }

    @Bean
    @ConditionalOnMissingBean
    public ServerList<?> ribbonServerList(IClientConfig config, Provider<EurekaClient> eurekaClientProvider) {
        if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
            return this.propertiesFactory.get(ServerList.class, config, serviceId);
        }
        DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(config, eurekaClientProvider);
        DomainExtractingServerList serverList = new DomainExtractingServerList(discoveryServerList, config, this.approximateZoneFromHostname);
        return serverList;
    }
}

从 DiscoveryClient 获取Server列表

DynamicServerListLoadBalancer 中通过 ServerList 的 getUpdatedListOfServers 方法全量获取服务列表,在 eureka-client 环境下,ServerList 默认实现类为 DomainExtractingServerList,那就先看下它的 getUpdatedListOfServers 方法。

可以看出,DomainExtractingServerList 先用 DomainExtractingServerList 获取服务列表,然后根据 Ribbon 客户端配置重新构造 Server 对象返回。获取服务列表的核心在 DiscoveryEnabledNIWSServerList 中。

@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers() {
    // list => DiscoveryEnabledNIWSServerList
    List<DiscoveryEnabledServer> servers = setZones(this.list.getUpdatedListOfServers());
    return servers;
}

private List<DiscoveryEnabledServer> setZones(List<DiscoveryEnabledServer> servers) {
    List<DiscoveryEnabledServer> result = new ArrayList<>();
    boolean isSecure = this.ribbon.isSecure(true);
    boolean shouldUseIpAddr = this.ribbon.isUseIPAddrForServer();
    // 根据客户端配置重新构造 DomainExtractingServer 返回
    for (DiscoveryEnabledServer server : servers) {
        result.add(new DomainExtractingServer(server, isSecure, shouldUseIpAddr, this.approximateZoneFromHostname));
    }
    return result;
}

先看下 DiscoveryEnabledNIWSServerList 的构造初始化:

  • 主要是传入了 Provider<EurekaClient> 用来获取 EurekaClient。
  • 另外还设置了客户端名称 clientName ,以及 vipAddresses 也是客户端名称,这个后面会用得上。
public DiscoveryEnabledNIWSServerList(IClientConfig clientConfig, Provider<EurekaClient> eurekaClientProvider) {
    this.eurekaClientProvider = eurekaClientProvider;
    initWithNiwsConfig(clientConfig);
}

@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
    // 客户端名称,就是服务名称
    clientName = clientConfig.getClientName();
    // vipAddresses 得到的也是客户端名称
    vipAddresses = clientConfig.resolveDeploymentContextbasedVipAddresses();

    // 其它的一些配置....
}

接着看获取实例的 getUpdatedListOfServers,可以看到它的核心逻辑就是根据服务名从 EurekaClient 获取 InstanceInfo 实例列表,然后封装 Server 信息返回。

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

private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
    List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
    // 得到 EurekaClient,实际类型是 CloudEurekaClient,其父类是 DiscoveryClient
    EurekaClient eurekaClient = eurekaClientProvider.get();
    if (vipAddresses!=null){
        // 分割 vipAddresses,默认就是服务名称
        for (String vipAddress : vipAddresses.split(",")) {
            // 根据服务名称从 EurekaClient 获取实例信息
            List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
            for (InstanceInfo ii : listOfInstanceInfo) {
                if (ii.getStatus().equals(InstanceStatus.UP)) {
                    // 根据实例信息 InstanceInfo 创建 Server
                    DiscoveryEnabledServer des = createServer(ii, isSecure, shouldUseIpAddr);
                    serverList.add(des);
                }
            }
        }
    }
    return serverList;
}

注意这里的 vipAddress 其实就是服务名:

最后看 EurekaClient 的 getInstancesByVipAddress,到这里就很清楚了,其实就是从 DiscoveryClient 的本地应用 Applications 中根据服务名取出所有的实例列表。

这里就和 Eureka 源码那块衔接上了,eureka-client 全量抓取注册表以及每隔30秒增量抓取注册表,都是合并到本地的 Applications 中。Ribbon 与 Eureka 结合后,Ribbon 获取 Server 就从 DiscoveryClient 的 Applications 中获取 Server 列表了。

public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure, String region) {
    // ...
    Applications applications;
    if (instanceRegionChecker.isLocalRegion(region)) {
        // 取本地应用 Applications
        applications = this.localRegionApps.get();
    } else {
        applications = remoteRegionVsApps.get(region);
        if (null == applications) {
            return Collections.emptyList();
        }
    }

    if (!secure) {
        // 返回服务名对应的实例
        return applications.getInstancesByVirtualHostName(vipAddress);
    } else {
        return applications.getInstancesBySecureVirtualHostName(vipAddress);
    }
}

定时更新Server列表

DynamicServerListLoadBalancer 初始化时,有个方法还没说,就是 enableAndInitLearnNewServersFeature()。这个方法只是调用 ServerListUpdater 启动了一个 UpdateAction,这个 UpdateAction 又只是调用了一下 updateListOfServers 方法,就是前面讲解过的全量更新 Server 的逻辑。

public void enableAndInitLearnNewServersFeature() {
    serverListUpdater.start(updateAction);
}

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

ServerListUpdater 的默认实现类是 PollingServerListUpdater,看下它的 start 方法:

其实就是以固定的频率,每隔30秒调用一下 updateListOfServers 方法,将 DiscoveryClient 中 Applications 中缓存的实例同步到 ILoadBalancer 中的 allServerList 列表中。

public synchronized void start(final UpdateAction updateAction) {
    if (isActive.compareAndSet(false, true)) {
        final Runnable wrapperRunnable = new Runnable() {
            @Override
            public void run() {
                // 执行一次 updateListOfServers
                updateAction.doUpdate();
                // 设置最后更新时间
                lastUpdated = System.currentTimeMillis();
            }
        };

        // 固定频率调度
        scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                wrapperRunnable,
                initialDelayMs, // 默认 1000
                refreshIntervalMs, // 默认 30 * 1000
                TimeUnit.MILLISECONDS
        );
    } else {
        logger.info("Already active, no-op");
    }
}

到这里我们就要注意下了,结合 eureka 抓取注册表的流程我们来分析下 Ribbon 客户端最多需要多久才能感应到一个新实例的注册:

  • 首先 DiscoveryClient 每隔 30秒 向注册中心抓取一次增量注册表,抓取增量注册表会先从只读缓存读取
  • 然后 eureka server 端注册表的设计是三层缓存结构,每隔 30秒 从读写缓存 readWriteMap 同步到只读缓存 readOnlyMap
  • 注册中心的实例同步到客户端本地后,Ribbon 又会间隔30秒从 DiscoveryClient 同步到 BaseLoadBalancer

总结一下,也就是说 Ribbon 负载均衡可能最多需要90秒才能感应到注册中心注册的新实例。

判断Server是否存活

在创建 ILoadBalancer 时,IPing 还没有看过是如何工作的。在初始化的时候,可以看到,主要就是设置了当前的 ping,然后重新设置了一个调度任务,默认每隔30秒调度一次 PingTask 任务。

public void setPing(IPing ping) {
    if (ping != null) {
        if (!ping.equals(this.ping)) {
            this.ping = ping;
            // 设置 Ping 任务
            setupPingTask();
        }
    } else {
        this.ping = null;
        // cancel the timer task
        lbTimer.cancel();
    }
}

void setupPingTask() {
    // ...
    // 创建一个定时调度器
    lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name, true);
    // pingIntervalTime 默认为 30 秒,每隔30秒调度一次 PingTask
    lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
    // 立即发起以 Ping
    forceQuickPing();
}

ShutdownEnabledTimer 可以简单了解下,它是继承自 Timer 的,它在创建的时候向 Runtime 注册了一个回调,在 jvm 关闭的时候来取消 Timer 的执行,进而释放资源。

public class ShutdownEnabledTimer extends Timer {
    private Thread cancelThread;
    private String name;

    public ShutdownEnabledTimer(String name, boolean daemon) {
        super(name, daemon);
        this.name = name;
        // 取消定时器的线程
        this.cancelThread = new Thread(new Runnable() {
            public void run() {
                ShutdownEnabledTimer.super.cancel();
            }
        });

        // 向 Runtime 注册一个钩子,在 jvm 关闭时,调用 cancelThread 取消定时任务
        Runtime.getRuntime().addShutdownHook(this.cancelThread);
    }

    @Override
    public void cancel() {
        super.cancel();
        try {
            // 移除回调
            Runtime.getRuntime().removeShutdownHook(this.cancelThread);
        } catch (IllegalStateException ise) {
            LOGGER.info("Exception caught (might be ok if at shutdown)", ise);
        }
    }
}

再来看下 PingTask,PingTask 核心逻辑就是遍历 allServers 列表,使用 IPingStrategyIPing 来判断 Server 是否存活,并更新 Server 的状态,以及将所有存活的 Server 更新到 upServerList 中,upServerList 缓存了所有存活的 Server。

class PingTask extends TimerTask {
    public void run() {
        try {
            // pingStrategy => SerialPingStrategy
            new Pinger(pingStrategy).runPinger();
        } catch (Exception e) {
            logger.error("LoadBalancer [{}]: Error pinging", name, e);
        }
    }
}

class Pinger {
    private final IPingStrategy pingerStrategy;

    public Pinger(IPingStrategy pingerStrategy) {
        this.pingerStrategy = pingerStrategy;
    }

    public void runPinger() throws Exception {
        if (!pingInProgress.compareAndSet(false, true)) {
            return; // Ping in progress - nothing to do
        }

        Server[] allServers = null;
        boolean[] results = null;

        Lock allLock = null;
        Lock upLock = null;

        try {
            allLock = allServerLock.readLock();
            allLock.lock();
            // 加读锁,取出 allServerList 中的 Server
            allServers = allServerList.toArray(new Server[allServerList.size()]);
            allLock.unlock();

            int numCandidates = allServers.length;
            // 使用 IPingStrategy 和 IPing 对所有 Server 发起 ping 请求
            results = pingerStrategy.pingServers(ping, allServers);

            final List<Server> newUpList = new ArrayList<Server>();
            final List<Server> changedServers = new ArrayList<Server>();

            for (int i = 0; i < numCandidates; i++) {
                boolean isAlive = results[i];
                Server svr = allServers[i];
                boolean oldIsAlive = svr.isAlive();
                // 设置 alive 是否存活
                svr.setAlive(isAlive);

                // 实例变更
                if (oldIsAlive != isAlive) {
                    changedServers.add(svr);
                }

                // 添加存活的 Server
                if (isAlive) {
                    newUpList.add(svr);
                }
            }
            upLock = upServerLock.writeLock();
            upLock.lock();
            // 更新 upServerList,upServerList 只保存了存活的 Server
            upServerList = newUpList;
            upLock.unlock();
            // 通知变更
            notifyServerStatusChangeListener(changedServers);
        } finally {
            pingInProgress.set(false);
        }
    }
}

IPingStrategy 的默认实现类是 SerialPingStrategy,进入可以发现它只是遍历所有 Server,然后用 IPing 判断 Server 是否存活。

private static class SerialPingStrategy implements IPingStrategy {
    @Override
    public boolean[] pingServers(IPing ping, Server[] servers) {
        int numCandidates = servers.length;
        boolean[] results = new boolean[numCandidates];

        for (int i = 0; i < numCandidates; i++) {
            results[i] = false;
            try {
                if (ping != null) {
                    // 使用 IPing 判断 Server 是否存活
                    results[i] = ping.isAlive(servers[i]);
                }
            } catch (Exception e) {
                logger.error("Exception while pinging Server: '{}'", servers[i], e);
            }
        }
        return results;
    }
}

在集成 eureka-client 后,IPing 默认实现类是 NIWSDiscoveryPing,看它的 isAlive 方法,其实就是判断对应 Server 的实例 InstanceInfo 的状态是否是 UP 状态,UP状态就表示 Server 存活。

public boolean isAlive(Server server) {
    boolean isAlive = true;
    if (server!=null && server instanceof DiscoveryEnabledServer){
        DiscoveryEnabledServer dServer = (DiscoveryEnabledServer)server;
        InstanceInfo instanceInfo = dServer.getInstanceInfo();
        if (instanceInfo!=null){
            InstanceStatus status = instanceInfo.getStatus();
            if (status!=null){
                // 判断Server对应的实例状态是否是 UP
                isAlive = status.equals(InstanceStatus.UP);
            }
        }
    }
    return isAlive;
}

一张图总结 Ribbon 核心原理

1、Ribbon 核心工作原理总结

首先,Ribbon 的7个核心接口共同定义了 Ribbon 的行为特性,它们就是 Ribbon 的核心骨架。

  • 使用 Ribbon 来对客户端做负载均衡,基本的用法就是用 @LoadBalanced 注解标注一个 RestTemplate 的 bean 对象,之后在 LoadBalancerAutoConfiguration 配置类中会对带有 @LoadBalanced 注解的 RestTemplate 添加 LoadBalancerInterceptor 拦截器。
  • LoadBalancerInterceptor 会拦截 RestTemplate 的 HTTP 请求,将请求绑定进 Ribbon 负载均衡的生命周期,然后使用 LoadBalancerClientexecute 方法来处理请求。
  • LoadBalancerClient 首先会得到一个 ILoadBalancer,再使用它去得到一个 Server,这个 Server 就是具体某一个实例的信息封装。得到 Server 之后,就用 Server 的 IP 和端口重构原始 URI。
  • ILoadBalancer 最终在选择实例的时候,会通过 IRule 均衡策略来选择一个 Server。
  • ILoadBalancer 的父类 BaseLoadBalancer 中有一个 allServerList 列表缓存了所有 Server,Ribbon 中 Server 的来源就是 allServerList。
  • 在加载Ribbon客户端上下文时,ILoadBalancer 会用 ServerList 从 DiscoveryClient 的 Applications 中获取客户端对应的实例列表,然后使用 ServerListFilter 过滤,最后更新到 allServerList 中。
  • ILoadBalancer 还会开启一个后台任务 ServerListUpdater ,每隔30秒运行一次,用 ServerList 将 DiscoveryClient 的 Applications 中的实例列表同步到 allServerList 中。
  • ILoadBalancer 还会开启一个后台任务 PingTask,每隔30秒运行一次,用 IPing 判断 Server 的存活状态,EurekaClient 环境下,就是判断 InstanceInfo 的状态是否为 UP

2、下面用一张图来总结下 Ribbon 这块获取Server的核心流程以及对应的核心接口间的关系。