Series of articles:

SpringCloud source series (1) – registry initialization for Eureka

SpringCloud source code series (2) – Registry Eureka service registration, renewal

SpringCloud source code series (3) – Registry Eureka crawl registry

SpringCloud source code series (4) – Registry Eureka service offline, failure, self-protection mechanism

SpringCloud source series (5) – Registry EurekaServer cluster for Eureka

SpringCloud source Code Series (6) – Summary of the Registry Eureka

SpringCloud source series (7) – load balancing Ribbon RestTemplate

ILoadBalancer load balancer

One of the most important components of a RestTemplate load balancing capability is the ILoadBalancer, because it is used to obtain the Server that can be called. The original URI with the service name can be refactored with the Server. This section takes a look at how the Ribbon load balancer is created and how to use it to fetch servers.

RibbonLoadBalancerClient:

public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
    // Get ILoadBalancer from SpringClientFactory
    ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
    // Use the load balancer to obtain the 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);
}
Copy the code

SpringClientFactory and context

As you learned from the previous article, the ILoadBalancer is retrieved by SpringClientFactory when the RibbonLoadBalancerClient performs load balancing requests. From getInstance step by step, finally will enter into the NamedContextFactory, from getContext method can be found that different service will create a AnnotationConfigApplicationContext, That is, an ApplicationContext ApplicationContext. That is, each service has its own context that binds to a different ILoadBalancer.

RibbonLoadBalancerClient:

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

Copy the code

SpringClientFactory:

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

public <C> C getInstance(String name, Class<C> type) {
	// From the parent NamedContextFactory class
    C instance = super.getInstance(name, type);
    if(instance ! =null) {
        return instance;
    }
    IClientConfig config = getInstance(name, IClientConfig.class);
    return instantiateWithConfig(getContext(name), type, config);
}
Copy the code

NamedContextFactory:

/** * name - service name * type - The type of the bean to get */ 
public <T> T getInstance(String name, Class<T> type) {
    // Obtain by name
    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);
}
Copy the code

Debugging see AnnotationConfigApplicationContext context, you can see in the and the service binding ILoadBalancer, IClientConfig, RibbonLoadBalancerContext, etc.

Why does it bind an ApplicationContext for each service here? Because the list of service instances can come from multiple sources, such as from the Eureka registry, through code configuration, through configuration files, and there are many personalized configurations for each service, including default configurations, customized global configurations, service-specific configurations, and so on, In doing so, it makes it easy for users to customize the load balancing strategy for each service.

Create ILoadBalancer

Where is ILoadBalancer created? See RibbonClientConfiguration, this configuration class provides a default ILoadBalancer creation method, can see the default implementation of ILoadBalancer ZoneAwareLoadBalancer class.

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(a) {
        DefaultClientConfigImpl config = new DefaultClientConfigImpl();
        config.loadProperties(this.name);
        // You can see that the default connection timeout and read timeout are both 1 second
        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
       
         serverList, ServerListFilter
        
          serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater)
        
        {
        // Check whether a load balancer is configured in the configuration file
        if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
            // Create by reflection
            return this.propertiesFactory.get(ILoadBalancer.class, config, name);
        }
        return newZoneAwareLoadBalancer<>(config, rule, ping, serverList, serverListFilter, serverListUpdater); }}Copy the code

IClientConfig, ServerList

, ServerListFilter

, IRule, IPing, ServerListUpdater, These six interfaces and ILoadBalancer are the core interfaces of the Ribbon. Together, they define the Ribbon’s behavior.

Can know from RibbonClientConfiguration these seven core interface and the default implementation class:

ILoadBalancer choose Server

Now that we have ILoadBalancer, we need to get the Server. As can be seen from the getServer method, RibbonLoadBalancerClient uses ILoadBalancer to obtain the Server.

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

ILoadBalancer default implementation class is ZoneAwareLoadBalancer, enter its chooseServer method, if only one zone is configured, go to the parent class chooseServer, otherwise select instances from multiple zones.

public Server chooseServer(Object key) {
    / / ENABLED = > ZoneAwareNIWSDiscoveryLoadBalancer. ENABLED by default is true
    AvailableZones is configured with only one defaultZone
    if(! ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <=1) {
        logger.debug("Zone aware logic disabled or there is only one zone");
        // Get the Server logic from the parent class
        return super.chooseServer(key);
    }

    // Multi-zone logic....
}
Copy the code

