In depth analysis of source code: how do Eureka and Ribbon do service discovery?

Posted by gpittingale on Sat, 22 Jan 2022 16:02:26 +0100

This article is based on spring cloud dalston, and the article is long. Please choose a comfortable posture to read.

What are Eureka and Ribbon? What does it have to do with service discovery?

Eureka and Ribbon are microservice components provided by Netflix, which are used for service registration and discovery and load balancing respectively. At the same time, both belong to the spring cloud netflix system and are seamlessly integrated with spring cloud, which is also known by everyone.

Eureka itself is a service registration and discovery component, which implements a complete Service Registry and Service Discovery.

Ribbon is a load balancing component. What does it have to do with service discovery? Load balancing is next to service discovery in the whole microservice invocation model, and the ribbon framework actually acts as a bridge between the developer's service consumption behavior and the underlying service discovery component Eureka. Strictly speaking, ribbon does not do service discovery, but due to the loose coupling of Netflix components, ribbon needs to perform a behavior similar to "service discovery" on Eureka's cache service list, so as to build its own load balancing list and update it in time, that is, the object of "service discovery" in ribbon becomes Eureka (or other service discovery components).

Eureka's service registration and discovery

We will first describe Eureka's service discovery, focusing on how Eureka client registers and discovers services. At the same time, we will not stay in Eureka's architecture, Eureka server implementation, Zone/Region and other categories.

Eureka client's service discovery is implemented by the DiscoveryClient class, which mainly includes the following functions:

  • Register a service instance with Eureka server
  • Update lease term in Eureka server
  • Cancel the lease on Eureka server (service offline)
  • Discover service instances and update them regularly

Service registration

All scheduled tasks of DiscoveryClient are in the initScheduledTasks() method. We can see the following key codes:

private void initScheduledTasks() {
    ...
    if (clientConfig.shouldRegisterWithEureka()) {
        ...
         // InstanceInfo replicator
        instanceInfoReplicator = new InstanceInfoReplicator(
                this,
                instanceInfo,
                clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                2); // burstSize
        ...
        instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
    }
}

We can see that an instanceInfoReplicator instance is created in the if judgment branch, which will execute a scheduled task through start:

public void run() {
        try {
            discoveryClient.refreshInstanceInfo();

            Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
            if (dirtyTimestamp != null) {
                discoveryClient.register();
                instanceInfo.unsetIsDirty(dirtyTimestamp);
            }
        } catch (Throwable t) {
            logger.warn("There was a problem with the instance info replicator", t);
        } finally {
            Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
            scheduledPeriodicRef.set(next);
        }
    }

We can find this paragraph in the run() method of the InstanceInfoReplicator class. At the same time, we can find that the key point of its registration is discoveryclient Let's click register() to see:

boolean register() throws Throwable {
        logger.info(PREFIX + appPathIdentifier + ": registering service...");
        EurekaHttpResponse<Void> httpResponse;
        try {
            httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
        } catch (Exception e) {
            logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
            throw e;
        }
        if (logger.isInfoEnabled()) {
            logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
        }
        return httpResponse.getStatusCode() == 204;
    }

It can be found here that the instanceinfo instance information is registered on Eureka server through HTTP REST (jersey client) request. Let's take a brief look at the instanceinfo object. The attributes can basically see the name and meaning:

