Eureka 系列(07)服务注册与主动下线

Spring Cloud 系列目录 - Eureka 篇

在上一篇 Eureka 系列(05)消息广播 中对 Eureka 消息广播的源码进行了分析,之后的几篇文章会具体分析本地服务注册、主动下线、心跳续约、自动过期等的实现机制。

  • PeerAwareInstanceRegistryImpl 负责集群内部消息通信。
  • AbstractInstanceRegistry 负责本地服务信息管理,这也是之后几篇文章关注的重点。

表1:Eureka OPEN API

资源 功能 url
ApplicationsResource 获取全部或增量服务实例信息 GET /apps
GET /apps/delta
ApplicationResource 1. 获取单个应用的信息
2. 注册实例信息
GET /apps/{appName}
POST /apps/{appName}
InstanceResource 服务实例的CURD:
1. 获取实例的信息
2.修改服务实例元信息
3. 删除实例信息,服务下线
4. 发送心跳
GET /apps/{appName}/{id}
PUT /apps/{appName}/{id}/metadata
DELETE /apps/{appName}/{id}
PUT /apps/{appName}/{id}
InstancesResource 直接根据实例id获取实例信息 GET /instances/{id}
PeerReplicationResource 集群内部批量数据同步 POST /peerreplication/batch
ServerInfoResource ??? POST /serverinfo/statusoverrides
StatusResource ??? GET /statusoverrides

注: {appName} 表示应用名称或服务id,{id} 表示实例id。eg: http://localhost:8080/eureka/apps

1. 服务注册

1.1 服务实例注册流程

图1:Eureka 服务实例注册时序图

sequenceDiagram
participant ApplicationResource
participant PeerAwareInstanceRegistryImpl
participant AbstractInstanceRegistry
participant PeerEurekaNode
note over ApplicationResource: POST:/euraka/apps/{appName}<br/>addInstance(instanceInfo,isReplication)
ApplicationResource ->> PeerAwareInstanceRegistryImpl: 注册请求:register(instanceInfo,isReplication)
PeerAwareInstanceRegistryImpl ->> AbstractInstanceRegistry: 1. 本地数据更新: register(instanceInfo,leaseDuration,isReplication)
loop 同步到其它 Eureka Server 节点
PeerAwareInstanceRegistryImpl ->> PeerAwareInstanceRegistryImpl: 2.1 数据同步:replicateInstanceActionsToPeers
PeerAwareInstanceRegistryImpl ->> PeerEurekaNode: 2.2 register(instanceInfo) -> POST:/euraka/apps/{appName}
end

总结: Eureka Web 使用的是 Jersey 容器,服务注册的请求入口是 ApplicationResource 的 register 方法,请求的路径是 POST:/euraka/apps/{appName}

1.2 ApplicationResource

// ApplicationResource HTTP请求入口
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
... // 数据检验
registry.register(info, "true".equals(isReplication));
return Response.status(204).build(); // 204 to be backwards compatible
}

总结: ApplicationResource 入口主要是进行参数检验,主要的逻辑都委托给了 PeerAwareInstanceRegistryImpl 完成。

注意:isReplication 参数,如果是客户端的请求则为 false,表示需要将这个消息广播给其它服务器。如果是集群内部消息广播则为true,表示不再需要继续广播,否则会造成循环广播的问题。

// 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);
// 消息广播
replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}

总结: PeerAwareInstanceRegistryImpl 这个类应该都很熟悉了,主要是负责进行集群间内部通信的,其父类 AbstractInstanceRegistry 则负责本地服务信息管理,也是本文的研究重点。

1.3 AbstractInstanceRegistry

在 Eureka 中,服务注册信息存储在内存中,数据结构为 ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry,Map 嵌套了两层,外层是的 Key 是 appName,内存的 Key 是 InstanceId。

