服务获取和服务续约

Eureka 客户端核心机制:服务获取与服务续约的实现解析

Eureka 客户端(Eureka Client)通过两个核心定时任务与 Eureka Server 交互:服务获取(拉取服务注册表)和服务续约(发送心跳维持存活状态)。这两个任务在DiscoveryClient类中初始化,是保证服务发现准确性和服务状态有效性的关键。

定时任务初始化:initScheduledTasks 方法

initScheduledTasks是 Eureka 客户端初始化时的核心方法,用于启动服务获取和服务续约的定时任务。其核心逻辑是:根据配置判断是否需要拉取注册表和注册服务,若需要则创建对应的定时任务。

private void initScheduledTasks() {
    // 1. 服务获取定时任务(拉取服务注册表)
    if (clientConfig.shouldFetchRegistry()) {
        // 拉取间隔(默认30秒,可通过eureka.client.registry-fetch-interval-seconds配置)
        int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
        // 指数退避边界(任务失败时重试间隔的最大倍数)
        int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
        
        // 启动定时任务:每registryFetchIntervalSeconds秒执行一次CacheRefreshThread
        scheduler.schedule(
            new TimedSupervisorTask(
                "cacheRefresh",  // 任务名称
                scheduler,
                cacheRefreshExecutor,  // 执行线程池
                registryFetchIntervalSeconds,
                TimeUnit.SECONDS,
                expBackOffBound,
                new CacheRefreshThread()  // 服务获取线程
            ),
            registryFetchIntervalSeconds, TimeUnit.SECONDS);
    }

    // 2. 服务续约定时任务(发送心跳)
    if (clientConfig.shouldRegisterWithEureka()) {
        // 续约间隔(默认30秒,由eureka.instance.lease-renewal-interval-in-seconds配置)
        int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
        int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
        
        // 启动定时任务:每renewalIntervalInSecs秒执行一次HeartbeatThread
        scheduler.schedule(
            new TimedSupervisorTask(
                "heartbeat",  // 任务名称
                scheduler,
                heartbeatExecutor,  // 执行线程池
                renewalIntervalInSecs,
                TimeUnit.SECONDS,
                expBackOffBound,
                new HeartbeatThread()  // 服务续约线程
            ),
            renewalIntervalInSecs, TimeUnit.SECONDS);
        
        // 省略实例信息同步相关逻辑...
    }
}

核心设计

  • 两个任务独立线程池执行(cacheRefreshExecutorheartbeatExecutor),避免相互影响;
  • 使用TimedSupervisorTask包装,实现任务失败时的指数退避重试(避免频繁失败占用资源);
  • 执行间隔可通过配置自定义,平衡实时性与性能。

服务获取:CacheRefreshThread

服务获取的核心目标是:定期从 Eureka Server 拉取服务注册表并更新本地缓存,确保服务消费者能获取最新的服务实例列表。

1. 核心逻辑:fetchRegistry 方法

private boolean fetchRegistry(boolean forceFullRegistryFetch) {
    try {
        // 获取本地已缓存的服务注册表
        Applications applications = getApplications();
        
        // 触发全量拉取的条件:
        // 1. 禁用增量更新(eureka.client.disable-delta=true)
        // 2. 首次拉取(本地缓存为空)
        // 3. 强制全量拉取(forceFullRegistryFetch=true)
        // 4. 本地缓存版本无效(version=-1)
        if (clientConfig.shouldDisableDelta() || forceFullRegistryFetch || applications == null) {
            // 全量拉取注册表
            getAndStoreFullRegistry();
        } else {
            // 增量拉取(仅获取上次更新后的变化)
            getAndUpdateDelta(applications);
        }
        
        // 刷新缓存并更新服务实例状态
        onCacheRefreshed();
        updateInstanceRemoteStatus();
        return true;
    } catch (Throwable e) {
        logger.error("无法刷新服务缓存!", e);
        return false;
    }
}