First look at the class hierarchy structure of the ZoneAwareLoadBalancer, ZoneAwareLoadBalancer DynamicServerListLoadBalancer is the direct parent class, The parent class is BaseLoadBalancer DynamicServerListLoadBalancer.

ZoneAwareLoadBalancer calls the parent’s chooseServer method in BaseLoadBalancer, which uses IRule to select instances, leaving the policy to the IRule interface.

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; }}}Copy the code

IRule polling selects the Server

The default implementation class of IRule is ZoneAvoidanceRule, and first look at the inheritance structure of ZoneAvoidanceRule. The immediate parent of ZoneAvoidanceRule is PredicateBasedRule.

In PredicateBasedRule, getPredicate() returns a CompositePredicate created by ZoneAvoidanceRule, It is this assertion that is used to filter out available servers and return a Server through a polling policy. The source of the Server list is obtained through ILoadBalancer’s getAllServers() method.

public Server choose(Object key) {
    ILoadBalancer lb = getLoadBalancer();
    // getPredicate() Server predicate => CompositePredicate
    // RoundRobin obtains an instance in RoundRobin mode
    // Servers => Obtain all servers from the LB load balancer
    Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
    if (server.isPresent()) {
        return server.get();
    } else {
        return null; }}Copy the code

ILoadBalancer is responsible for getting the list of servers, while IRule is responsible for selecting a Server from the list of servers by a policy.

When initializes the ZoneAvoidanceRule configuration, a CompositePredicate is created, and you can see that the CompositePredicate has two main assertions: one asserts whether the zone of the Server is available, and one asserts whether the Server itself is available. For example, the Server cannot be pinged through.

public void initWithNiwsConfig(IClientConfig clientConfig) {
    // Declare whether the Server zone is available, even if there is only one defaultZone
    ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this, clientConfig);
    // Assert whether the Server is available
    AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this, clientConfig);
    // Encapsulate composite assertions
    compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate);
}

private CompositePredicate createCompositePredicate(ZoneAvoidancePredicate p1, AvailabilityPredicate p2) {
    // The builder pattern creates assertions
    return CompositePredicate.withPredicates(p1, p2)
                         .addFallbackPredicate(p2)
                         .addFallbackPredicate(AbstractServerPredicate.alwaysTrue())
                         .build();
}
Copy the code

Then choose the Server chooseRoundRobinAfterFiltering, parameters of the servers is obtained through ILoadBalancer all instances, You can see that it actually returns all the servers that ILoadBalancer has cached in memory. We’ll see where this Server came from later.

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

All instances are filtered by assertions to remove unavailable servers, and then a Server is returned by polling. This is how ILoadBalancer (ZoneAwareLoadBalancer) selects the Server via IRule (ZoneAvoidanceRule) on default configuration, which uses a polling policy to get the Server.

public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
    // Assert to get the available Server
    List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
    
    // Polling the Server by fetching a mold
    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();
        // Assert for each Server
        for (Server server: servers) {
            if (this.apply(newPredicateKey(loadBalancerKey, server))) { results.add(server); }}returnresults; }}private int incrementAndGetModulo(int modulo) {
    for (;;) {
        int current = nextIndex.get();
        // The modular operation takes the remainder
        int next = (current + 1) % modulo;
        // CAS updates nextIndex
        if (nextIndex.compareAndSet(current, next) && current < modulo)
            returncurrent; }}Copy the code

The Ribbon integrates EurekaClient to pull the Server list

Lb.getallservers () = lb.getallServers () = lb.getallServers ();

ILoadBalancer initialization

The default implementation of ILoadBalancer class is ZoneAwareLoadBalancer. Look at the ZoneAwareLoadBalancer constructor to see what’s going on.

@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList
       
         serverList, ServerListFilter
        
          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);
}
Copy the code

As you can see, ZoneAwareLoadBalancer directly call the parent class DynamicServerListLoadBalancer constructor, DynamicServerListLoadBalancer first calls the superclass BaseLoadBalancer initialization, and then did some remaining initialization.

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

public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping, ServerList
       
         serverList, ServerListFilter
        
          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());
    }
    // Initialize the rest
    restOfInit(clientConfig);
}

public BaseLoadBalancer(IClientConfig config, IRule rule, IPing ping) {
    / / createLoadBalancerStatsFromConfig = > LoadBalancerStats statistics
    initWithConfig(config, rule, ping, createLoadBalancerStatsFromConfig(config));
}    
Copy the code

