- 前言:由于自己学习过于浮躁,很多知识学过就忘,因此准备转换学习方式,记录一下自己的学习过程,博文有出错的地方,欢迎留言批评指正。
- 微服务概述及eureka使用就不再概述,直接进入eureka源码部分
- 环境:SpringCloud Hoxton.SR1,SpringBoot2.2.2(官方推荐),jdk8,Maven3.5,IDEA
MVC架构层
Eureka分为eureka-client端与eureka-server端(C/S架构)。作为服务注册与发现的模块,eureka内部采用基于Rest的网络通讯。在我们平时进行开发web应用时,要接受请求、处理请求,大多数采用了SpringMVC的MVC封装,eureka里面也是有一个MVC的架构层,但不是SpringMVC,这里它采用了Jersey框架,关于这个框架,可以点击这里Jersey入门进行大致了解。
@EnableEurekaServer注解
@EnableEurekaServer注解表示该SpringBoot服务作为一个注册中心,为什么有这个注解皆可以成为一个注册中心呢?让我们点击进去这个注解进行观察。
点开这个注解发现,前三个注解是java元注解,@Import注解给Spring容器注册一个EurekaServerMarkerConfiguration配置类,这个类是干什么的?为什么要给Spring容器导入这样一个类呢?打开这个类,看一下这个类到底是干嘛的。
点进去这个配置类,发现它只做了一件事,实例化一个Marker类,到这里看的一头雾水,这个Marker好像没有任何作用,实际上marker是标记的意思,在这里也只是仅用来实现标记功能。具体这个Marker在哪被使用的?要从SpringBoot自动配置原理来看了,自动配置原理这里不作赘述,打开springcloud整合eureka的maven依赖包,如下,注意是是spring-cloud-netflix-eureka-server包,不是含有starter的那个包,不要找错了。
这个包里有一个META-INF目录,找到spring.factories文件,springboot在启动时会自动实例化配置在这里的类。
这里有一个EurekaServerAutoConfiguration类,eureka的自动配置都是由这个类实现的。
暂时不去看eureka是怎么去配置的,我们先考虑一个问题,在平时我们使用SpringMVC时,要先初始化DispatchServlet分发器,之后才可以为我们拦截请求进行处理,Eureka使用的Jersey框架也是这样,不过它底层是通过过滤器拦截请求,我们要让它工作起来,要将过滤器初始化。这里类比一下SpringMVC就好理解了。
所以要让eureka-server工作起来,要初始化eureka工作需要的bean以及将Jersey的filter给注入到Spring容器中。我们打开EurekaServerAutoConfiguration看一下。
这个类实现了WebMvc接口,上面有几个注解,@Import注解导入EurekaServer初始化的配置(这个配置之后会讲到,本章节暂且不去讨论),@ConditionalOnBean这个注解表示如果容器中有Marker类,才会注入标注此注解的bean,即EurekaServerAutoConfiguration才可以正常工作。到这里,@EnableEurekaServer注解才算解读完毕,我们接下来去看一下EurekaServerAutoConfiguration做的几件比较核心的事情:
- 读取配置,通过@PropertySource注解读取;
- Jersey框架实例化,即初始化filter,设置拦截路径(了解即可,不必深究);
- 初始化eureka
服务注册(责任链模式)
为了追踪服务注册的过程,以debug的方式启动eureka-server,还需要打断点,这个断点要打在哪里?类比SpringMVC,我们可以在controller层去追踪,Jersey里与之类似的是Resources,我们打开maven依赖里的eureka-core,找到ApplicationResource,这个类是用来拦截微服务的请求。
既然是服务注册,POST方法,我们去寻找相关的方法,发现addInstance()方法,在这里打断点,启动一个eureka-client(即一个微服务)
info:client注册信息
isReplication:是否来自于集群同步(来自客户端的注册信息,此项默认null,只有作集群信息同步时,才会携带此参数,这个参数要稍微注意一下,与集群同步有关)
@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
}
首先要经过一系列的参数校验,当校验通过以后,还要处理客户端可能注册错误的DataCenterInfo而丢失数据的情况(可以直接忽略,一般不会进入此方法),真正执行服务注册的是register方法,我们进入此方法。
发现进入InstanceRigistry中,这个类并没有真正做服务注册,handleRegistration()追踪进去,发布了一个服务注册的事件,之后交给父类进行注册,打开InstanceRigistry的继承树看一下:
也就是说,InstranceRegistry交给PeerAwareInstanceRegistryImpl去注册,自己只是发布了一个注册事件,继续追踪下去,
这边出现了一个新的类Lease(租债器),leaseDuration表示Eureka服务器在接收到实例的最后一次发出的心跳后,需要等待多久才可以将此实例删除,默认为90秒,可以在eureka.instance.leaseExpirationDurationInSeconds进行配置。PeerAwareInstanceRegistryImpl类也没有真正去进行服务注册,它调用了它的父类AbstractInstanceRegistry的register功能进行服务注册,等待注册完,调用自己的replicateToPeers()方法进行集群信息同步,关于集群同步源码,之后再进行解读。我们继续跟着程序走进下去,追踪注册信息。
友情提示:以下代码先大致过一遍,之后再具体分析
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try { // 注意,这里已经是父类AbstractInstanceRegistry的注册方法了
read.lock(); //读锁,可以并发
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication); // 做统计用
if (gMap == null) { //如果没有查到该微服务的集群信息,就新增一个map用来存放该微服务的集群信息,因为可能有其他相同名称的微服务也会注册进eureka
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
// putIfAbsent 如果传入key对应的value已经存在,就返回存在的value,不进行替换。如果不存在,就添加key和value,返回null
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
// 此时 gMap 是该微服务名下的Map<String,Lease>
}
// 根据传过来的注册信息ID去查找有没有对应的租债器
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
// Retain the last dirty timestamp without overwriting it, if there is already a lease
// 如果已经有租债器了,就保留最近的一个,即比较服务端存在的该实例与本次注册的实例留下最新的那个,这里的if是为了解决并发注册的冲突
if (existingLease != null && (existingLease.getHolder() != null)) {
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
// this is a > instead of a >= because if the timestamps are equal, we still take the remote transmitted
// InstanceInfo instead of the server local copy.
// 这里官方解释是如果两个时间相等,也用本次注册的实例信息,不用服务端保存的信息
if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
" than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
registrant = existingLease.getHolder();
}
} else {
// The lease does not exist and hence it is a new registration
// 判断该实例的租债器不存在,本次是新注册
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();
}
}
logger.debug("No previous lease information found; it is new registration");
}
// 新建一个本次注册实例的租债器,用来进行本次微服务实例的注册
Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
// 这里真正保存了注册信息
gMap.put(registrant.getId(), lease);
// 服务注册已经完毕,之后的内容不在本次解读范围之内
synchronized (recentRegisteredQueue) {
recentRegisteredQueue.add(new Pair<Long, String>(
System.currentTimeMillis(),
registrant.getAppName() + "(" + registrant.getId() + ")"));
}
// This is where the initial state transfer of overridden status happens
if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
+ "overrides", registrant.getOverriddenStatus(), registrant.getId());
if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
logger.info("Not found overridden id {} and hence adding it", registrant.getId());
overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
}
}
InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
if (overriddenStatusFromMap != null) {
logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
registrant.setOverriddenStatus(overriddenStatusFromMap);
}
// Set the status based on the overridden status rules
InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
registrant.setStatusWithoutDirty(overriddenInstanceStatus);
// If the lease is registered with UP status, set lease service up timestamp
if (InstanceStatus.UP.equals(registrant.getStatus())) {
lease.serviceUp();
}
registrant.setActionType(ActionType.ADDED);
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
registrant.setLastUpdatedTimestamp();
invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
logger.info("Registered instance {}/{} with status {} (replication={})",
registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
} finally {
read.unlock();
}
}
可以发现,AbstractInstanceRegistry真正执行了注册方法gMap.put(registrant.getId(), lease);
,我们来看一下eureka服务器是怎么样来保存客户端的注册信息的,先看一下携带的参数:
- InstanceInfo registrant eureka-client的注册信息
- int leaseDuration 租债器租用时间(s),表示服务端多久没有收到客户端的心跳,就将其移除
- boolean isReplication 注册信息是否来自于集群同步
服务注册这里eureka加了一把读锁,关于这里为什么加读锁,请参考eureka-server服务发现源码解读。
我们来看一下eureka是怎么保存微服务的注册信息的,它在本类维护了一个ConcurrentHashMap。
ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
最外层的String表示微服务的name,内层的Map表示该微服务(名称一致)对应的集群,这也是为什么在搭建微服务集群的时候,要把spring.application.name设置一致的原因。现在讲解一下内层的Map,String表示微服务实例的id,Lease是维护了具体实例的一个租债器,租债器的结构如下,作用就是记录该实例的各个时间(注册时间、剔除时间、服务上线时间,最后操作时间以及租约有效时间),holder就是具体维护的实例对象。
明白了这些概念,我们回过头来分析一下注册代码。
友情提示:现在可以跟着注释去分析上面的服务注册代码。
尾声:
- eureka服务注册代码其实很简单,就是在eureka-server内存里维护了一个map,相当于增,之后心跳续约相当于改,服务下架、服务剔除相当于删,服务发现相当于查,学习源码就是要学习源码的思想。
- 第一次写博客,如有不正确的地方,请批评指正。