一.服务注册接口
1.前景回顾
SpringCloud-Eureka服务端如何给客户端提供服务?
在上一篇中,讲述了Eureka如何提供服务,且Jesery如何拦截请求,并进行url解析的过程。最后找到了具体业务处理的位置。
今天尝试来捋一下在Eureka诸多接口之中的注册接口com.netflix.eureka.resources.ApplicationResource#addInstance
2.基本代码解读
com.netflix.eureka.resources.ApplicationResource#addInstance
方法做些什么:
- 参数校验
- 数据中心处理(非重点)
- 交给
InstanceRegistry
进一步注册
@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);
// 1.参数校验
if (isBlank(info.getId())) {
// 实例ID校验,组成:域名:服务名:端口
// windows10.microdone.cn:springcloud-eureka:6662
// 这个是可以自定义的,可以参考第三篇中application.xml的eureka.instance.instance-id的配置
return Response.status(400).entity("Missing instanceId").build();
} else if (isBlank(info.getHostName())) {
// 对应eureka.instance.hostname 配置 SpringCloudEurekaNodeA
return Response.status(400).entity("Missing hostname").build();
} else if (isBlank(info.getIPAddr())) {
// IP地址
return Response.status(400).entity("Missing ip address").build();
} else if (isBlank(info.getAppName())) {
//应用名称,对应配置spring.application.name 示例:SPRINGCLOUD-EUREKA
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) {
// 数据中心名称校验,如果不是走亚马逊部署的话,那么这个值默认是MyOwn
return Response.status(400).entity("Missing dataCenterInfo Name").build();
}
// handle cases where clients may be registering with bad DataCenterInfo with missing data
//2.(不重要)处理客户端注册到错误的DataCenterInfo时可能缺少数据的情况
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());
}
}
}
// 3.(核心) 注册instance到注册表中, isReplication标识是否为副本,同为eureka服务端时为true
registry.register(info, "true".equals(isReplication));
return Response.status(204).build(); // 204 to be backwards compatible
}
2.InstanceRegistry的过渡
在上面的源码解析中,可以看到在ApplicationResource
只做了参与校验与客户端是否注册到错误的数据中心校验。然后将注册的事情交给了org.springframework.cloud.netflix.eureka.server.InstanceRegistry
处理。
然而InstaceRegistry
只是做了一个事件推送处理,具体的注册事情是交给了它的父类PeerAwareInstanceRegistryImpl
3.PeerAwareInstanceRegistryImpl做啥呢?
1.确定续约时间,如果客户端有设置,那就用客户端的,否则就用服务端的默认值(服务端进行失效剔除使用)
2.复制注册信息到服务器端进群节点去
好了,继续深入,在AbstractInstanceRegistry进行真正的注册
4.真正的注册
直接搂源码
1.获取实例集群MAP,没有就初始化
2.判断是否已注册过(断线重连,重启客户端,分区隔离),新旧数据进行合并
3.客户端在线状态判断,新旧数据覆写,上线时间,最后修改时间等监控信息添加
/**
* Registers a new instance with a given duration.
*
* @see com.netflix.eureka.lease.LeaseManager#register(java.lang.Object, int, boolean)
*/
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
//写的时候,读锁一下
read.lock();
//1. 获取实例集群MAP
//一个服务可能存在多个实例来构成集群,所以是个Map
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
// 统计是客户端过来实例化的还是复制过来的
REGISTER.increment(isReplication);
if (gMap == null) {
//如果服务器还没有注册过该应用,那么初始化一个,放到注册中心registry(没错,绝大多数的注册表都是Map集合)中
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
、 // 2.获取注册的续约对象(封装了实例信息),如果存在表示不是第一次注册
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.
//如果从当前注册表获取的实例对象的时间的大于当前注册过来的时间,那直接用已存在的实例对象
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
// 2.如果实例对象不存在,那么这是个新的注册
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<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
if (existingLease != null) {
// 如果服务端本身已存在该实例,那么服务上线时间戳,用服务端已存在的
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
// 将续约对象丢到服务集群map中
gMap.put(registrant.getId(), lease);
synchronized (recentRegisteredQueue) {
// 最近已注册的队列,新增一个
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
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();
}
}
5.Eureka服务器节点如何同步
用自己的eureka客户端向服务器发送注册实例信息
二.总结
1.每个服务器端也都是客户端
2.服务端在接收到客户端的注册信息后,会将一个服务的集群放在同一个map中,通过instanceId(可以自定义)作为键值。
3.最后整个注册表也是个map,以应用名称为key,集群map为值
4.会将非复制节点注册信息,同步到其他服务器上,很多其他信息也是通过这个方法同步的,我们可以看到节点移除,心跳,状态更新,移除状态覆写都是通过这个方法去同步的。