BaseLoadBalancer initWithConfig (BaseLoadBalancer);

  • Set IPing and IRule. The ping interval is30 seconds.setPingWill start a background scheduled task, and then every other30 secondsTo run aPingTaskTask.
  • The ILoadBalancer statistic is setLoadBalancerStatsTo collect statistics on ILoadBalancer Server status, such as connection failure, success, and fuses.
  • With PrimeConnections requests preheated, create PrimeConnections to preheat the connection between the client and the Server. The default is off.
  • Finally, some monitoring is registered and request preheating is enabled.
void initWithConfig(IClientConfig clientConfig, IRule rule, IPing ping, LoadBalancerStats stats) {
    this.config = clientConfig;
    String clientName = clientConfig.getClientName();
    this.name = clientName;
    // Ping interval, 30 seconds by default
    int pingIntervalTime = Integer.parseInt(clientConfig.getProperty(CommonClientConfigKey.NFLoadBalancerPingInterval, Integer.parseInt("30")));
    // Don't see any use for it
    int maxTotalPingTime = Integer.parseInt(clientConfig.getProperty(CommonClientConfigKey.NFLoadBalancerMaxTotalPingTime, Integer.parseInt("2")));
    // Set the ping interval and reset the ping task
    setPingInterval(pingIntervalTime);
    setMaxTotalPingTime(maxTotalPingTime);

    // Set IRule and IPing
    setRule(rule);
    setPing(ping);

    // PrimeConnections, request preheating, default off
    // This function is mainly used to solve the deployment environment (such as read EC2) before the actual use of real-time requests, from the firewall connection/path warm-up (such as whitelist, initialization, etc. It is time-consuming, can be used to get through).
    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);
    }
    // Register some monitoring
    init();
}

protected void init(a) {
    Monitors.registerObject("LoadBalancer_" + name, this);
    // register the rule as it contains metric for available servers count
    Monitors.registerObject("Rule_" + name, this.getRule());
    // It is disabled by default
    if(enablePrimingConnections && primeConnections ! =null) { primeConnections.primeConnections(getReachableServers()); }}Copy the code

Again see DynamicServerListLoadBalancer initialization, the core of initialization logic in restOfInit, main is to do two things:

  • Enable dynamic update Server features, such as instance online, offline, failure, etc., to update the list of ILoadBalancer servers.
  • The local Server list is then fully updated.
void restOfInit(IClientConfig clientConfig) {
    boolean primeConnection = this.isEnablePrimingConnections();
    // turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
    this.setEnablePrimingConnections(false);

    // Enable dynamic Server update
    enableAndInitLearnNewServersFeature();

    // Update the Server list
    updateListOfServers();

    // Preheat available servers with request preheating enabled
    if (primeConnection && this.getPrimeConnections() ! =null) {
        this.getPrimeConnections().primeConnections(getReachableServers());
    }
    this.setEnablePrimingConnections(primeConnection);
}
Copy the code

Update the Server list in full

Take a look at how updateListOfServers() updates the Server list and then how ILoadBalancer stores the Server.

  • The first to useServerListGet the list of all servers inRibbonClientConfigurationIs configured inConfigurationBasedServerListAfter, but had inherited, is not ConfigurationBasedServerList, then come back the next section.
  • Then use theServerListFilterThe default implementation class for Server list filtering isZonePreferenceServerListFilter, which filters out the servers in the current Zone (defaultZone).
  • The last step is to update the list of all servers, starting with SettingsServer alive, and then call the parent class (BaseLoadBalancer)setServersListTo update the Server list, which indicates that the Server is stored inBaseLoadBalancerIn the.
