0. 前言
- springboot版本:2.1.9.RELEASE
- springcloud版本:Greenwich.SR4
1. 客户端发起注册请求场景
- 客户端初始化时直接注册(需要在配置文件配置)
- 客户端发起心跳续租请求时收到服务端返回404,会立即再发起注册
- 当客户端检测到相应配置更新时,向服务端发起注册请求进行数据同步
2. ApplicationResource
服务端处理客户端的注册请求,在 ApplicationResource 类中的 addInstance() 方法
ps:Eureka 中大量打印 debug 级别日志,可以在配置文件配置 logging.level.root=debug
,方便阅读源码调试
// ApplicationResource.class
@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());
}
}
}
// 3 处理客户端实例注册
registry.register(info, "true".equals(isReplication));
return Response.status(204).build(); // 204 to be backwards compatible
}
3. 处理客户端实例注册
// PeerAwareInstanceRegistryImpl.class
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();
}
// 4 调用父类注册方法
super.register(info, leaseDuration, isReplication);
// 5 同步复制给集群节点
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
4. 父类注册方法
《EurekaServer-同步注册表机制》中已讲过
5. replicateToPeers()
// PeerAwareInstanceRegistryImpl.class
private void replicateToPeers(Action action, String appName, String id,
InstanceInfo info /* optional */,
InstanceStatus newStatus /* optional */, boolean isReplication) {
Stopwatch tracer = action.getTimer().start();
try {
// isReplication:是否是集群节点间同步复制的请求
if (isReplication) {
// 如果是集群节点间同步复制的请求,最近一分钟处理同步复制请求数+1
numberOfReplicationsLastMin.increment();
}
// If it is a replication already, do not replicate again as this will create a poison replication
if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
// 如果本地配置的集群节点为空,则不进行同步复制
// 如果是集群节点间同步复制的请求,则不进行同步复制(设想一下,如果集群节点间不断同步复制,那么会形成一个循环永不结束)
return;
}
// peerEurekaNodes:存放集群节点
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
// If the url represents this host, do not replicate to yourself.
// 如果 url 表示的是当前主机,就不要复制给自己了
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
// 5.1 同步复制给集群节点处理
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
} finally {
tracer.stop();
}
}
5.1 同步复制给集群节点处理
// PeerAwareInstanceRegistryImpl.class
private void replicateInstanceActionsToPeers(Action action, String appName,
String id, InstanceInfo info, InstanceStatus newStatus,
PeerEurekaNode node) {
try {
InstanceInfo infoFromRegistry = null;
CurrentRequestVersion.set(Version.V2);
// 此时为 action = Action.Register
switch (action) {
case Cancel:
// 下架
node.cancel(appName, id);
break;
case Heartbeat:
// 心跳续租
InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
infoFromRegistry = getInstanceByAppAndId(appName, id, false);
node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
break;
case Register:
// 5.2 注册
node.register(info);
break;
case StatusUpdate:
// 更改状态
infoFromRegistry = getInstanceByAppAndId(appName, id, false);
node.statusUpdate(appName, id, newStatus, infoFromRegistry);
break;
case DeleteStatusOverride:
// 删除状态
infoFromRegistry = getInstanceByAppAndId(appName, id, false);
node.deleteStatusOverride(appName, id, infoFromRegistry);
break;
}
} catch (Throwable t) {
logger.error("Cannot replicate information to {} for action {}", node.getServiceUrl(), action.name(), t);
}
}
5.2 node.register()
// PeerEurekaNode.class
public void register(final InstanceInfo info) throws Exception {
long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
// batchingDispatcher:批量处理执行器,把任务放入队列中,后台有专门的线程对队列进行处理
batchingDispatcher.process(
taskId("register", info),
new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
public EurekaHttpResponse<Void> execute() {
// 5.3 发起注册请求
return replicationClient.register(info);
}
},
expiryTime
);
}
5.3 replicationClient.register()
// AbstractJerseyEurekaHttpClient.class
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
try {
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
// 添加 HEADER_REPLICATION = true 标记,表明这是一个集群节点间的同步复制请求
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();
}
}
}
// JerseyReplicationClient.class
@Override
protected void addExtraHeaders(Builder webResource) {
webResource.header(PeerEurekaNode.HEADER_REPLICATION, "true");
}
- 集群节点间同步复制处理下架、心跳续租、删除状态、更新状态都和注册类似
6. 总结
- 服务端接收到客户端实例的注册请求后,先注册相关实例信息到本地注册表
- 然后同步复制给集群节点