Eureka Client service discovery principle

Posted by surreal5335 on Mon, 03 Jan 2022 18:32:49 +0100

 

1, Eureka Client's work

  1. Application startup phase
    1. Read the configuration information interacting with Eureka Server and package it into Eureka clientconfig
    2. Read its own service instance configuration information and package it into EurekaInstanceConfig
    3. Pull the registry information from Eureka Server and cache it locally
    4. Service registration
    5. Initialize the scheduled tasks of sending heartbeat, cache refresh (pull registry information to update local cache) and on-demand registration (monitor the change of service instance information, decide whether to re initiate registration, and update service instance metadata in the registry)
  2. Application execution phase
    1. Regularly send heartbeat to Eureka Server and maintain the lease in the registry.
    2. Periodically pull the registry information from Eureka Server and update the local registry cache.
    3. Monitor the change of the application's own information. If it changes, it is necessary to re initiate the service registration.
  3. Application destruction phase
    1. Unregister its own service instance from Eureka Server.

2, Source code analysis

Eureka Client introduces dependencies through Starter. Spring Boot will use the following automatic configuration classes for the project:

1. EurekaClientAutoConfiguration: Eureka Client automatic configuration class, which is responsible for the configuration and initialization of key Beans in Eureka Client, such as ApplicationInfoManager and EurekaClientConfig.

2. Ribbon Eureka autoconfiguration: configuration related to ribbon load balancing.

3. Eureka discovery client configuration: configure the health checker for automatic registration and application.

2.1 read application configuration information

Spring Boot helps Eureka Client read and configure the properties of many necessary beans through Eureka discoveryclientconfig configuration class. Now list the property reading and configuration classes in Eureka discoveryclientconfiguration.

Class name

Role and introduction

EurekaClientConfig

Encapsulate the configuration information required for the interaction between Eureka Client and Eureka Server. Spring Cloud provides EurekaClientConfigBean with a default configuration class. You can use the prefix Eureka. Config in the configuration file Client + attribute name for attribute override

ApplicationInfoManager

As the application information manager, it manages the information class InstanceInfo of the service instance and the configuration information class EurekaInstanceConfig of the service instance

InstanceInfo

Encapsulates the service instance metadata that will be sent to Eureka Server for service registration. It represents a service instance in the registration of Eureka Server. Other service instances can know the relevant information of the service instance through InstanceInfo to initiate service requests

EurekaInstanceConfig

Encapsulates the configuration information of Eureka Client's own service instance, which is mainly used to build InstanceInfo. Usually, this information is contained in eureka.com in the configuration file Set under the instance prefix. Spring Cloud provides the default configuration through the EurekaInstanceConfigBean configuration class

DiscoveryClient

The Spring Cloud defines a client interface for service discovery

2.2 service discovery client

2.1. 1. Responsibilities of discoveryclient

DiscoveryClient is the core class of Eureka Client, including the key logic for interacting with Eureka Server. It has the following functions:

  1. Register the service instance to Eureka Server
  2. Send heartbeat update lease with Eureka Server
  3. Cancel the lease from Eureka Server when the service is shut down, and the service goes offline
  4. Query the list of service instances registered in Eureka Server

2.1.2 DiscoveryClient constructor

Initializes timing tasks such as sending heartbeat and cache refresh in the constructor

// com.netflix.discovery.DiscoveryClient#DiscoveryClient()
// The corresponding configuration is Eureka Client. Fetch register, true indicates that Eureka Client will pull registry information from Eureka Server
if (config.shouldFetchRegistry()) {
    this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
    this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}
// The corresponding configuration is Eureka Client. Register with Eureka, true indicates that Eureka Client will register with Eureka Server
if (config.shouldRegisterWithEureka()) {
    this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
} else {
    this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
}

Then define a timer thread pool ScheduledExecutorService based on thread pool. The thread pool size is 2. One thread is used to send heartbeat and the other thread is used for cache refresh. At the same time, define the send heartbeat and cache refresh thread pool. The code is as follows:

// com.netflix.discovery.DiscoveryClient#DiscoveryClient()

scheduler = Executors.newScheduledThreadPool(2,
        new ThreadFactoryBuilder()
                .setNameFormat("DiscoveryClient-%d")
                .setDaemon(true)
                .build());

heartbeatExecutor = new ThreadPoolExecutor(
        1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>(),
        new ThreadFactoryBuilder()
                .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
                .setDaemon(true)
                .build()
);  // use direct handoff