private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
read.lock();
// 1. 获取该服务对应的所有服务实例,如果不存在就创建一个新的Map
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>>();
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
} // 2. 两种情况:一是实例已经注册,二是实例没有注册
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
// 2.1 实例已经注册,就需要PK,PK原则:谁最后一次更新就是谁赢
// 也就是说如果已经注册的实例最近更新了,就不用重新更新了
if (existingLease != null && (existingLease.getHolder() != null)) {
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
// 已经注册的实例PK赢了
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
registrant = existingLease.getHolder();
}
// 2.2 没有注册,很好处理。更新注册的实例个数
} else {
synchronized (lock) {
if (this.expectedNumberOfClientsSendingRenews > 0) {
this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
updateRenewsPerMinThreshold();
}
}
} // 3. 更新注册信息(核心步骤)
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
// (核心步骤)
gMap.put(registrant.getId(), lease);
// 添加到最近的注册队列里面去,以时间戳作为Key,名称作为value,主要是为了运维界面的统计数据
synchronized (recentRegisteredQueue) {
recentRegisteredQueue.add(new Pair<Long, String>(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
} // 4. 更新实例状态 InstanceStatus
if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
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();
} // 5. 清理缓存等善后工作
registrant.setActionType(ActionType.ADDED);
// 租约变更记录队列,记录了实例的每次变化, 用于注册信息的增量获取
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
// 清理缓存 ,传入的参数为key
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
} finally {
read.unlock();
}
}

总结: 前三步的逻辑都很清楚,目的就是更新内存中的实例信息,只是需要注意实例已经存在的情况下需要 PK 一下,原则就是谁最后一次更新就是谁赢。

第四步更新服务实例的状态,OverriddenStatus 参考 InstanceInfo 中 OverriddenStatus 的作用

每五步清理缓存等善后工作。目前来说,到 gMap.put(registrant.getId(), lease)这一步就够了。

2. 主动下线

服务下线对应的 OPEN API 为 DELETE /apps/{appName}/{id}

protected boolean internalCancel(String appName, String id, boolean isReplication) {
try {
read.lock();
CANCEL.increment(isReplication);
// 1. 清空registry中注册的实例信息(核心步骤)
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToCancel = null;
if (gMap != null) {
leaseToCancel = gMap.remove(id);
}
// 2. 添加到 recentCanceledQueue 队列中
synchronized (recentCanceledQueue) {
recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
}
InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
if (leaseToCancel == null) {
CANCEL_NOT_FOUND.increment(isReplication);
return false;
} else {
// 3. 和注册时一样,也要做一下清除缓存等善后工作
leaseToCancel.cancel();
InstanceInfo instanceInfo = leaseToCancel.getHolder();
String vip = null;
String svip = null;
if (instanceInfo != null) {
instanceInfo.setActionType(ActionType.DELETED);
recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
instanceInfo.setLastUpdatedTimestamp();
vip = instanceInfo.getVIPAddress();
svip = instanceInfo.getSecureVipAddress();
}
invalidateCache(appName, vip, svip);
return true;
}
} finally {
read.unlock();
}
}

总结: 服务的注册和主动下线的逻辑还是很清楚的。目前来说,到 gMap.remove(id)这一步就够了。至于细节以后真正用到 Eureka 再继续深入研究。


每天用心记录一点点。内容也许不重要,但习惯很重要!

