本文中源码,来自Eureka的v1.7.x版本。代码仓:github.com/Netflix/eur…
在《Eureka Client的初始化》一文中,我们知道了客户端的启动流程。
现在一起来看看客户端是如何注册到Eureka server的。请考虑以下问题:
- 服务节点注册与否,对应代码层面的表现是什么?
- 节点启动时注册了,节点下线或宕机时,如何取消注册?
一 注册表的数据结构
1.1 Application类
DiscoveryClient类的localRegionApps,就是应用集的原子引用。
private final AtomicReference<Applications> localRegionApps = new AtomicReference<Applications>();
Applications类,就是从Eureka server返回的注册表信息。注册表是一个HashMap,key就是appName,value是Application实例。
Application是对应用集群的封装,包含已注册的服务节点。
1.2 InstanceInfo类
InstanceInfo类是对服务节点信息的封装,有以下关键属性:
InstanceInfo.equals()中,使用ID来判断两个实例是否相同,就用到了instanceId。
在Eureka客户端中,可通过eureka.instance.instance-id配置,指定该节点的instanceId,通常配置为
${spring.application.name}-${server.port}。
当节点尚未注册,或节点状态发生变化时,应该及时同步给Eureka。用下面两个属性做了标识:
// 最新节点信息是否已同步给Eureka
@XStreamOmitField
private volatile boolean isInstanceInfoDirty = false;
// 上次节点信息变动时间戳
private volatile Long lastDirtyTimestamp = System.currentTimeMillis();
1.3 服务注册的代码表现
- 服务注册,就是将某节点的InstanceInfo(网络地址、运行终态等信息),添加到Application.instances中。
- 取消注册,就是将该节点的InstanceInfo,从Application.instances中移除;
- 节点信息变更同步,就是更新Application.instances中特定instanceId的元素;
二 与Eureka的网络通信
客户端与Eureka间的网络通信组件,被封装到EurekaTransport类。
EurekaHttpClient,默认使用JerseyReplicationClient,底层使用了ApacheHttpClient。
三 客户端注册
3.1 信息来源
InstanceInfo从何而来
可以从配置、运行环境中获取信息,来创建InstanceInfo对象。然后交由ApplicationInfoManager管理,它有以下属性。
// key是ListenerId
protected final Map<String, StatusChangeListener> listeners;
private final InstanceStatusMapper instanceStatusMapper;
// 节点信息
private InstanceInfo instanceInfo;
// 客户端配置信息
private EurekaInstanceConfig config;
在创建核心类DiscoveryClient时,applicationInfoManager就是构造参数之一。
Eureka地址从何而来
要发起注册,得先知道Eureka的网络地址啊。由配置提供,如springboot中:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
3.2 发起注册
提交心跳任务
在创建DiscoveryClient实例时,创建了3个线程池:
scheduler仅用于周期任务或延迟任务的调度,任务体的执行将交由其他两个线程池完成。
在DiscoveryClient.initScheduledTasks()中,添加了“heartbeat”延迟任务,延迟时间即续租间隔时间,默认为30秒。
TimedSupervisorTask类
执行体是TimedSupervisorTask类,它是TimerTask的子类,真正的任务体是HeartbeatThread,该类的作用在于:
- 对任务超时、被拒绝、执行异常,提供了数量统计与监控;
- 异步执行任务体,使用DiscoveryClient中的heartbeatExecutor线程池;
- 对超时时间自适应:本次心跳超时后,会将下次心跳的延迟时间放大,一旦心跳成功后,将延迟时间恢复为正常值;
- 提交下一次的延迟任务
TimedSupervisorTask简化后的run逻辑:
public void run() {
Future future = null;
try {
future = executor.submit(task);
future.get(timeoutMillis, TimeUnit.MILLISECONDS); // block until done or timeout
// 未超时,重置延迟时间为正常值
delay.set(timeoutMillis);
} catch (TimeoutException e) {
timeoutCounter.increment();
// 增大延迟时间为两倍
long currentDelay = delay.get();
long newDelay = Math.min(maxDelay, currentDelay * 2);
delay.compareAndSet(currentDelay, newDelay);
} catch (RejectedExecutionException e) {
rejectedCounter.increment();
} catch (Throwable e) {
throwableCounter.increment();
} finally {
// 提交下次延迟任务
if (!scheduler.isShutdown()) {
scheduler.schedule(this, delay.get(), TimeUnit.MILLISECONDS);
}
}
}
发送心跳
HeartbeatThread是DiscoveryClient的内部类,续租则调用renew()。
首次续租时,由于节点尚未注册,Eureka会响应404,此时立即发起注册。
心跳请求是通过JerseyReplicationClient#sendHeartBeat发出的。url和参数如下。
心跳请求时,只将节点状态、lastDirtyTimestamp传送给Eureka。
可以猜测,在Eureka server中,会将心跳请求参数lastDirtyTimestamp,与注册表中节点的lastDirtyTimestamp相比较;如果前者更大,说明客户端信息已发生变化,则响应客户端需要发起一次信息同步请求。
发起注册
调用DiscoveryClient.register(),POST请求体就是instanceInfo,url是apps/appName/id。
四 客户端取消注册
DiscoveryClient#shutdown中,将节点状态设置为DOWN,调用unregister()。
取消注册url也是
apps/appName/id,与register请求的相同,只是改用DELETE请求。
五 相关配置
-
是否注册到Eureka
- 源码中配置名是
registration.enabled,默认为true。 - springboot中是
eureka.client.registerWithEureka,默认为true。
- 源码中配置名是
-
续租间隔周期
- 源码中配置名是
renewalIntervalInSecs,默认30秒。 - springboot中配置项是
eureka.instance.lease-renewal-interval-in-seconds,默认30秒
- 源码中配置名是