cacheRefreshExecutor = new ThreadPoolExecutor(...);

// Internal class that encapsulates the Jersey client for http calls
eurekaTransport = new EurekaTransport();
scheduleServerEndpointTask(eurekaTransport, args);

Then pull the registry information from Eureka Server. The code is as follows:

if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
    fetchRegistryFromBackup();
}

After pulling the registry information in Eureka Server, the service instance will be registered. The code is as follows:

if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
    try {
        // Initiate service registration
        if (!register() ) {
            throw new IllegalStateException("Registration error at startup. Invalid server response.");
        }
    } catch (Throwable th) {
        logger.error("Registration error at startup: {}", th.getMessage());
        throw new IllegalStateException(th);
    }
}
// Initialize scheduled tasks
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
initScheduledTasks();

Finally, in the constructor of DiscoveryClient, we mainly do the following things in turn:

  1. Assignment of related configurations, such as ApplicationInfoManager, EurekaClientConfig, etc
  2. The initialization of the backup registry is not implemented by default
  3. Pull the information from the Eureka Server registry
  4. Preprocessing before registration
  5. Register yourself with Eureka Server
  6. Initialize timing tasks such as heartbeat, cache refresh and on-demand registration

2.3 pull registry information

com.netflix.discovery.DiscoveryClient#fetchRegistry

private boolean fetchRegistry(boolean forceFullRegistryFetch) {
    Stopwatch tracer = FETCH_REGISTRY_TIMER.start();

    try {
        //If incremental pull is disabled or Applications is null, all pull is performed
        // If the delta is disabled or if it is the first time, get all
        // applications
        Applications applications = getApplications();

        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
        {
            // Full pull registry information
            getAndStoreFullRegistry();
        } else {
            // Incremental pull registry information
            getAndUpdateDelta(applications);
        }
        // Compute application set consistency hash code
        applications.setAppsHashCode(applications.getReconcileHashCode());
        // Print the total number of all service instances on the registry
        logTotalInstances();
    } catch (Throwable e) {
        logger.error(PREFIX + "{} - was unable to refresh its cache! status = {}", appPathIdentifier, e.getMessage(), e);
        return false;
    } finally {
        if (tracer != null) {
            tracer.stop();
        }
    }
    // The cache refresh event is pushed before the remote instance state is updated, but Eureka does not provide a default event listener
    // Notify about cache refresh before updating the instance remote status
    onCacheRefreshed();
    
    // Update the remote instance status based on the flushed data in the cache
    // Update remote status based on refreshed data held in the cache
    updateInstanceRemoteStatus();

    // registry was fetched successfully, so return true
    return true;
}

2.3. 1. Pull the registry information in full

com.netflix.discovery.DiscoveryClient#getAndStoreFullRegistry

Interface: http://localhost:20000/eureka/apps/

getAndStoreFullRegistry method may be called by multiple threads at the same time, resulting in the newly pulled registry being overwritten by the old registry and generating dirty data. For this, Eureka tracks the updated version of apps through currentUpdateGeneration of AtomicLong type. If the updated versions are inconsistent, the registration information pulled this time is outdated and does not need to be saved locally. After pulling the registry information, the obtained apps will be filtered, and only the service instance information in UP status will be retained.

2.3. 2. Incremental pull of registry information

com.netflix.discovery.DiscoveryClient#getAndUpdateDelta

Interface: http://localhost:20000/eureka/apps/delta

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 the incremental pull fails, the full pull is performed
    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 {
                // Update local cache
                updateDelta(delta);
                // Compute application set consistency hash code
                reconcileHashCode = getReconcileHashCode(applications);
            } finally {
                fetchRegistryUpdateLock.unlock();
            }
        } else {
            logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
        }
        // Compare the application set consistency hash codes. If they are inconsistent, it will be considered that the incremental pull data is dirty, and a full pull will be initiated to update the local registry information
        // 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());
    }
}

The general representation of appsHashCode is:

appsHashCode = ${status}_${count}_

By splicing the application status and quantity into a string, it represents the statistical information of the service instance status in the current registry. For a simple example, 10 application instances have the status of UP, 5 application instances have the status of DOWN, and the other status data is 0 (not represented). Then the form of appsHashCode will be:

appsHashCode = UP_10_DOWN_5_

2.4 service registration