Eureka 系列(07)服务注册与主动下线的更多相关文章

  1. Eureka系列(二) 服务注册Server端具体实现

    服务注册 Server端流程   我们先看下面这张图片,这张图片简单描述了下我们EurekaClient 在调用EurekaServer 提供的服务注册Http接口,Server端实现接口执行的大致流 ...

  2. Spring Cloud Eureka 2 (Eureka Server搭建服务注册中心)

    工具:IntelliJ IDEA 2017.1.2 x64.maven3.3.9 打开IDE  file===>new===>project next next 选择相应的依赖 next ...

  3. .net core + eureka + spring boot 服务注册与调用

    .net core + eureka + spring boot 服务注册与简单的调用 假期小长假遇上疫情只能去家里蹲了,刚好有时间总结一下. 概述 微服务架构是当前比较火的分布式架构,本篇基于.ne ...

  4. SpringCloud系列之服务注册发现(Eureka)应用篇

    @ 目录 前言 项目版本 Eureka服务端 Eureka客户端 服务访问 前言 大家好,距离上周发布的配置中心基础使用已过去差不多一周啦,趁着周末继续完善后续SpringCloud组件的集成,本次代 ...

  5. 微服务~Eureka实现的服务注册与发现及服务之间的调用

    微服务里一个重要的概念就是服务注册与发现技术,当你有一个新的服务运行后,我们的服务中心可以感知你,然后把加添加到服务列表里,然后当你死掉后,会从服务中心把你移除,而你作为一个服务,对其它服务公开的只是 ...

  6. Spring Cloud(2)A Eureka server端 服务注册建立

    1. 父项目pom <dependency> <groupId>org.springframework.cloud</groupId> <artifactId ...

  7. 微服务Consul系列之服务注册与服务发现

    在进行服务注册之前先确认集群是否建立,关于服务注册可以看上篇微服务Consul系列之集群搭建的介绍,两种注册方式:一种是注册HTTP API.另一种是通过配置文件定义,下面讲解的是基于后者配置文件定义 ...

  8. 【Eureka】 作为服务注册中心,Eureka比Zookeeper好在哪里

    著名的 CAP 理论指出,一个分布式系统不可能同时满足 C(一致性) A(可用性) 和 P(分区容错性).由于分区容错性 P 是在分布式系统中必须保证的,因此我们只能在 A 和 C 之间进行权衡. Z ...

  9. Nacos源码系列—关于服务注册的那些事

    点赞再看,养成习惯,微信搜索[牧小农]关注我获取更多资讯,风里雨里,小农等你,很高兴能够成为你的朋友. 项目源码地址:公众号回复 nacos,即可免费获取源码 简介 首先我们在看Nacos源码之前,要 ...

随机推荐

  1. SQLAlchemy应用到Flask中

    安装模块 pip install Flask-SQLAlchemy 加入Flask-SQLAlchemy第三方组件 from flask import Flask # 导入Flask-SQLAlche ...

  2. pychrm和linux进行链接上传代码

    众享周知:现在在windows文件中我们有pycharm工具帮我们去编辑python脚本,这会省去我们大把的时间让我们进行更多的脚本编辑.有这样的一种方法,我们可以使用pycharm编辑的脚本上传到l ...

  3. 2018-8-10-win10-uwp-横向-AppBarButton

    title author date CreateTime categories win10 uwp 横向 AppBarButton lindexi 2018-08-10 19:16:50 +0800 ...

  4. SSM框架搭建过程

    引入依赖的jar包(pom.xml) a. <!--Spring SpringMVC相关-->  spring-webmvc b. <!--Spring事务-->  sprin ...

  5. 常用yum源之(Percona MySQL)

    [percona]name = CentOS $releasever - Perconabaseurl=http://repo.percona.com/centos/$releasever/os/$b ...

  6. 【leetcode】756. Pyramid Transition Matrix

    题目如下: We are stacking blocks to form a pyramid. Each block has a color which is a one letter string, ...

  7. PHP copy() 函数

    定义和用法 copy() 函数复制文件. 该函数如果成功则返回 TRUE,如果失败则返回 FALSE. 语法 copy(file,to_file) 参数 描述 file 必需.规定要复制的文件. to ...

  8. delphi中SendMessage使用说明

    SendMessage基础知识 函数功能:该函数将指定的消息发送到一个或多个窗口.此函数为指定的窗口调用窗口程序,直到窗口程序处理完消息再返回.而函数PostMessage不同,将一个消息寄送到一个线 ...

  9. c++ 获取文件图标,类型名称,属性 SHGetFileInfo

    SHGetFileInfo是一个相当实用的Windows API函数. // [MoreWindows工作笔记4] 获取文件图标,类型名称,属性 SHGetFileInfo #include < ...

  10. JMeter 阶梯式加压测试插件 Stepping Thread Group

    在日常性能测试过程中,有时需要对被测对象不断的增加压力,直至达到某个值后,并持续运行一段时间.这里将借助jmeter插件模拟这种情况. 本文介绍在jmeter中,使用插件Stepping Thread ...