目录
1、Eureka Server工作流程
- Eureka Server,负责服务注册与发现,负责记录微服务集群下的服务信息,包括服务ip,port等服务基础信息;
- Eureka Client服务在启动时,会将自身机器信息,注册到Eureka Server中;
- Eureka Client服务,每隔30秒,会向Eureka Server发送请求,获取最新的服务注册信息,然后将注册中心上的所有服务信息,更新到本地注册表中;
- Eureka Client服务,每隔30秒,会向Eureka Server发送一次心跳请求,告诉Eureka Server自己还活着,否则Eureka Server会将该服务下线。
2、Eureka Server注册表存储结构
其中,registry变量的key = 服务名称,value = 一个服务的多个实例。
而value = Map<String, Lease<InstanceInfo>>, key为服务实例id,InstanceInfo为服务实例的具体信息(机器ip,port等),Lease则维护了每个服务最近一次发送心跳的时间。
2.1、基本存储结构
Eureka Server的注册表直接基于纯内存,如上图所示,Eureka Server直接在内存中维护了一个ConcurrentHasnMap数据结构,用于保存Eureka Server服务注册信息。
每个Eureka Client服务的注册、服务下线、服务故障,都会再内存里维护和更新这个registry这个注册表。
客户端每隔30秒拉取服务的时候,Eureka Server就会直接提供内存中有变化的注册表数据。
客户端每隔30秒发送心跳请求时,Eureka Server就会直接在内存中更新服务的心跳时间。
2.2、Eureka Server集群之间的注册表同步
- Eureka Client在进行注册、发送心跳、下线、更改状态等操作时,会向Eureka Server集群中的任意一个Eureka Server发送请求;
- 收到请求的Eureka Server更新自身的注册表信息;
- 收到请求的Eureka Server将Client操作数据发送给集群中除自己以外的所有节点。
以Eureka Client注册为例:
a、Eureka Server接收到客户端注册请求,进行实例数据检查,并通过registry注册实例。
@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
}
b、registry方法,将注册信息存储到本地,并在集群间进行同步
@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);
// 注册 下线 故障 心跳 同步到其他eureka server
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}
c、将Eureka Client的注册操作复制到集群中除自己以外的所有节点
private void replicateToPeers(Action action, String appName, String id,
InstanceInfo info /* optional */,
InstanceStatus newStatus /* optional */, boolean isReplication) {
Stopwatch tracer = action.getTimer().start();
try {
if (isReplication) {
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;
}
// 遍历集群中所有节点
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
// 如果遍历到的Eureka Server节点是当前节点,则跳过
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
// 复制实例操作到集群中其他节点
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
} finally {
tracer.stop();
}
}
d、replicateInstanceActionsToPeers执行具体操作
private void replicateInstanceActionsToPeers(Action action, String appName,
String id, InstanceInfo info, InstanceStatus newStatus,
PeerEurekaNode node) {
try {
InstanceInfo infoFromRegistry = null;
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;
// 注册操作
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);
}
}
e、以实例注册 case Registry为例
public void register(final InstanceInfo info) throws Exception {
long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
batchingDispatcher.process(
//将服务的信息转化为字符串
taskId("register", info),
//对复制任务这个实体进行赋值
new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
public EurekaHttpResponse<Void> execute() {
//向其他eureka server发送注册信息
return replicationClient.register(info);
}
},
expiryTime
);
}
@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
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();
}
}
}
3、Eureka Server多级缓存机制
3.1、多级缓存介绍
目的:为了避免同时读写内存数据结构造成的并发冲突问题。
三级缓存:ReadOnlyCacheMap、ReadWriteCacheMap、registry三级缓存。
3.1.1、拉取注册表流程
- 首先从ReadOnlyCacheMap里查缓存的注册表。
- 若没有,就找ReadWriteCacheMap里缓存的注册表。
- 如果还没有,就从内存registry中获取实际的注册表数据。
3.1.2、更新注册表流程
- 在内存registry中更新变更的注册表信息,同时清除ReadWriteCacheMap;
- 一段时间内(30秒),客户端还能从ReadOnlyCacheMap中获取旧的注册表信息;
- 30秒过后,Eureka Server发现ReadWriteCacheMap被清空了,会去清空ReadOnlyCacheMap中旧的注册表信息;
- 下次有新的拉取请求,由于ReadOnlyCacheMap和ReadWriteCacheMap已被清空,此时会直接从内存registry中拉取最新的注册表信息,同时填充各缓存数据。
3.2、多级缓存优点
-
尽可能保证了内存注册表数据不会出现频繁的读写冲突问题。
-
进一步保证对Eureka Server的大量请求,都是快速从纯内存走,性能极高。
以上内容为个人学习汇总,仅供学习参考,如有问题,欢迎在评论区指出,谢谢!
部分图片来源于网络,如有侵权,请联系作者删除。