@JsonCreator
    public InstanceInfo(
            @JsonProperty("instanceId") String instanceId,
            @JsonProperty("app") String appName,
            @JsonProperty("appGroupName") String appGroupName,
            @JsonProperty("ipAddr") String ipAddr,
            @JsonProperty("sid") String sid,
            @JsonProperty("port") PortWrapper port,
            @JsonProperty("securePort") PortWrapper securePort,
            @JsonProperty("homePageUrl") String homePageUrl,
            @JsonProperty("statusPageUrl") String statusPageUrl,
            @JsonProperty("healthCheckUrl") String healthCheckUrl,
            @JsonProperty("secureHealthCheckUrl") String secureHealthCheckUrl,
            @JsonProperty("vipAddress") String vipAddress,
            @JsonProperty("secureVipAddress") String secureVipAddress,
            @JsonProperty("countryId") int countryId,
            @JsonProperty("dataCenterInfo") DataCenterInfo dataCenterInfo,
            @JsonProperty("hostName") String hostName,
            @JsonProperty("status") InstanceStatus status,
            @JsonProperty("overriddenstatus") InstanceStatus overriddenstatus,
            @JsonProperty("leaseInfo") LeaseInfo leaseInfo,
            @JsonProperty("isCoordinatingDiscoveryServer") Boolean isCoordinatingDiscoveryServer,
            @JsonProperty("metadata") HashMap<String, String> metadata,
            @JsonProperty("lastUpdatedTimestamp") Long lastUpdatedTimestamp,
            @JsonProperty("lastDirtyTimestamp") Long lastDirtyTimestamp,
            @JsonProperty("actionType") ActionType actionType,
            @JsonProperty("asgName") String asgName) {
        this.instanceId = instanceId;
        this.sid = sid;
        this.appName = StringCache.intern(appName);
        this.appGroupName = StringCache.intern(appGroupName);
        this.ipAddr = ipAddr;
        this.port = port == null ? 0 : port.getPort();
        this.isUnsecurePortEnabled = port != null && port.isEnabled();
        this.securePort = securePort == null ? 0 : securePort.getPort();
        this.isSecurePortEnabled = securePort != null && securePort.isEnabled();
        this.homePageUrl = homePageUrl;
        this.statusPageUrl = statusPageUrl;
        this.healthCheckUrl = healthCheckUrl;
        this.secureHealthCheckUrl = secureHealthCheckUrl;
        this.vipAddress = StringCache.intern(vipAddress);
        this.secureVipAddress = StringCache.intern(secureVipAddress);
        this.countryId = countryId;
        this.dataCenterInfo = dataCenterInfo;
        this.hostName = hostName;
        this.status = status;
        this.overriddenstatus = overriddenstatus;
        this.leaseInfo = leaseInfo;
        this.isCoordinatingDiscoveryServer = isCoordinatingDiscoveryServer;
        this.lastUpdatedTimestamp = lastUpdatedTimestamp;
        this.lastDirtyTimestamp = lastDirtyTimestamp;
        this.actionType = actionType;
        this.asgName = StringCache.intern(asgName);

        // ---------------------------------------------------------------
        // for compatibility

        if (metadata == null) {
            this.metadata = Collections.emptyMap();
        } else if (metadata.size() == 1) {
            this.metadata = removeMetadataMapLegacyValues(metadata);
        } else {
            this.metadata = metadata;
        }

        if (sid == null) {
            this.sid = SID_DEFAULT;
        }
    }

The whole process is summarized as follows:

Service renewal

Service renewal may be obscure. In fact, it is to make regular calls on the client side to let eureka server know that it is still alive. The comments in eureka code are interpreted as heart beat.

Here are two important configurations to note:

  • instance.leaseRenewalIntervalInSeconds
    Indicates the update frequency of the client. The default is 30s, that is, the update operation will be initiated to Eureka server every 30s.
  • instance.leaseExpirationDurationInSeconds
    This is the expiration time of the server perspective. The default is 90s, that is, Eureka server will reject the renew operation from the client if it does not receive it within 90s.

Let's look directly from the code point of view. Similarly, the related scheduled tasks are in the initScheduledTasks() method:

private void initScheduledTasks() {
    ...
    if (clientConfig.shouldRegisterWithEureka()) {
        ...
         // Heartbeat timer
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "heartbeat",
                            scheduler,
                            heartbeatExecutor,
                            renewalIntervalInSecs,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new HeartbeatThread()
                    ),
                    renewalIntervalInSecs, TimeUnit.SECONDS);
        ...
    }
}

You can see that a HeartbeatThread() thread is created here to perform operations:

private class HeartbeatThread implements Runnable {

        public void run() {
            if (renew()) {
                lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
            }
        }
    }

Let's look directly at the renew() method:

    boolean renew() {
        EurekaHttpResponse<InstanceInfo> httpResponse;
        try {
            httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
            logger.debug("{} - Heartbeat status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
            if (httpResponse.getStatusCode() == 404) {
                REREGISTER_COUNTER.increment();
                logger.info("{} - Re-registering apps/{}", PREFIX + appPathIdentifier, instanceInfo.getAppName());
                return register();
            }
            return httpResponse.getStatusCode() == 200;
        } catch (Throwable e) {
            logger.error("{} - was unable to send heartbeat!", PREFIX + appPathIdentifier, e);
            return false;
        }
    }

It is relatively simple here. It can be found that it is similar to service registration. It also uses HTTP REST to initiate a heartbeat request, and the bottom layer uses the jersey client.
The whole process is summarized as follows:

Service logoff

The service logoff logic is relatively simple. It is not triggered in the scheduled task itself. Instead, it is triggered by calling the shutdown method by marking the method @ PreDestroy. Finally, it will call the unRegister() method to logoff. Similarly, this is also an HTTP REST request. You can simply see the following Code:

    @PreDestroy
    @Override
    public synchronized void shutdown() {
        if (isShutdown.compareAndSet(false, true)) {
            logger.info("Shutting down DiscoveryClient ...");

            if (statusChangeListener != null && applicationInfoManager != null) {
                applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
            }

            cancelScheduledTasks();

            // If APPINFO was registered
            if (applicationInfoManager != null && clientConfig.shouldRegisterWithEureka()) {
                applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
                unregister();
            }

            if (eurekaTransport != null) {
                eurekaTransport.shutdown();
            }

            heartbeatStalenessMonitor.shutdown();
            registryStalenessMonitor.shutdown();

            logger.info("Completed shut down of DiscoveryClient");
        }
    }

    /**
     * unregister w/ the eureka service.
     */
    void unregister() {
        // It can be null if shouldRegisterWithEureka == false
        if(eurekaTransport != null && eurekaTransport.registrationClient != null) {
            try {
                logger.info("Unregistering ...");
                EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());
                logger.info(PREFIX + appPathIdentifier + " - deregister  status: " + httpResponse.getStatusCode());
            } catch (Exception e) {
                logger.error(PREFIX + appPathIdentifier + " - de-registration failed" + e.getMessage(), e);
            }
        }
    }