public void updateListOfServers(a) {
    List<T> servers = new ArrayList<T>();
    if(serverListImpl ! =null) {
        // Get the list of all servers from ServerList
        servers = serverListImpl.getUpdatedListOfServers();

        if(filter ! =null) {
            // Use ServerListFilter to filter the Serverservers = filter.getFilteredListOfServers(servers); }}// Update all servers to the local cache
    updateAllServerList(servers);
}

protected void updateAllServerList(List<T> ls) {
    if (serverListUpdateInProgress.compareAndSet(false.true)) {
        try {
            for (T s : ls) {
                s.setAlive(true); // Set Server alive
            }
            setServersList(ls);
            // Forcibly initialize Ping
            super.forceQuickPing();
        } finally {
            serverListUpdateInProgress.set(false); }}}public void setServersList(List lsrv) {
    // BaseLoadBalancer
    super.setServersList(lsrv);

    // Update Server to LoadBalancerStats statistics....
}
Copy the code

AllServerList, the data structure that stores all servers, is a thread-safe container with synchronized. SetServersList simply replaces allServerList with the resulting list of servers.


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>();
    / / write locks
    writeLock.lock();
    try {
        // The for loop transfers the Server in LSRV to allServers
        ArrayList<Server> allServers = new ArrayList<Server>();
        for (Object server : lsrv) {
            if (server instanceof String) {
            	// Static configuration of the IP :port format
                server = new Server((String) server);
            }
            if (server instanceof Server) {
            	// The Server synchronized from EurekaClient
                allServers.add((Server) server);
            } else {
                throw new IllegalArgumentException("Type String or Server expected, instead found:"+ server.getClass()); }}// If service warmup is enabled, start Server warmup...

        // Direct substitution
        allServerList = allServers;
        
        // Ping To check the Server status
        if (canSkipPing()) {
            for (Server s : allServerList) {
                s.setAlive(true);
            }
            upServerList = allServerList;
        } else if(listChanged) { forceQuickPing(); }}finally {
        // Release the write lockwriteLock.unlock(); }}Copy the code

Front chooseRoundRobinAfterFiltering to obtain a list of all Server is returning the allServerList.

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

Eureka Ribbon client configuration

Access to the Server component is ServerList, RibbonClientConfiguration configured in the default implementation class is ConfigurationBasedServerList. ConfigurationBasedServerList default from the configuration file, you can like this configuration service instance address, multiple Server addresses with commas.

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

But when combined with Eureka-client, which introduces spring-cloud-starter-Netflix-Eureka-client’s client-side dependencies, It will help us to introduce the spring – the cloud – netflix – eureka – client dependence, this package has a RibbonEurekaAutoConfiguration automation configuration class, It through @ RibbonClients annotation defines the Ribbon of global client configuration such as EurekaRibbonClientConfiguration.

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

Enter the EurekaRibbonClientConfiguration can see:

  • The default implementation class for IPing isNIWSDiscoveryPing.
  • ServerList default implementation class for DomainExtractingServerList, but DomainExtractingServerList at construction time into another typeDiscoveryEnabledNIWSServerListServerList. The name can be seen, presumably, DiscoveryEnabledNIWSServerList is obtained from EurekaClient Server components.
@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
    publicServerList<? > 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);
        returnserverList; }}Copy the code

Obtain the Server list from DiscoveryClient

In DynamicServerListLoadBalancer ServerList getUpdatedListOfServers method the whole amount for services through the list, in eureka – client environment, ServerList default implementation class for DomainExtractingServerList, then you have to look at it first getUpdatedListOfServers method.

It can be seen that DomainExtractingServerList with DomainExtractingServerList access service list first, and then according to the Ribbon client configuration to reconstruct the Server object to return. To obtain a list service core in DiscoveryEnabledNIWSServerList.

@Override
public List<DiscoveryEnabledServer> getUpdatedListOfServers(a) {
    // 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 is restructured according to the client configuration
    for (DiscoveryEnabledServer server : servers) {
        result.add(new DomainExtractingServer(server, isSecure, shouldUseIpAddr, this.approximateZoneFromHostname));
    }
    return result;
}
Copy the code

First look at the structure of the DiscoveryEnabledNIWSServerList initialization:

  • It’s mostly incomingProvider<EurekaClient>Used to get EurekaClient.
  • Also set the clientName clientName, and vipAddresses are also the clientName, which you’ll need later.
public DiscoveryEnabledNIWSServerList(IClientConfig clientConfig, Provider<EurekaClient> eurekaClientProvider) {
    this.eurekaClientProvider = eurekaClientProvider;
    initWithNiwsConfig(clientConfig);
}

@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
    // The client name is the service name
    clientName = clientConfig.getClientName();
    // vipAddresses get the client name
    vipAddresses = clientConfig.resolveDeploymentContextbasedVipAddresses();

    // Some other configurations....
}
Copy the code

Looking at the getUpdatedListOfServers that get the instance, you can see that its core logic is to get the InstanceInfo instance list from EurekaClient based on the service name, and then return the Server information wrapped around it.

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