拉取策略

  • 全量拉取(getAndStoreFullRegistry):首次启动或配置禁用增量时,拉取完整的服务注册表(所有服务 + 所有实例);
  • 增量拉取(getAndUpdateDelta):后续拉取仅获取上次同步后新增 / 修改 / 删除的服务实例,减少网络传输开销。

2. 缓存刷新:onCacheRefreshed

拉取注册表后,需要更新本地缓存,并通知负载均衡器(如 Ribbon)刷新实例列表:

private void onCacheRefreshed() {
    // 发布缓存刷新事件(CacheRefreshedEvent)
    if (cacheRefreshedListener != null) {
        cacheRefreshedListener.onCacheRefreshed();
    }
    // 通知所有注册的监听器
    for (EurekaEventListener listener : eventListeners) {
        listener.onEvent(new CacheRefreshedEvent());
    }
}

负载均衡器更新
缓存刷新事件被EurekaNotificationServerListUpdater捕获后,会触发负载均衡器的实例列表更新:

// 简化逻辑:从缓存中获取最新实例列表并更新负载均衡器
updateAction.doUpdate();  // 更新负载均衡器中的服务实例映射
lastUpdated.set(System.currentTimeMillis());  // 记录最后更新时间

这样,服务消费者在调用服务时,就能使用最新的实例列表进行负载均衡。

服务续约:HeartbeatThread

服务续约的核心目标是:定期向 Eureka Server 发送心跳,证明服务实例存活,避免被 Eureka Server 剔除。

1. 核心逻辑:renew 方法

boolean renew() {
    EurekaHttpResponse<InstanceInfo> httpResponse;
    try {
        // 发送心跳请求:PUT /eureka/apps/{服务名}/{实例ID}
        httpResponse = eurekaTransport.registrationClient.sendHeartBeat(
            instanceInfo.getAppName(),  // 服务名
            instanceInfo.getId(),       // 实例ID
            instanceInfo,               // 实例信息
            null
        );
        
        // 处理响应:
        // 1. 若返回404(NOT_FOUND):说明实例已从Server删除,需重新注册
        if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
            REREGISTER_COUNTER.increment();  // 重新注册计数器+1
            logger.info("实例已不存在,重新注册服务...");
            // 标记实例信息为"脏数据"(需要同步)
            long timestamp = instanceInfo.setIsDirtyWithTime();
            // 执行重新注册
            boolean success = register();
            if (success) {
                instanceInfo.unsetIsDirty(timestamp);  // 清除"脏数据"标记
            }
            return success;
        }
        // 2. 若返回200(OK):续约成功
        return httpResponse.getStatusCode() == Status.OK.getStatusCode();
    } catch (Throwable e) {
        logger.error("发送心跳失败!", e);
        return false;
    }
}

核心机制

  • 心跳请求格式:PUT /eureka/apps/{服务名}/{实例ID},携带实例当前状态;
  • 异常处理:若心跳失败(如网络波动),TimedSupervisorTask会触发重试(指数退避策略);
  • 重新注册:若 Server 返回 404(实例已被删除),客户端会自动重新注册,保证服务可用性。

2. 续约配置参数

配置参数 默认值 说明
eureka.instance.lease-renewal-interval-in-seconds 30 心跳间隔(秒),即服务续约的定时任务间隔
eureka.instance.lease-expiration-duration-in-seconds 90 服务过期时间(秒):Server 多久未收到心跳则标记实例为失效

总结:服务获取与续约的核心价值

  1. 服务获取
    通过 “全量拉取 + 增量更新” 的策略,在减少网络开销的同时,保证客户端缓存的服务注册表尽可能新,为服务调用提供准确的实例列表。
  2. 服务续约
    通过定时心跳机制,让 Eureka Server 实时感知服务实例的存活状态,结合 “自动重新注册” 逻辑,最大程度避免服务因临时故障被误剔除。
  3. 整体设计思想
    Eureka 客户端通过 “定时任务 + 容错重试 + 增量更新” 的组合策略,在保证服务发现实时性的同时,兼顾了网络开销和系统稳定性,这也是 Eureka 作为 AP 系统高可用特性的重要体现