Service discovery in Eureka revolves around two primary requirements for a client: obtaining the list of service provider addresses during bootstrap and dynamically detectnig changes in the Eureka server registry. This article explores how these processes are implemented on both the client and server sides.
Registry Initialization in DiscoveryClient
When the DiscoveryClient is instantiated, it immediately attemptss to fetch the service registry. If the initial fetch fails, it falls back to a backup registry if one is configured. The core logic resides within the constructor:
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, ...) {
// ... initialization ...
if (clientConfig.shouldFetchRegistry() && !pullRegistryData(false)) {
loadFromBackupRegistry();
}
}
Registry Fetch Strategy: Full vs. Delta
The pullRegistryData method (commonly known as fetchRegistry in the source) determines whether the client should perform a full download of all service instances or just an incremental udpate (delta). Incremental updates reduce network overhead but require a consistent state.
private boolean pullRegistryData(boolean forceFullPull) {
try {
Applications currentApps = getLocalApplications();
// Determine if a full fetch is required based on configuration or state
if (clientConfig.shouldDisableDelta()
|| !isValid(currentApps)
|| forceFullPull
|| isFirstFetch(currentApps)) {
executeFullRegistryFetch();
} else {
executeDeltaUpdate(currentApps);
}
currentApps.setAppsHashCode(currentApps.getReconcileHashCode());
} catch (Exception err) {
logger.error("Registry refresh failed: {}", err.getMessage());
return false;
}
notifyCacheRefreshListeners();
syncInstanceStatus();
return true;
}
Scheduling Background Updates
To keep the local cache up to date, Eureka uses a scheduled task that runs every 30 seconds by default. This is managed by a TimedSupervisorTask, which provides resilience against network timeouts and server instability.
private void setupRefreshTasks() {
if (clientConfig.shouldFetchRegistry()) {
int interval = clientConfig.getRegistryFetchIntervalSeconds();
int maxBackoff = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
cacheRefreshTask = new TimedSupervisorTask(
"cacheRefresh",
scheduler,
refreshExecutor,
interval,
TimeUnit.SECONDS,
maxBackoff,
new RegistryRefreshRunnable()
);
scheduler.schedule(cacheRefreshTask, interval, TimeUnit.SECONDS);
}
}
Resilience with TimedSupervisorTask
The TimedSupervisorTask implements an exponential backoff strategy. If a task times out, the delay before the next execution is increased (doubled) until it hits a defined limit. Once a task succeeds, the interval resets to the original value.
public void run() {
Future<?> taskFuture = null;
try {
taskFuture = executor.submit(workRunnable);
taskFuture.get(currentTimeoutMillis, TimeUnit.MILLISECONDS);
// Success: Reset delay
delay.set(initialTimeoutMillis);
} catch (TimeoutException te) {
// Handle timeout: Increase delay exponentially
long nextDelay = Math.min(maxDelayLimit, delay.get() * 2);
delay.set(nextDelay);
} catch (Throwable ex) {
// Handle other failures
} finally {
if (taskFuture != null) taskFuture.cancel(true);
if (!scheduler.isShutdown()) {
scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
}
}
}
Server-Side Registry Retrieval
On the Eureka server, the ApplicationsResource handles incoming fetch requests. It distinguishes between full registry requests and delta requests. To optimize performance, Eureka employs a multi-tiered caching strategy.
@GET
public Response getApplications(@HeaderParam(HEADER_ACCEPT) String acceptHeader,
@QueryParam("regions") String regions) {
if (!registry.isAccessAllowed()) {
return Response.status(Status.FORBIDDEN).build();
}
// Identify response format (JSON or XML)
Key.KeyType format = (acceptHeader != null && acceptHeader.contains("json"))
? Key.KeyType.JSON : Key.KeyType.XML;
Key cacheKey = new Key(Key.EntityType.Application,
ResponseCacheImpl.ALL_APPS,
format, ...);
// Retrieve from cache
Object responseData = responseCache.get(cacheKey);
return Response.ok(responseData).build();
}
The Response Cache Mechanism
The ResponseCache minimizes the overhead of generating the registry response. It uses a readOnlyCacheMap (a concurrent map) for fast lookups and a readWriteCacheMap (backed by Guava/Caffeine) that interacts with the actual registry data. If a key is missing in the read-only cache, it is pulled from the read-write cache and populated.
Value getCacheValue(final Key key, boolean useReadOnly) {
if (useReadOnly) {
Value val = readOnlyCacheMap.get(key);
if (val == null) {
val = readWriteCacheMap.get(key);
readOnlyCacheMap.put(key, val);
}
return val;
}
return readWriteCacheMap.get(key);
}
This layered approach ensures that frequent client polls do not overwhelm the server's registry processing logic, as most requests are served from the high-speed read-only cache.