Eureka源码分析--服务注册(2)
服务注册
client端发送注册请求
初看eureka client端源码,想找找服务是怎么注册的,在DiscoveryClient方法中顺着看的时候,看到一行代码,
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);
}
}
这里面不就有个很眼熟的的单词吗,register(),这不就是要找的东西吗,点进去看看,源码如下:
/**
* Register with the eureka service by making the appropriate REST call.
*/
//通过进行适当的 REST 调用向 eureka 服务注册。
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse<Void> httpResponse;
try {
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();
}
这英文注释不是清清楚楚吗,这不就是我要找的注册流程吗,但是,回过头看找到register的入口时发现,这里有一个clientConfig.shouldEnforceRegistrationAtInit()这样的配置项,点进去看发现默认就是false,翻译了一下这个配置的注解意思也没有太理解,
而且在我们项目中也并没有使用过这个注解。这是我几乎可以肯定的确定这里并不是register方法的真正入口。
后续通过反查找,找到了这样几个方法
473行是我第一次看到的地方,之前我尝试打断点并未进入,所以目标只有907行和121行,当我点到907行时发现调用是在renew的方法中,而根据这个方法所提供的的注释来看,描述的是通过进行适当的 REST 调用来更新 eureka 服务
并且该方法开始会先去调用一个sendHeartBeat,抛开具体执行的方法,单从字面上来看这里是做心跳续约的,并且调用register时会接收一个返回值。而凭感觉来说,正常的注册流程调用完就完了嘛,我要你返回参数干啥。
所以自然而然的确定了最后一处调用,121行。
public void run() {
try {
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 {
Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
scheduledPeriodicRef.set(next);
}
}
看到这里可以更加确定这里就是client进行注册时调用的地方,但是还是需要向上找,该方法属于InstanceInfoReplicator类,并且该方法为run,而该类实现了Runnable,所以可以断定的是有一个地方一定是
InstanceInfoReplicator.run()。继续向上查找,在该类找到onDemandUpdate()方法中调用了this.run().
public boolean onDemandUpdate() {
if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {
if (!scheduler.isShutdown()) {
scheduler.submit(new Runnable() {
@Override
public void run() {
logger.debug("Executing on-demand update of local InstanceInfo");
Future latestPeriodic = scheduledPeriodicRef.get();
if (latestPeriodic != null && !latestPeriodic.isDone()) {
logger.debug("Canceling the latest scheduled update, it will be rescheduled at the end of on demand update");
latestPeriodic.cancel(false);
}
InstanceInfoReplicator.this.run();
}
});
return true;
} else {
logger.warn("Ignoring onDemand update due to stopped scheduler");
return false;
}
} else {
logger.warn("Ignoring onDemand update due to rate limiter");
return false;
}
}
而该方法继续向上找便会找到Discovery方法中initScheduledTasks()。到目前为止算是找到真正的入口了,那么便从入口在重新梳理一次。
完整的流程如下:
1、调用initScheduledTasks方法
2、实例化InstanceInfoReplicator(实例信息复制器(直译)),调用onDemandUpdate方法
3、onDemandUpdate方法中调用run方法进行真正的注册
4、run方法中会先判断一个dirtyTimestamp是否为空,而dirtyTime会在InstanceInfoReplicator.start()方法中调用setIsDirty()设置
5、调用register()方法发送请求至server端进行注册
整体的注册流程就结束了,但是这里面有一个非常有意思的问题,在InstanceInfoReplicator.start(),将自己放入里一个定时任务的线程池中,并且第一次初始化的时候会延迟40s(默认值,可以通过配置修改)
通过查找官方文档,找到官方解释
再看一下register的请求源码
@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
//这里会拼接一个url ,如果服务名为ServerA 那么 当前urlPath的路径即为 apps/ServerA
String urlPath = "apps/" + info.getAppName();
Response response = null;
try {
//构建请求信息
Builder resourceBuilder = jerseyClient.target(serviceUrl).path(urlPath).request();
addExtraProperties(resourceBuilder);
addExtraHeaders(resourceBuilder);
response = resourceBuilder
.accept(MediaType.APPLICATION_JSON)
.acceptEncoding("gzip")
.post(Entity.json(info));
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey2 HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
server端接收client发送的请求
其实当client发送出请求以后,就已经不知道发到何处了。。。
经过再三的查找,在eureka-server的项目下发现了一个webapp,里面有个web.xml,这个就很神奇,名字叫server,还是个web.xml,这里面不就是拦截器啊
这里的拦截器分为四种:1、负责状态相关的处理逻辑 2、对请求进行授权认证的处理 3、负责限流相关的逻辑 4、负责压缩、编码相关的处理
最后都会走jersey的servlet,这个servlet会根据请求的路径,将请求转发给eureka core里面的resource,继续找core里的resource。
根据请求路径分析,最终会找到ApplicationResource中addInstance方法上去
在这个方法中上来就是一堆的if..else.. 看的人头皮发麻,不过简单了解都是些校验之类的东西,没有深究的必要,后续又执行了一个对我们无关紧要的东西,直接略过,再道最后出现了关键的方法register();
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
// validate that the instanceinfo contains all the necessary required fields
if (isBlank(info.getId())) {
return Response.status(400).entity("Missing instanceId").build();
} else if (isBlank(info.getHostName())) {
return Response.status(400).entity("Missing hostname").build();
} else if (isBlank(info.getIPAddr())) {
return Response.status(400).entity("Missing ip address").build();
} else if (isBlank(info.getAppName())) {
return Response.status(400).entity("Missing appName").build();
} else if (!appName.equals(info.getAppName())) {
return Response.status(400).entity("Mismatched appName, expecting " + appName + " but was " + info.getAppName()).build();
} else if (info.getDataCenterInfo() == null) {
return Response.status(400).entity("Missing dataCenterInfo").build();
} else if (info.getDataCenterInfo().getName() == null) {
return Response.status(400).entity("Missing dataCenterInfo Name").build();
}
// handle cases where clients may be registering with bad DataCenterInfo with missing data
DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
if (dataCenterInfo instanceof UniqueIdentifier) {
String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId();
if (isBlank(dataCenterInfoId)) {
boolean experimental = "true".equalsIgnoreCase(serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
if (experimental) {
String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
return Response.status(400).entity(entity).build();
} else if (dataCenterInfo instanceof AmazonInfo) {
AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo;
String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId);
if (effectiveId == null) {
amazonInfo.getMetadata().put(AmazonInfo.MetaDataKey.instanceId.getName(), info.getId());
}
} else {
logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
}
}
}
registry.register(info, "true".equals(isReplication));
return Response.status(204).build(); // 204 to be backwards compatible
}
继续进入register方法中查看,发现调用了一个super.register
@Override
public void register(final InstanceInfo info, final boolean isReplication) {
int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
super.register(info, leaseDuration, isReplication);
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
进入super.register,便是注册的核心流程
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
//加读锁,表示可以多个实例同时来注册
read.lock();
try {
//从根据注册的服务名取出当前server存储的所有该服务名的实例信息
//注册表结构
/** {
“ServiceA”: {
“001”: Lease<InstanceInfo>,
“002”: Lease<InstanceInfo>,
“003”: Lease<InstanceInfo>
},
“ServiceB”: {
“001”: Lease<InstanceInfo>
}
} **/
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
//如果当前服务没有其他实例信息,说明该服务是第一次注册,构造一个新的并发newMap放如gMap中去,而registry也是一个ConcurrentHashMap,能够保证并发的安全性
if (gMap == null) {
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
//取出注册服务的当前实例信息,如果是第一次注册,一定为null
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
// Retain the last dirty timestamp without overwriting it, if there is already a lease
//判断是否重复注册,并且如果已经存在当前实例信息,说明实例信息发生变动,需要再次注册
if (existingLease != null && (existingLease.getHolder() != null)) {
//取出最后一次存活的脏时间
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
//取出最后一次注册的脏时间
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
// this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
// InstanceInfo instead of the server local copy.
//这是一个 > 而不是 >= 因为如果时间戳相等,我们仍然采用远程传输的 InstanceInfo 而不是服务器本地副本。
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
" than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
registrant = existingLease.getHolder();
}
} else {
// The lease does not exist and hence it is a new registration
//第一次注册
synchronized (lock) {
//设置两个实例属性,和服务故障剔除有关
if (this.expectedNumberOfClientsSendingRenews > 0) {
// Since the client wants to register it, increase the number of clients sending renews
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
updateRenewsPerMinThreshold();
}
}
logger.debug("No previous lease information found; it is new registration");
}
//将当前注册实例封装到Lease中,并放入本地注册表中
Lease<InstanceInfo> lease = new Lease<>(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
gMap.put(registrant.getId(), lease);
//维护最近注册队列,用于统计信息
recentRegisteredQueue.add(new Pair<Long, String>(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
// This is where the initial state transfer of overridden status happens
//处理实例状态
if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
+ "overrides", registrant.getOverriddenStatus(), registrant.getId());
if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
logger.info("Not found overridden id {} and hence adding it", registrant.getId());
overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
// Set the status based on the overridden status rules
InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
// If the lease is registered with UP status, set lease service up timestamp
//如果租约注册为 UP 状态,则设置租约服务上线时间戳
if (InstanceStatus.UP.equals(registrant.getStatus())) {
lease.serviceUp();
}
registrant.setActionType(ActionType.ADDED);
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
//清楚缓存
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
logger.info("Registered instance {}/{} with status {} (replication={})",
registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
} finally {
read.unlock();
}
}
服务注册的方法主要为几步:
1、获取原始注册表中当前实例服务名称的所有信息
2、根据当前注册实例的id判断是否为第一次注册,分别走不同的逻辑
3、将注册信息封装到gMap中
4、根据实例状态来做不同的事情,如果为up状态,则将信息放入最近变更的队列中去
5、清除缓存
总结
整体上服务的注册流程已经完成,其中逻辑大体上比较清晰顺畅,但是比较坑爹的是client的服务注册的入口比较隐秘,不易查找,通过反推法最终也可以找到。