0%

服务获取和服务续约

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

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

定时任务初始化:initScheduledTasks 方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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)刷新实例列表:

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

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

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

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

服务续约:HeartbeatThread

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

1. 核心逻辑:renew 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 系统高可用特性的重要体现

欢迎关注我的其它发布渠道