private List<DiscoveryEnabledServer> obtainServersViaDiscovery(a) {
    List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();
    // Get EurekaClient. The actual type is CloudEurekaClient and its parent is DiscoveryClient
    EurekaClient eurekaClient = eurekaClientProvider.get();
    if(vipAddresses! =null) {// Split vipAddresses, default is the service name
        for (String vipAddress : vipAddresses.split(",")) {
            // Get instance information from EurekaClient based on the service name
            List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
            for (InstanceInfo ii : listOfInstanceInfo) {
                if (ii.getStatus().equals(InstanceStatus.UP)) {
                    // Create a Server based on InstanceInfoDiscoveryEnabledServer des = createServer(ii, isSecure, shouldUseIpAddr); serverList.add(des); }}}}return serverList;
}
Copy the code

Note that vipAddress is the service name:

Finally, the getInstancesByVipAddress of EurekaClient is clearly a list of all instances from the service name in the native application Applications of DiscoveryClient.

Eureka-client fully captures the registry and increments capture the registry every 30 seconds. These are incorporated into native Applications. When Ribbon is combined with Eureka, the Ribbon retrieves the Server list from DiscoveryClient’s Applications.

public List<InstanceInfo> getInstancesByVipAddress(String vipAddress, boolean secure, String region) {
    // ...
    Applications applications;
    if (instanceRegionChecker.isLocalRegion(region)) {
        // Take the local application
        applications = this.localRegionApps.get();
    } else {
        applications = remoteRegionVsApps.get(region);
        if (null == applications) {
            returnCollections.emptyList(); }}if(! secure) {// Return the instance corresponding to the service name
        return applications.getInstancesByVirtualHostName(vipAddress);
    } else {
        returnapplications.getInstancesBySecureVirtualHostName(vipAddress); }}Copy the code

Update the Server list periodically

DynamicServerListLoadBalancer initialization time, didn’t say there is a way, is enableAndInitLearnNewServersFeature (). This method simply calls the ServerListUpdater to launch an UpdateAction, which in turn simply calls the updateListOfServers method, which is the logic of fully updating the Server described earlier.

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

protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
    @Override
    public void doUpdate(a) {
        / / call updateListOfServersupdateListOfServers(); }};Copy the code

PollingServerListUpdater PollingServerListUpdater PollingServerListUpdater PollingServerListUpdater PollingServerListUpdater PollingServerListUpdater

It’s basically calling the updateListOfServers method at a fixed frequency of 30 seconds, Synchronize instances cached in Applications in DiscoveryClient to the allServerList list in ILoadBalancer.

public synchronized void start(final UpdateAction updateAction) {
    if (isActive.compareAndSet(false.true)) {
        final Runnable wrapperRunnable = new Runnable() {
            @Override
            public void run(a) {
                // Perform an updateListOfServers
                updateAction.doUpdate();
                // Set the last update timelastUpdated = System.currentTimeMillis(); }};// Fixed frequency scheduling
        scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                wrapperRunnable,
                initialDelayMs, / / the default 1000
                refreshIntervalMs, // The default is 30 x 1000
                TimeUnit.MILLISECONDS
        );
    } else {
        logger.info("Already active, no-op"); }}Copy the code

How long does it take the Ribbon client to register a new instance?

  • First DiscoveryClient every other30 secondsFetching an incremental registry from the registry first reads from the read-only cache
  • Then eureka server registry is designed with a three-tier cache structure, every other30 secondsSynchronize from readWriteMap to readOnlyMap
  • After instances of the registry are synchronized locally to the client, the Ribbon will interval them30 secondsSynchronize from DiscoveryClient to BaseLoadBalancer

To summarize, this means that the Ribbon load balancing may take up to 90 seconds to detect new instances registered in the registry.

Check whether the Server is alive

IPing has not seen how this works when it creates ILoadBalancer. On initialization, you can see that you basically set up the current ping and then reset a scheduling task to schedule PingTask every 30 seconds by default.

public void setPing(IPing ping) {
    if(ping ! =null) {
        if(! ping.equals(this.ping)) {
            this.ping = ping;
            // Set the Ping tasksetupPingTask(); }}else {
        this.ping = null;
        // cancel the timer tasklbTimer.cancel(); }}void setupPingTask(a) {
    // ...
    // Create a timer scheduler
    lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name, true);
    // pingIntervalTime defaults to 30 seconds, and PingTask is scheduled every 30 seconds
    lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
    // Initiate Ping immediately
    forceQuickPing();
}
Copy the code

ShutdownEnabledTimer is a descendant of Timer. It registers a callback with the Runtime at creation time to cancel the Timer execution when the JVM is shut down, freeing resources.

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

    public ShutdownEnabledTimer(String name, boolean daemon) {
        super(name, daemon);
        this.name = name;
        // Cancel the timer thread
        this.cancelThread = new Thread(new Runnable() {
            public void run(a) {
                ShutdownEnabledTimer.super.cancel(); }});// Register a hook with the Runtime that cancelThread cancels scheduled tasks when the JVM shuts down
        Runtime.getRuntime().addShutdownHook(this.cancelThread);
    }

    @Override
    public void cancel(a) {
        super.cancel();
        try {
            // Remove the callback
            Runtime.getRuntime().removeShutdownHook(this.cancelThread);
        } catch (IllegalStateException ise) {
            LOGGER.info("Exception caught (might be ok if at shutdown)", ise); }}}Copy the code

The core logic of PingTask is to iterate through the list of allServers, use IPingStrategy and IPing to determine whether the Server is alive and update the status of the Server. And update all live servers to the upServerList, which caches all live servers.

class PingTask extends TimerTask {
    public void run(a) {
        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(a) 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();
            // Add lock to allServerList
            allServers = allServerList.toArray(new Server[allServerList.size()]);
            allLock.unlock();

            int numCandidates = allServers.length;
            Use IPingStrategy and IPing to ping all servers
            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();
                // Set alive to whether to live
                svr.setAlive(isAlive);

                // Instance change
                if(oldIsAlive ! = isAlive) { changedServers.add(svr); }// Add a living Server
                if (isAlive) {
                    newUpList.add(svr);
                }
            }
            upLock = upServerLock.writeLock();
            upLock.lock();
            // Update the upServerList. The upServerList stores only the surviving servers
            upServerList = newUpList;
            upLock.unlock();
            // Notify the change
            notifyServerStatusChangeListener(changedServers);
        } finally {
            pingInProgress.set(false); }}}Copy the code

The default implementation class of IPingStrategy is SerialPingStrategy, which simply iterates through all servers and uses IPing to determine whether the Server is alive or not.

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) {
                    // Run IPing to check whether the Server is aliveresults[i] = ping.isAlive(servers[i]); }}catch (Exception e) {
                logger.error("Exception while pinging Server: '{}'", servers[i], e); }}returnresults; }}Copy the code