Service discovery and update

Let's look at the key logic as a service consumer, that is, discovering services and updating services.

First, the consumer will get all the service lists from Eureka server at startup and cache them locally. At the same time, because there is a local cache, it needs to be updated regularly, and the update frequency can be configured.

At startup, the fetchRegistry() method will be called in discoveryClient by the consumer:

private boolean fetchRegistry(boolean forceFullRegistryFetch) {
            ...
            if (clientConfig.shouldDisableDelta()
                    || (!Strings.isNullOrEmpty(clientConfig.getRegistryRefreshSingleVipAddress()))
                    || forceFullRegistryFetch
                    || (applications == null)
                    || (applications.getRegisteredApplications().size() == 0)
                    || (applications.getVersion() == -1)) //Client application does not have latest library supporting delta
            {
                ...
                getAndStoreFullRegistry();
            } else {
                getAndUpdateDelta(applications);
            }
            ...
}

Here you can see that there are two judgment branches in fetchRegistry, corresponding to the first update and subsequent updates. The first update will call the getAndStoreFullRegistry() method. Let's take a look:

 private void getAndStoreFullRegistry() throws Throwable {
        long currentUpdateGeneration = fetchRegistryGeneration.get();

        logger.info("Getting all instance registry info from the eureka server");

        Applications apps = null;
        EurekaHttpResponse<Applications> httpResponse = clientConfig.getRegistryRefreshSingleVipAddress() == null
                ? eurekaTransport.queryClient.getApplications(remoteRegionsRef.get())
                : eurekaTransport.queryClient.getVip(clientConfig.getRegistryRefreshSingleVipAddress(), remoteRegionsRef.get());
        if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
            apps = httpResponse.getEntity();
        }
        logger.info("The response status is {}", httpResponse.getStatusCode());

        if (apps == null) {
            logger.error("The application is null for some reason. Not storing this information");
        } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
            localRegionApps.set(this.filterAndShuffle(apps));
            logger.debug("Got full registry with apps hashcode {}", apps.getAppsHashCode());
        } else {
            logger.warn("Not updating applications as another thread is updating it already");
        }
    }

It can be seen that similar to before, if there is no special specification, we will launch an HTTP REST request to pull the information of all Applications and cache it. The cache object is Applications. You can check it further if you are interested.

Next, in the familiar initScheduledTasks() method, we will also start a task to update the application information cache:

private void initScheduledTasks() {
        if (clientConfig.shouldFetchRegistry()) {
            // registry cache refresh timer
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "cacheRefresh",
                            scheduler,
                            cacheRefreshExecutor,
                            registryFetchIntervalSeconds,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new CacheRefreshThread()
                    ),
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }
        ...
}

In the run method of the task CacheRefreshThread(), we will still call the fetchRegistry() method before us. At the same time, when judging, we will go to another branch, that is, call the getAndUpdateDelta() method:

private void getAndUpdateDelta(Applications applications) throws Throwable {
        long currentUpdateGeneration = fetchRegistryGeneration.get();

        Applications delta = null;
        EurekaHttpResponse<Applications> httpResponse = eurekaTransport.queryClient.getDelta(remoteRegionsRef.get());
        if (httpResponse.getStatusCode() == Status.OK.getStatusCode()) {
            delta = httpResponse.getEntity();
        }

        if (delta == null) {
            logger.warn("The server does not allow the delta revision to be applied because it is not safe. "
                    + "Hence got the full registry.");
            getAndStoreFullRegistry();
        } else if (fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1)) {
            logger.debug("Got delta update with apps hashcode {}", delta.getAppsHashCode());
            String reconcileHashCode = "";
            if (fetchRegistryUpdateLock.tryLock()) {
                try {
                    updateDelta(delta);
                    reconcileHashCode = getReconcileHashCode(applications);
                } finally {
                    fetchRegistryUpdateLock.unlock();
                }
            } else {
                logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
            }
            // There is a diff in number of instances for some reason
            if (!reconcileHashCode.equals(delta.getAppsHashCode()) || clientConfig.shouldLogDeltaDiff()) {
                reconcileAndLogDifference(delta, reconcileHashCode);  // this makes a remoteCall
            }
        } else {
            logger.warn("Not updating application delta as another thread is updating it already");
            logger.debug("Ignoring delta update with apps hashcode {}, as another thread is updating it already", delta.getAppsHashCode());
        }
    }

As you can see, HTTP REST is used to initiate a getDelta request, and the local Applications cache object will be updated in the updateDelta() method.

To sum up, the whole process of service discovery and update is as follows:

Ribbon's "service discovery"

Next, let's take a look at how Ribbon performs "service discovery" based on Eureka. As we said before, "service discovery" here is not service discovery in the strict sense, but how Ribbon builds its own load balancing list based on Eureka and updates it in time, At the same time, we do not pay attention to other specific logic of Ribbon load balancing (including IRule routing and IPing judging availability).

We can make some conjectures first. Firstly, Ribbon must be based on Eureka's service discovery. As described above, Eureka will pull all service information into the local cache Applications, so Ribbon must build the load balancing list based on the Applications cache. At the same time, the load balancing list also needs a regular update mechanism to ensure consistency.

Service call

First, from the initial use of the developer, the developer can open the Ribbon logic by opening the @ LoadBalanced annotation on the RestTemplate. Obviously, this is a method similar to interception. stay
In the loadbalancenautoconfiguration class, we can see the relevant codes:

...
@Bean
	public SmartInitializingSingleton loadBalancedRestTemplateInitializer(
			final List<RestTemplateCustomizer> customizers) {
		return new SmartInitializingSingleton() {
			@Override
			public void afterSingletonsInstantiated() {
				for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
					for (RestTemplateCustomizer customizer : customizers) {
						customizer.customize(restTemplate);
					}
				}
			}
		};
	}


	@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 new RestTemplateCustomizer() {
				@Override
				public void customize(RestTemplate restTemplate) {
					List<ClientHttpRequestInterceptor> list = new ArrayList<>(
							restTemplate.getInterceptors());
					list.add(loadBalancerInterceptor);
					restTemplate.setInterceptors(list);
				}
			};
		}
	}
...

You can see that during initialization, the interceptor LoadBalancerInterceptor is added to RestTemplate by calling the customize() method. LoadBalancerInterceptor uses loadBalancer(RibbonLoadBalancerClient class) in the interception method to complete the request call:

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

Service discovery

So far, our request call has been encapsulated by the RibbonLoadBalancerClient, and its "service discovery" also occurs in the RibbonLoadBalancerClient.

We click on its execute() method:

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

Here, an ILoadBalancer is built according to the serviceId, and the final instance server information is obtained from the loadBalancer. ILoadBalancer is an interface that defines load balancing. Its key method, chooseServer(), is to select a server from the load balancing list according to the routing rules. Of course, our main concern is how the load balancing list is built. Through the source code tracking, we find that after the ILoadBalancer object is built through the getLoadBalancer() method, the service list has been included in the object. So let's take a look at how the ILoadBalancer object is created:

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

So this is actually the clientFactory encapsulated by spring cloud, which will find the corresponding bean in the applicationContext container.

Through source code tracing, we can find the corresponding code in the automatic configuration class RibbonClientConfiguration:

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

