前言
在前一篇文章里,描述了Eureka的启动流程和剔除任务,本篇将会继续介绍Eureka Server端的注册、集群同步的相关概念
Server端服务的注册
首先找到入口类ApplicationResource:
@Produces({"application/xml", "application/json"})
public class ApplicationResource {
// 省略其他方法和属性
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info, @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
// 1. 参数验证, 省略
// 2. 数据中心相关的处理, 不做介绍
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());
}
}
}
// 完成注册功能的逻辑, 注意isReplication
registry.register(info, "true".equals(isReplication));
// 注册成功后的状态码
return Response.status(204).build();
}
}
完成注册的核心逻辑为register方法,里面有个参数isReplication,该参数从header中获取,其含义为:
- 当isReplication为null时:表示服务的注册请求(对应即为false)。
- 当isReplication为true时:表示集群间的服务同步请求。
这里先看注册,所以isReplication为null,那么上面倒数第二行代码处调用的逻辑,参数实际为registry.register(info, false);
下面来看该方法的具体实现:
# PeerAwareInstanceRegistryImpl
@Override
public void register(final InstanceInfo info, final boolean isReplication) {
// 租约的有效期, 默认时间为90秒
int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
super.register(info, leaseDuration, isReplication); // [1]
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication); // [2]
}
该方法的实现是在PeerAwareInstanceRegistryImpl类中,通过上面的源码,得出注册时, 主要完成两件事:
- 完成注册功能。该功能是在其父类AbstractInstanceRegistry中实现的
- 将刚刚注册的这个服务,同步到集群中的其他peer中去
先看注册功能,对应上面[1]处的代码:
# stractInstanceRegistry
public abstract class AbstractInstanceRegistry implements InstanceRegistry {
// 省略其他的属性和方法、日志等代码
// 存储注册过来的服务,可以理解为一级缓存,是实时的
// 结构为:Map<appName, <instanceId, InstanceInfo>>
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
// 该类完成了二级缓存和三级缓存的同步、读取等功能,会详细说明
protected volatile ResponseCache responseCache;
// 新服务过来的注册, 就是通过该方法完成的, 这里的isReplication值前面已经说过了, 为false
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
read.lock();
// 取出appName对应的服务集合
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
// 为空, 则创建一个, 这里其实就是为空的话, 初始化一下。
if (gMap == null) {
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
// 把新创建的空元素gNewMap, 存入registry中, 并把gNewMap赋值给gMap
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
// 获取注册前的服务列表
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
if (existingLease != null && (existingLease.getHolder() != null)) {
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
registrant = existingLease.getHolder();
}
} else {
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();
}
}
}
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
// 完成向registry中添加一条租约信息
gMap.put(registrant.getId(), lease);
// 往recentRegisteredQueue添加一条信息
recentRegisteredQueue.add(new Pair<Long, String>(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
registrant.getId());
if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
if (InstanceStatus.UP.equals(registrant.getStatus())) {
// 修改服务的启动时间为当前时间
lease.serviceUp();
}
// 注意这里的动作类型为add
registrant.setActionType(ActionType.ADDED);
// 往recentlyChangedQueue添加该租约信息
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
// 使appName对应的注册表readWriteCacheMap(二级缓存)中的服务,全都失效
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
} finally {
read.unlock();
}
}
}
总结一下上面的这个注册方法:
- 往注册表registry中添加一条注册信息
- 使注册表readWriteCacheMap中,appName对应的租约信息失效。
这里就引申出一个问题,新注册服务后,readWriteCacheMap中如果存在旧的对应服务会被清除掉,那这个缓存是什么时候被放进去的?这个后面会详细说明。
上面聊了[1]处服务的注册,下面再聊聊[2]处的集群同步逻辑。
# PeerAwareInstanceRegistryImpl
private void replicateToPeers(Action action, String appName, String id,
InstanceInfo info /* optional */,
InstanceStatus newStatus /* optional */, boolean isReplication) {
Stopwatch tracer = action.getTimer().start();
try {
// 如果是新注册, 而不是向其他集群同步, 则数量原子加1
if (isReplication) {
numberOfReplicationsLastMin.increment();
}
// 这个很重要, 下面会细说
if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
return;
}
// 完成将刚刚注册完成的服务, 向集群中其他peer节点的同步功能
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
// 得把自己排除掉
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
// 这里的action是个枚举,上一步传过来的为注册类型Register,
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
} finally {
tracer.stop();
}
}
来分析下上面代码的第二个if。该if有两个条件,第一个条件表示存不存在对等节点peer,第二个参数isReplication,在前面说过其含义,这里是新注册的功能,所以这里的值是false。所以当在集群环境下,又是新册的时候,那么这个if条件不满足,所以会执行下面的for循环,向其他的peer同步。
下面再看replicateInstanceActionsToPeers(..)
方法:
@Singleton
public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry {
private void replicateInstanceActionsToPeers(Action action, String appName, String id, InstanceInfo info, InstanceStatus newStatus, PeerEurekaNode node) {
try {
InstanceInfo infoFromRegistry;
CurrentRequestVersion.set(Version.V2);
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;
// 向其他peer注册
case Register:
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);
} finally {
CurrentRequestVersion.remove();
}
}
}
# PeerEurekaNode
public void register(final InstanceInfo info) throws Exception {
long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
batchingDispatcher.process(
taskId("register", info),
// 这里最后一个参数即前面说的isReplication,传入了true
new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
public EurekaHttpResponse<Void> execute() {
return replicationClient.register(info);
}
},
expiryTime
);
}
上面再创建实例复制任务InstanceReplicationTask时候,传入的最后一个参数isReplication为true,所在在下一次向其他peer再次进行同步的时候,前面说过得两个if条件,第二个参数变成了true,所以会结束掉,不会执行下面的for循环。从这里我们可以得出结论,Eureka在超过两个节点之后,最好在配置里,将所有的节点都配置上,否则会出现数据不同的情况。这里补充一下,Eureka是基于Http完成节点的注册、续约等功能的。
总结
本篇主要介绍了Server端服务注册的过程,在该过程中,主要完成了三件事:
- 完成注册功能,添加到registry中。
- 清空对应服务的缓存。
- 向其他peer节点同步,并且只会同步一次,由参数isReplication来控制,true表示同步,false表示注册。
文章最后简单说下一peer的概念:
# 7900.yml
eureka:
client:
service-url:
defaultZone: http://euk1.com:7900/eureka/,http://euk1.com:7901/eureka/,http://euk1.com:7902/eureka/
在7900.yml这个配置里, 7901和7902就是7900的peer。也就是说,出现在它的配置列表里的Eureka节点,就是他的peer