After eureka-client is integrated, the default implementation class of IPing is NIWSDiscoveryPing. The isAlive method is used to determine whether the InstanceInfo of the corresponding Server is in UP state. The UP state indicates that the Server is alive.

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) {// Check whether the Server instance is UPisAlive = status.equals(InstanceStatus.UP); }}}return isAlive;
}
Copy the code

One diagram summarizes the core principles of the Ribbon

1. Summary of Ribbon core working principles

First, the seven core interfaces of the Ribbon define the Ribbon’s behavior. They are the core framework of the Ribbon.

  • Use the Ribbon to load balance clients@LoadBalancedThe annotation annotates a RestTemplate bean object, followed by theLoadBalancerAutoConfigurationThe RestTemplate with the @loadBalanced annotation is added to the configuration classLoadBalancerInterceptorThe interceptor.
  • The LoadBalancerInterceptor intercepts HTTP requests from RestTemplate, binds the request to the Ribbon load balancing lifecycle, and then uses theLoadBalancerClientexecuteMethod to process the request.
  • The LoadBalancerClient first gets oneILoadBalancer, and then use it to get a Server, which is the information encapsulation of a specific instance. Once you have the Server, you reconstruct the original URI using the IP and port of the Server.
  • ILoadBalancer will eventually pass when the instance is selectedIRuleBalancing policy to select a Server.
  • ILoadBalancer has one of its parents, BaseLoadBalancerallServerListTables cache all servers. The source of servers in the Ribbon is allServerList.
  • ILoadBalancer is used when loading the Ribbon client contextServerListFrom the DiscoveryClientApplicationsTo obtain the list of instances corresponding to the client, and then useServerListFilterFilter and finally update to allServerList.
  • ILoadBalancer also starts a background taskServerListUpdaterevery30 secondsRun once to synchronize the list of instances in DiscoveryClient’s Applications to allServerList with ServerList.
  • ILoadBalancer also starts a background taskPingTaskevery30 secondsRun the IPing command once to check the Server status. In EurekaClient environment, the IPing command is used to check the Server statusInstanceInfoIs the state ofUP.

2. The following figure summarizes the core process of acquiring the Server through the Ribbon and the relationship between the corresponding core interfaces.