We see that ILoadBalancer is finally built here. Its implementation class is ZoneAwareLoadBalancer. We observe the initialization of its superclass:

    public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
                                         ServerList<T> serverList, ServerListFilter<T> filter,
                                         ServerListUpdater serverListUpdater) {
        super(clientConfig, rule, ping);
        this.serverListImpl = serverList;
        this.filter = filter;
        this.serverListUpdater = serverListUpdater;
        if (filter instanceof AbstractServerListFilter) {
            ((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
        }
        restOfInit(clientConfig);
    }

Finally, restOfInit() method is executed to further trace:

    void restOfInit(IClientConfig clientConfig) {
        boolean primeConnection = this.isEnablePrimingConnections();
        // turn this off to avoid duplicated asynchronous priming done in BaseLoadBalancer.setServerList()
        this.setEnablePrimingConnections(false);
        enableAndInitLearnNewServersFeature();

        updateListOfServers();
        if (primeConnection && this.getPrimeConnections() != null) {
            this.getPrimeConnections()
                    .primeConnections(getReachableServers());
        }
        this.setEnablePrimingConnections(primeConnection);
        LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
    }

The updateListOfServers() method is to obtain all the serverlists, which are ultimately determined by the
serverListImpl.getUpdatedListOfServers() gets the list of all services. In this serverlistimpl, the implementation class is DiscoveryEnabledNIWSServerList. The DiscoveryEnabledNIWSServerList has getInitialListOfServers() and getUpdatedListOfServers() methods. The specific code is as follows

    @Override
    public List<DiscoveryEnabledServer> getInitialListOfServers(){
        return obtainServersViaDiscovery();
    }

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

At this time, we can see that the obtainserverviadiscovery() method is basically close to the essence of things. It creates an EurekaClient object, which is Eureka's DiscoveryClient implementation class, calls its getInstancesByVipAddress() method, and finally selects the corresponding service information from the Applications cache of DiscoveryClient according to the serviceId:

    private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
        List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();

        if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
            logger.warn("EurekaClient has not been initialized yet, returning an empty list");
            return new ArrayList<DiscoveryEnabledServer>();
        }

        EurekaClient eurekaClient = eurekaClientProvider.get();
        if (vipAddresses!=null){
            for (String vipAddress : vipAddresses.split(",")) {
                // if targetRegion is null, it will be interpreted as the same region of client
                List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
                for (InstanceInfo ii : listOfInstanceInfo) {
                    if (ii.getStatus().equals(InstanceStatus.UP)) {

                        if(shouldUseOverridePort){
                            if(logger.isDebugEnabled()){
                                logger.debug("Overriding port on client name: " + clientName + " to " + overridePort);
                            }

                            // copy is necessary since the InstanceInfo builder just uses the original reference,
                            // and we don't want to corrupt the global eureka copy of the object which may be
                            // used by other clients in our system
                            InstanceInfo copy = new InstanceInfo(ii);

                            if(isSecure){
                                ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build();
                            }else{
                                ii = new InstanceInfo.Builder(copy).setPort(overridePort).build();
                            }
                        }

                        DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, isSecure, shouldUseIpAddr);
                        des.setZone(DiscoveryClient.getZone(ii));
                        serverList.add(des);
                    }
                }
                if (serverList.size()>0 && prioritizeVipAddressBasedServers){
                    break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers
                }
            }
        }
        return serverList;
    }

Service update

We already know how the Ribbon completes the construction of the load balancing list in combination with Eureka during the initial startup. Similar to Eureka, we also need to update the service list in time to ensure consistency.

When building the ILoadBalancer in the RibbonClientConfiguration auto configuration class, we can see that there is a ServerListUpdater object in its constructor, which is also built in the current class:

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

We observe the start() method in this object to see how the update is completed:

@Override
    public synchronized void start(final UpdateAction updateAction) {
        if (isActive.compareAndSet(false, true)) {
            final Runnable wrapperRunnable = new Runnable() {
                @Override
                public void run() {
                    if (!isActive.get()) {
                        if (scheduledFuture != null) {
                            scheduledFuture.cancel(true);
                        }
                        return;
                    }
                    try {
                        updateAction.doUpdate();
                        lastUpdated = System.currentTimeMillis();
                    } catch (Exception e) {
                        logger.warn("Failed one update cycle", e);
                    }
                }
            };

            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                    wrapperRunnable,
                    initialDelayMs,
                    refreshIntervalMs,
                    TimeUnit.MILLISECONDS
            );
        } else {
            logger.info("Already active, no-op");
        }
    }

There are two configurations here, that is, initialDelayMs first detection defaults to 1s, and refreshIntervalMs detection interval defaults to 30s (consistent with Eureka). A scheduled task is created to execute updateaction Doupdate() method.

Let's go back to the previous restOfInit() method and look at the
Enableandinitrlearnnewserversfeature() method, you can see that the start method of ServerListUpdater is triggered here, and the updateAction object is passed in:

    public void enableAndInitLearnNewServersFeature() {
        LOGGER.info("Using serverListUpdater {}", serverListUpdater.getClass().getSimpleName());
        serverListUpdater.start(updateAction);
    }

In fact, updateAction has been created from the beginning. It still calls the previous updateListOfServers method for subsequent updates:

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

To sum up, the overall process of Ribbon's three parts of service discovery is as follows:

Topics: Python Java Spring Spring Boot Back-end