com.netflix.discovery.DiscoveryClient#register

Interface: http://localhost:20000/eureka/apps/${APP_NAME}

/**
 * Register with the eureka service by making the appropriate REST call.
 */
boolean register() throws Throwable {
    logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
    EurekaHttpResponse<Void> httpResponse;
    try {
        // Encapsulate its own service instance metadata into InstanceInfo and send it to Eureka Server to request service registration. When Eureka Server returns 204 status code, it indicates that the service registration is successful.
        httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
    } catch (Exception e) {
        logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
        throw e;
    }
    if (logger.isInfoEnabled()) {
        logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
    }
    return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}

2.5 initialize scheduled tasks

com.netflix.discovery.DiscoveryClient#initScheduledTasks

In this method, three timer tasks are initialized. One is used to pull the registry information from Eureka Server and refresh the local cache; One is used to send heartbeat to Eureka Server; An operation for on-demand registration. The code is as follows:

/**
 * Initializes all scheduled tasks.
 */
private void initScheduledTasks() {
    if (clientConfig.shouldFetchRegistry()) {
        // Registry cache refresh timer
        // Get the refresh interval in the configuration file. The default is 30 seconds. You can use Eureka client. Register fetch interval seconds
        // 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);
    }

    if (clientConfig.shouldRegisterWithEureka()) {
        int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
        int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
        logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);
        
        // Send heartbeat timer. The default is to send a heartbeat every 30 seconds
        // Heartbeat timer
        scheduler.schedule(
                new TimedSupervisorTask("heartbeat", scheduler, heartbeatExecutor,  renewalIntervalInSecs,
                        TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread()
                ),
                renewalIntervalInSecs, TimeUnit.SECONDS);

        // Register timers on demand....

    } else {
        logger.info("Not registering with Eureka server per configuration");
    }
}

2.5. 1 register scheduled tasks on demand

The function of on-demand registration timing task is to re initiate a registration request to Eureka Server when the InstanceInfo or status in Eureka Client changes, update the service instance information in the registry, and ensure that the service instance information in the Eureka Server registry is valid and available.

      // Register timers on demand....
         // InstanceInfo replicator
        // Regularly check and refresh the service instance information, check whether there are changes and whether re registration is required
        instanceInfoReplicator = new InstanceInfoReplicator(
                this,
                instanceInfo,
                clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                2); // burstSize
        // Monitor the status change of the application, and initiate re registration in case of any change
        statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
            @Override
            public String getId() {
                return "statusChangeListener";
            }

            @Override
            public void notify(StatusChangeEvent statusChangeEvent) {
                if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
                        InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
                    // log at warn level if DOWN was involved
                    logger.warn("Saw local status change event {}", statusChangeEvent);
                } else {
                    logger.info("Saw local status change event {}", statusChangeEvent);
                }
                instanceInfoReplicator.onDemandUpdate();
            }
        };

        if (clientConfig.shouldOnDemandUpdateStatusChange()) {
            // Register application status change monitor
            applicationInfoManager.registerStatusChangeListener(statusChangeListener);
        }
        // Start timer to register scheduled tasks on demand
        instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());

2.6 service offline

com.netflix.discovery.DiscoveryClient#shutdown

/**
 * Shuts down Eureka Client. Also sends a deregistration request to the
 * eureka server.
 */
@PreDestroy
@Override
public synchronized void shutdown() {
    // Synchronization method
    if (isShutdown.compareAndSet(false, true)) {
        logger.info("Shutting down DiscoveryClient ...");
        // Atomic operation to ensure that it will be performed only once
        if (statusChangeListener != null && applicationInfoManager != null) {
            // Unregister status listener
            applicationInfoManager.unregisterStatusChangeListener(statusChangeListener.getId());
        }
        // Cancel scheduled task
        cancelScheduledTasks();

        // If APPINFO was registered
        if (applicationInfoManager != null
                && clientConfig.shouldRegisterWithEureka()
                && clientConfig.shouldUnregisterOnShutdown()) {
            // Service offline
            applicationInfoManager.setInstanceStatus(InstanceStatus.DOWN);
            unregister();
        }
        // Close the Jersey client
        if (eurekaTransport != null) {
            eurekaTransport.shutdown();
        }
        // Close the related Monitor
        heartbeatStalenessMonitor.shutdown();
        registryStalenessMonitor.shutdown();

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

Topics: Spring Boot eureka