0. 前言
- springboot版本:2.1.9.RELEASE
- springcloud版本:Greenwich.SR4
1. InstanceResource
服务端处理客户端心跳续租请求,在 InstanceResource 类的 renewLease() 方法
// InstanceResource.class
public Response renewLease(
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
@QueryParam("overriddenstatus") String overriddenStatus,
@QueryParam("status") String status,
@QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
// isReplication:是否是集群节点间的同步复制请求,如果是客户端服务实例发送续租请求为 null ,如果是集群节点同步复制为 true
// overriddenStatus:覆盖状态
// status:真实的状态
// lastDirtyTimestamp:客户端保存的最新修改时间戳(脏)
boolean isFromReplicaNode = "true".equals(isReplication);
// 2 调用心跳续租方法
boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
// Not found in the registry, immediately ask for a register
if (!isSuccess) {
logger.warn("Not Found (Renew): {} - {}", app.getName(), id);
// 本地注册表中没有相应实例信息,返回404
// 客户端在发起续租心跳请求后收到服务端返回404,会立即再进行注册
return Response.status(Status.NOT_FOUND).build();
}
// Check if we need to sync based on dirty time stamp, the client
// instance might have changed some value
// 校验,如果我们需要根据客服端的最新修改时间戳(脏)同步,客户端实例可能需要更改数据
Response response;
if (lastDirtyTimestamp != null && serverConfig.shouldSyncWhenTimestampDiffers()) {
// 客户端请求中的最新修改时间戳(脏)不为空 且 本地配置的 shouldSyncWhenTimestampDiffers = true 时
// shouldSyncWhenTimestampDiffers:检查最新修改时间戳(脏)不同时是否同步实例信息
// 4 检查本地的和客户端保存的最新修改时间戳(脏),根据具体情况返回相应的请求结果
response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp), isFromReplicaNode);
// Store the overridden status since the validation found out the node that replicates wins
if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()
&& (overriddenStatus != null)
&& !(InstanceStatus.UNKNOWN.name().equals(overriddenStatus))
&& isFromReplicaNode) {
// 检查后,如果满足下列三个条件:
// 1. 是集群节点同步复制到本地
// 2. 本地注册表中相应实例的最新修改时间戳(脏)小于同步复制过来的
// 3. 同步复制心跳续租请求中的 overriddenStatus 不为 null 也不为 UNKNOWN
// 2.6 更新本地相应的实例信息(覆盖状态)
registry.storeOverriddenStatusIfRequired(app.getAppName(), id, InstanceStatus.valueOf(overriddenStatus));
}
} else {
// 返回成功200
response = Response.ok().build();
}
logger.debug("Found (Renew): {} - {}; reply status={}", app.getName(), id, response.getStatus());
return response;
}
2. 处理心跳续租
// InstanceRegistry.class
public boolean renew(final String appName, final String serverId,
boolean isReplication) {
log("renew " + appName + " serverId " + serverId + ", isReplication {}"
+ isReplication);
// 获取本地所有已注册的服务实例
List<Application> applications = getSortedApplications();
// 遍历找出需要心跳续租的实例信息
for (Application input : applications) {
if (input.getName().equals(appName)) {
InstanceInfo instance = null;
for (InstanceInfo info : input.getInstances()) {
if (info.getId().equals(serverId)) {
instance = info;
break;
}
}
// 发布实例心跳续租事件
publishEvent(new EurekaInstanceRenewedEvent(this, appName, serverId,
instance, isReplication));
break;
}
}
// 2.1 调用父类续租方法
return super.renew(appName, serverId, isReplication);
}
2.1 super.renew()
// PeerAwareInstanceRegistryImpl.class
public boolean renew(final String appName, final String id, final boolean isReplication) {
// 2.2 调用父类心跳续租方法
if (super.renew(appName, id, isReplication)) {
// 2.3 心跳续租成功后同步复制给集群节点
replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
return true;
}
return false;
}
2.2 super.renew()
// AbstractInstanceRegistry.class
public boolean renew(String appName, String id, boolean isReplication) {
RENEW.increment(isReplication);
// 获取服务租约信息
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToRenew = null;
if (gMap != null) {
// 获取实例租约信息
leaseToRenew = gMap.get(id);
}
if (leaseToRenew == null) {
RENEW_NOT_FOUND.increment(isReplication);
logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
// 如果本地注册表中找不到实例租约信息,则返回 false
return false;
} else {
// 获取实例信息
InstanceInfo instanceInfo = leaseToRenew.getHolder();
if (instanceInfo != null) {
// touchASGCache(instanceInfo.getASGName());
// 根据规则,计算出 overriddenInstanceStatus
InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
instanceInfo, leaseToRenew, isReplication);
if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
// 计算后覆盖状态如果为 UNKNOWN ,返回 false
// 覆盖状态为 UNKNOWN 的情况:
// 1. overiddenStatusMap 中相应实例的 overiddenStatus 为 UNKNOWN
// 2. 本地注册表中实例的覆盖状态为 UNKNOWN
// 出现的这种状况的原因:
// 1. 客户端发起过删除状态请求(此时 overiddenStatusMap 中取出来为 null,overiddenStatus 为 UNKNOWN )
// 2. 客户端发起过修改状态请求(通过 actuator 设置 overiddenStatusMap 中相应的 overiddenStatus 为 UNKNOWN )
// 刚注册的情况 overiddenStatusMap 中取出来为 null,只有通过外部修改状态才会有值
logger.info("Instance status UNKNOWN possibly due to deleted override for instance {}"
+ "; re-register required", instanceInfo.getId());
RENEW_NOT_FOUND.increment(isReplication);
return false;
}
if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
logger.info(
"The instance status {} is different from overridden instance status {} for instance {}. "
+ "Hence setting the status to overridden status", instanceInfo.getStatus().name(),
instanceInfo.getOverriddenStatus().name(),
instanceInfo.getId());
// 实例信息的实例状态和覆盖状态不一致时,将实例状态的值设置为覆盖状态的值,并且不记录本地实例信息中的最新修改时间戳(脏)
instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
}
}
// 核心,最近一分钟处理的心跳续租数+1
renewsLastMin.increment();
// 核心,更新续租到期时间
leaseToRenew.renew();
return true;
}
}
// Lease.class
public void renew() {
// 更新续租到期时间,默认为当前时间+90s
lastUpdateTimestamp = System.currentTimeMillis() + duration;
}
2.3 replicateToPeers()
具体已在《EurekaServer-处理客户端注册请求》中讲过
// PeerAwareInstanceRegistryImpl.class
private void replicateToPeers(Action action, String appName, String id,
InstanceInfo info /* optional */,
InstanceStatus newStatus /* optional */, boolean isReplication) {
// ......
try {
// ......
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
// ......
// 同步复制心跳续租给集群节点
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
} finally {
tracer.stop();
}
}
// 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.Heartbeat
switch (action) {
// ......
case Heartbeat:
// 心跳
InstanceStatus overriddenStatus = overriddenInstanceStatusMap.get(id);
infoFromRegistry = getInstanceByAppAndId(appName, id, false);
// 2.4 同步复制心跳给当前集群节点 node
node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
break;
// ......
} catch (Throwable t) {
// ......
}
}
2.4 node.heartbeat()
// PeerEurekaNode.class
public void heartbeat(final String appName, final String id,
final InstanceInfo info, final InstanceStatus overriddenStatus,
boolean primeConnection) throws Throwable {
if (primeConnection) {
// We do not care about the result for priming request.
// PeerEurekaNode 类下的 heartbeat()方法被调用有两处:
// 1. 服务端处理客户端续租请求时,同步复制给集群节点, primeConnection = false
// 2. Aws 调用, primeConnection = true
replicationClient.sendHeartBeat(appName, id, info, overriddenStatus);
return;
}
// 同步复制任务,实现了发送请求的处理方法和请求失败后的处理方法
ReplicationTask replicationTask = new InstanceReplicationTask(targetHost, Action.Heartbeat, info, overriddenStatus, false) {
@Override
public EurekaHttpResponse<InstanceInfo> execute() throws Throwable {
// 发起续租请求
return replicationClient.sendHeartBeat(appName, id, info, overriddenStatus);
}
@Override
public void handleFailure(int statusCode, Object responseEntity) throws Throwable {
// 简单打印相关日志
super.handleFailure(statusCode, responseEntity);
if (statusCode == 404) {
logger.warn("{}: missing entry.", getTaskName());
if (info != null) {
logger.warn("{}: cannot find instance id {} and hence replicating the instance with status {}",
getTaskName(), info.getId(), info.getStatus());
// 请求返回404,则立即重新发起注册请求同步复制到集群节点
// 返回404是因为 本地注册表中相应实例的最新修改时间戳(脏) 大于 集群节点的最新修改时间戳(脏)
// 表明本地服务端的实例信息比集群节点的新
register(info);
}
} else if (config.shouldSyncWhenTimestampDiffers()) {
// “实例最新修改时间戳(脏)不同时是否同步实例信息”配置开启时
InstanceInfo peerInstanceInfo = (InstanceInfo) responseEntity;
if (peerInstanceInfo != null) {
// 当 本地注册表中相应实例的最新修改时间戳(脏) 小于 集群节点的最新修改时间戳(脏)
// 表明本地服务端的实例信息比集群节点的旧
// 2.5 根据请求返回响应体,同步集群节点的实例信息到本地注册表
syncInstancesIfTimestampDiffers(appName, id, info, peerInstanceInfo);
}
}
}
};
long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
// batchingDispatcher 执行器,把任务放入队列中,后台有专门的线程对队列进行处理
batchingDispatcher.process(taskId("heartbeat", info), replicationTask, expiryTime);
}
2.5 syncInstancesIfTimestampDiffers()
// PeerEurekaNode.class
private void syncInstancesIfTimestampDiffers(String appName, String id, InstanceInfo info, InstanceInfo infoFromPeer) {
try {
if (infoFromPeer != null) {
logger.warn("Peer wants us to take the instance information from it, since the timestamp differs,"
+ "Id : {} My Timestamp : {}, Peer's timestamp: {}", id, info.getLastDirtyTimestamp(), infoFromPeer.getLastDirtyTimestamp());
if (infoFromPeer.getOverriddenStatus() != null && !InstanceStatus.UNKNOWN.equals(infoFromPeer.getOverriddenStatus())) {
logger.warn("Overridden Status info -id {}, mine {}, peer's {}", id, info.getOverriddenStatus(), infoFromPeer.getOverriddenStatus());
// 2.6 更新本地相应的实例信息(覆盖状态)
registry.storeOverriddenStatusIfRequired(appName, id, infoFromPeer.getOverriddenStatus());
}
// 注册实例信息到本地注册表
registry.register(infoFromPeer, true);
}
} catch (Throwable e) {
logger.warn("Exception when trying to set information from peer :", e);
}
}
2.6 registry.storeOverriddenStatusIfRequired()
// AbstractInstanceRegistry.class
public void storeOverriddenStatusIfRequired(String appName, String id, InstanceStatus overriddenStatus) {
InstanceStatus instanceStatus = overriddenInstanceStatusMap.get(id);
// 如果本地 overriddenInstanceStatusMap 中相应实例的 overriddenStatus 为 null 或者和集群节点同步复制请求中的 overriddenStatus 不一致
// 则更新本地的 overriddenInstanceStatusMap 和 instanceInfo 中的 overriddenStatus
if ((instanceStatus == null) || (!overriddenStatus.equals(instanceStatus))) {
// We might not have the overridden status if the server got
// restarted -this will help us maintain the overridden state
// from the replica
logger.info("Adding overridden status for instance id {} and the value is {}",
id, overriddenStatus.name());
overriddenInstanceStatusMap.put(id, overriddenStatus);
InstanceInfo instanceInfo = this.getInstanceByAppAndId(appName, id, false);
instanceInfo.setOverriddenStatus(overriddenStatus);
logger.info("Set the overridden status for instance (appname:{}, id:{}} and the value is {} ",
appName, id, overriddenStatus.name());
}
}
3. 关键配置 syncWhenTimestampDiffers
假设,当客户端服务实例发起续租请求到服务端 A 时,服务端 A 处理完续租成功后,需要同步复制给集群的服务端 B 节点,这个时候可能存在服务端 A 和服务端 B 两边注册表中的同一实例的最新修改时间戳不一致,会有下列处理情况:
- 如果服务端 A 的实例最新修改时间戳(脏)大于服务端 B 的的实例最新修改时间戳(脏),则服务端 B 返回404给服务端 A 。服务端 A 根据返回404立即再发起同步注册请求给 服务端 B 。
- 如果服务端 A 的实例最新修改时间戳(脏)小于服务端 B 的的实例最新修改时间戳(脏),则服务端 B 返回409和服务端 B 中的实例信息给服务端 A 。服务端 A 根据返回409和响应体,同步服务端 B 的实例信息到本地注册表。
上述前提是需要在配置文件中,下列配置需要开启(默认开启):
eureka.server.sync-when-timestamp-differs=true
4. validateDirtyTimestamp()
// InstanceResource.class
private Response validateDirtyTimestamp(Long lastDirtyTimestamp,
boolean isReplication) {
// 获取本地相应实例信息
InstanceInfo appInfo = registry.getInstanceByAppAndId(app.getName(), id, false);
if (appInfo != null) {
if ((lastDirtyTimestamp != null) && (!lastDirtyTimestamp.equals(appInfo.getLastDirtyTimestamp()))) {
// 客户端实例续租请求中的最新修改时间戳(脏) 和 本地注册表中实例的最新修改时间戳(脏) 不相等时
Object[] args = {id, appInfo.getLastDirtyTimestamp(), lastDirtyTimestamp, isReplication};
if (lastDirtyTimestamp > appInfo.getLastDirtyTimestamp()) {
logger.debug(
"Time to sync, since the last dirty timestamp differs -"
+ " ReplicationInstance id : {},Registry : {} Incoming: {} Replication: {}",
args);
// 如果 客户端实例续租请求中的最新修改时间戳(脏) 大于 本地注册表中相应实例的最新修改时间戳(脏)
// 返回404,让客户端立即发起注册给当前服务端更新相应实例信息
return Response.status(Status.NOT_FOUND).build();
} else if (appInfo.getLastDirtyTimestamp() > lastDirtyTimestamp) {
// In the case of replication, send the current instance info in the registry for the
// replicating node to sync itself with this one.
// 如果 客户端实例续租请求中的最新修改时间戳(脏) 小于 本地注册表中相应实例的最新修改时间戳(脏)
// 客户端因角色不同处理可能有下列情况:
// 1. 如果是集群节点,同步复制给当前服务端,则当前服务端返回409且响应体中包含本地注册表的相应实例信息,让集群节点更新相应信息
// 2. 如果是服务实例,为自己发起心跳续租请求,则当前服务端返回成功200
if (isReplication) {
logger.debug(
"Time to sync, since the last dirty timestamp differs -"
+ " ReplicationInstance id : {},Registry : {} Incoming: {} Replication: {}",
args);
return Response.status(Status.CONFLICT).entity(appInfo).build();
} else {
return Response.ok().build();
}
}
}
}
// 返回成功200
return Response.ok().build();
}
5. 总结
- 首先,服务端接收到客户端发起的心跳续租请求后,会在本地注册表中获取相应实例信息,最近一分钟处理心跳续租请求数+1,更新续租过期时间
- 然后,同步复制给集群节点,根据集群节点返回的结果,如果不是成功进行相应处理:
- 集群节点返回404,表明本地注册表中相应实例信息比集群节点的新,立即再发起同步注册请求给集群节点
- 集群节点返回409,表明本地注册表中相应实例信息比集群节点的旧,根据请求返回响应体中的实例信息,更新本地相关实例信息(覆盖状态)和注册实例信息到本地注册表
- 最后,处理返回结果,也有不同的方式:
- 请求中的实例最新修改时间戳(脏) 大于 本地相应实例信息的最新修改时间戳(脏) ,则返回404让客户端立即再发起注册请求
- 如果是集群节点发起的请求,请求中的 overriddenStatus 不为 null 也不为 UNKNOWN,还需要更新本地相应的实例信息(覆盖状态)
- 请求中的实例最新修改时间戳(脏) 小于 本地相应实例信息的最新修改时间戳(脏) ,根据发起请求的角色处理方式也不同:
- 服务实例发起的请求,则返回200
- 集群节点发起的请求,则返回409且响应体中包含本地相应实例信息,让集群节点更新相应信息
- 请求中的实例最新修改时间戳(脏) 等于 本地相应实例信息的最新修改时间戳(脏) ,返回成功200
- 请求中的实例最新修改时间戳(脏) 大于 本地相应实例信息的最新修改时间戳(脏) ,则返回404让客户端立即再发起注册请求