Eureka-Client服务注册
服务的注册和服务的发现其实是两个概念,发现所描述的是instance作为调用者,获取需要调用的下游服务的列表信息的过程,注册所描述的是instance作为被调用者,要将自己的信息注册到注册中心的过程,共给其他instance(包括自己)进行服务发现使用。
对于注册过程,需要配置eureka.client.register-with-eureka = true,默认也是为true,所以不配置也是可以的。
触发服务注册的情况主要有以下几种:
- instance启动时
- instanceInfo状态改变时
- instance续约时Eureka-Server未发现注册信息返回非204时
其实归结起来就是Eureka-Client和Eureka-Server数据不一致时就会触发register动作。
instance启动时
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
......
// 略过部分代码,这里主要是服务发现相关的
......
//
try {
// default size of 2 - 1 each for heartbeat and cacheRefresh
// 创建了一个含有2个线程的任务调度池
scheduler = Executors.newScheduledThreadPool(2,
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-%d")
.setDaemon(true)
.build());
// 创建了一个只含有1个线程的心跳执行线程池
heartbeatExecutor = new ThreadPoolExecutor(
1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
// 创建了一个只含有1个线程的缓存刷新线程池
cacheRefreshExecutor = new ThreadPoolExecutor(
1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
new ThreadFactoryBuilder()
.setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
.setDaemon(true)
.build()
); // use direct handoff
// 主要是构造一个clientFactory,用作每次向Eureka-Server发请求时构建client使用
// 这里不做详细分析
eurekaTransport = new EurekaTransport();
scheduleServerEndpointTask(eurekaTransport, args);
......
} catch (Throwable e) {
throw new RuntimeException("Failed to initialize DiscoveryClient!", e);
}
// 这里有俩个条件
// 分别对应着配置文件里的两个启动配置参数
// 如果这两个条件都满足,就会在启动时向注册中心注册自己
if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
try {
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);
}
}
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
// 初始化所有的调度任务
initScheduledTasks();
try {
// 注册监视器,主要用于JMX
Monitors.registerObject(this);
} catch (Throwable e) {
logger.warn("Cannot register timers", e);
}
// This is a bit of hack to allow for existing code using DiscoveryManager.getInstance()
// to work with DI'd DiscoveryClient
// 最后将初始化的新的client及其config更新到instace管理器的属性中,供后续逻辑获取及更改
DiscoveryManager.getInstance().setDiscoveryClient(this);
DiscoveryManager.getInstance().setEurekaClientConfig(config);
initTimestampMs = System.currentTimeMillis();
logger.info("Discovery Client initialized at timestamp {} with initial instances count: {}",initTimestampMs, this.getApplications().size());
}
上面说到的两个参数可以查看spring-configuration-metadata.json中的值:
{
"name": "eureka.client.register-with-eureka",
"type": "java.lang.Boolean",
"description": "Indicates whether or not this instance should register its information with eureka server for discovery by others. In some cases, you do not want your instances to be discovered whereas you just want do discover other instances.",
"sourceType": "org.springframework.cloud.netflix.eureka.EurekaClientConfigBean",
"defaultValue": true
}
{
"name": "eureka.client.should-enforce-registration-at-init",
"type": "java.lang.Boolean",
"description": "Indicates whether the client should enforce registration during initialization. Defaults to false.",
"sourceType": "org.springframework.cloud.netflix.eureka.EurekaClientConfigBean",
"defaultValue": false
}
eureka.client.register-with-eureka默认是true,上面也提到过,不配置也可以
eureka.client.should-enforce-registration-at-init默认是false,所以在不配置该参数的情况下,instance在启动时并不会向注册中心注册自己
那么instance实在什么时候开始向注册中心注册自己呢?稍安勿躁,先继续看下第二种情况,原因就在其中
instanceInfo状态改变时
instance什么时候会发生状态改变呢?暂且留着这个疑问,先看下initScheduledTasks(),因为这个方法是初始化所有后台任务的入口
private void initScheduledTasks() {
if (clientConfig.shouldFetchRegistry()) {
// registry cache refresh timer
int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
// 构造缓存刷新任务
cacheRefreshTask = new TimedSupervisorTask(
"cacheRefresh",
scheduler,
cacheRefreshExecutor,
registryFetchIntervalSeconds,
TimeUnit.SECONDS,
expBackOffBound,
new CacheRefreshThread()
);
// 开始将cacheRefreshTask添加到scheduler这个延时调度器,
// 这个scheduler也就是上文在DiscoveryClient中创建的,
// 这时缓存刷新任务就在后台开启了
scheduler.schedule(
cacheRefreshTask,
registryFetchIntervalSeconds, TimeUnit.SECONDS);
}
// 这里同样时判断是否配置了eureka.client.register-with-eureka=true
// 如果是true,就要启动定时心跳任务
if (clientConfig.shouldRegisterWithEureka()) {
int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
logger.info("Starting heartbeat executor: " + "renew interval is: {}", renewalIntervalInSecs);
// Heartbeat timer
// 构造定时心跳任务
heartbeatTask = new TimedSupervisorTask(
"heartbeat",
scheduler,
heartbeatExecutor,
renewalIntervalInSecs,
TimeUnit.SECONDS,
expBackOffBound,
new HeartbeatThread()
);
// 加入scheduler,并在后台开始执行
scheduler.schedule(
heartbeatTask,
renewalIntervalInSecs, TimeUnit.SECONDS);
// InstanceInfo replicator
// 初始化instance信息复制器
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize
// 这里着重看下,注册了instance的状态监听器,触发状态改变事件就会触发
// 所以追着StatusChangeEvent就会发现instance都是在什么时候改变状态的
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有后台定时任务,
// 调用该方法时会中断之前的定时任务,并重新建立新的定时任务,
// 这里不做详细分析
instanceInfoReplicator.onDemandUpdate();
}
};
// 如果eureka.client.on-demand-update-status-change=true,就设置监听器,默认为true
if (clientConfig.shouldOnDemandUpdateStatusChange()) {
applicationInfoManager.registerStatusChangeListener(statusChangeListener);
}
// 启动复制器
instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
logger.info("Not registering with Eureka server per configuration");
}
}
通过这段代码可知instance的状态改变会触发事件通知监听器做一些操作,因此可以看一下setInstanceStatus方法,如下,就是在set方法中实现了通知机制
public synchronized void setInstanceStatus(InstanceStatus status) {
InstanceStatus next = instanceStatusMapper.map(status);
if (next == null) {
return;
}
InstanceStatus prev = instanceInfo.setStatus(next);
if (prev != null) {
for (StatusChangeListener listener : listeners.values()) {
try {
listener.notify(new StatusChangeEvent(prev, next));
} catch (Exception e) {
logger.warn("failed to notify listener: {}", listener.getId(), e);
}
}
}
}
继续追溯这个方法,就可以知道有以下几重情况会触发状态改变(不列举详细代码):
- 在instance停止时,即调用shutdown时,会触发状态改变为DOWN
- 在instance解注册时,会触发状态改变为DOWN
- 在instance注册时,会触发状态改变为UP
- 在instance定时做健康检查时,可能会触发状态改变(这里会通过默认地址“/healthcheck”发起http调用,详细可以自行看下
doHealthCheck()方法) - server instance在peer节点拉取到服务列表时,会将自己状态更新为UP,即可以提供注册服务(这个是server端之间的逻辑,不详细说明)
至此,了解到instance都实在什么情况下触发了状态变更。
instance的状态可以通过各种逻辑实现监控和变化了,那又是如何让注册中心得知的呢?
这就是靠InstanceInfoReplicator来实现的,看到上面代码中有new和start的过程,可能都不明白为什么会有这个东西,下面就来分析下它到底干了什么,首先new就不具体看了,这里就是构造了一个调度器,主要看下start的过程
public void start(int initialDelayMs) {
if (started.compareAndSet(false, true)) {
// start是在启动时会调用,设置dirty,就是为了触发一次任务
instanceInfo.setIsDirty(); // for initial register
Future next = scheduler.schedule(this, initialDelayMs, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
看一下InstanceInfoReplicator的run方法,上面的调度器就会触发run方法
public void run() {
try {
// 刷新InstanceInfo
discoveryClient.refreshInstanceInfo();
Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
if (dirtyTimestamp != null) {
// 注册服务
discoveryClient.register();
instanceInfo.unsetIsDirty(dirtyTimestamp);
}
} catch (Throwable t) {
logger.warn("There was a problem with the instance info replicator", t);
} finally {
// 着重看这里,这里会把task重新加入调度池,所以会产生定时器的效果
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
通过上面的finally可以实现定时任务,这种实现方式也同样用在定时刷新缓存和定时发送心跳,是一个道理,详细可以看TimedSupervisorTask的实现。run方法中主要有两个逻辑,先看一下refreshInstanceInfo()方法
void refreshInstanceInfo() {
// 刷新dataCenter
applicationInfoManager.refreshDataCenterInfoIfRequired();
// 刷新租约信息
applicationInfoManager.refreshLeaseInfoIfRequired();
InstanceStatus status;
try {
// 健康检查
// 也就是上述提到了健康检查的逻辑过程
status = getHealthCheckHandler().getStatus(instanceInfo.getStatus());
} catch (Exception e) {
logger.warn("Exception from healthcheckHandler.getStatus, setting status to DOWN", e);
status = InstanceStatus.DOWN;
}
if (null != status) {
applicationInfoManager.setInstanceStatus(status);
}
}
refreshDataCenterInfoIfRequired()方法主要是处理instance的hostName、ipAddr、dataCenterInfo属性的变化。但默认使用的是非RefreshableInstanceConfig实现的配置类(MyDataCenterInstanceConfig),因为AbstractInstanceConfig.hostInfo是静态属性,即使本机修改了IP等信息,Eureka-Client进程也不会感知到。
refreshLeaseInfoIfRequired()方法实例信息的renewalIntervalInSecs、durationInSecs属性的变化.
一般情况下,我们使用的是非 RefreshableInstanceConfig 实现的配置类( 一般是 MyDataCenterInstanceConfig ),因为 AbstractInstanceConfig.hostInfo 是静态属性,即使本机修改了 IP 等信息,Eureka-Client 进程也不会感知到。
第二个逻辑就是最关键的注册了,这个暂且放后,再来分析下第三种情况
instance续约失败
续约就是由heartbeatTask来实现的,具体实现方式就是renew方法
boolean renew() {
EurekaHttpResponse<InstanceInfo> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());
// 如果通过http调用续约接口返回NOT_FOUND时就会触发重新注册
if (httpResponse.getStatusCode() == Status.NOT_FOUND.getStatusCode()) {
REREGISTER_COUNTER.increment();
logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());
long timestamp = instanceInfo.setIsDirtyWithTime();
// 服务注册
boolean success = register();
if (success) {
instanceInfo.unsetIsDirty(timestamp);
}
return success;
}
return httpResponse.getStatusCode() == Status.OK.getStatusCode();
} catch (Throwable e) {
logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);
return false;
}
}
至此,三种情况就分析完了,上面的instance是什么时候发起注册的问题其实已经分析完了,这里做一下总结:
-
在默认配置情况下,Eureka-Client并不会在启动时注册
-
注册任务其实是在
InstanceInfoReplicator的start方法里,直接设置instanceInfo.setIsDirty(),这样就触发了InstanceInfoReplicator的run方法,进而实现服务注册,所以服务注册默认情况下是异步的 -
另一种情况就是renew续约时,状态返回NOT_FOUND时,也会触发instance在运行时的补注册
服务注册过程
这些分析完毕后,就真正开始看instance是如何注册的,看下register()方法
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse<Void> httpResponse;
try {
// 发起http调用注册,实际调用方法在下面
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();
}
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
try {
// 主要是构造一个jerseyclient,springcloud内部server都是通过jersey启动的
//
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);
// 这里真正的发起请求,完成注册
response = resourceBuilder
.header("Accept-Encoding", "gzip")
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
.post(ClientResponse.class, info);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
这里都是通过RetryableEurekaHttpClient来实现的重试机制,如果配置文件如下
eureka.client.service-url.defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/,http://localhost:8763/eureka/,http://localhost:8764/eureka/
注册时会从第一个注册中心地址开始尝试注册,如果第一个失败,就会通过retry机制去尝试第二个,默认情况下retry次数是3,所以Eureka-Client会尝试8761、8762、8763,如果8763也失败了,就结束流程,sleep一段时间再尝试注册,8764是一直不会使用到的,除非修改retry次数。

4467

被折叠的 条评论
为什么被折叠?



