Eureka Server 源码解析
1. 自动配置分析
1.1 入口
入口从starter开始:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
看到pom中引入了如下依赖:
1.2 @EnableEurekaServer
我们看到这个自动配置类要起效,要求容器中要有EurekaServerMarkerConfiguration.Marker的实例,那么这个Marker在哪创建的呢?
我们知道要启动EurekaServer,要在启动类上加@EnableEurekaServer注解,看下这个注解:
@EnableEurekaServer // 开启Eureka服务
@SpringBootApplication
public class ApplicationEuerkaServer8000 {
public static void main(String[] args) {
SpringApplication.run(ApplicationEuerkaServer8000.class, args);
}
}
//我们看这个注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EurekaServerMarkerConfiguration.class)//看到导入了一个配置类
public @interface EnableEurekaServer {
}
//看这个配置类
@Configuration(proxyBeanMethods = false)
public class EurekaServerMarkerConfiguration {
@Bean
public Marker eurekaServerMarkerBean() {
//看到就是在这里创建了一个Marker实例
return new Marker();
}
class Marker {
}
}
通过上面分析我们知道通过@EnableEurekaServer注解,会创建Marker实例,从而让EurekaServerAutoConfiguration这个自动配置类生效。
1.3 EurekaServerAutoConfiguration
现在我们看EurekaServerAutoConfiguration这个自动配置类都创建了哪些组件,先简单看一下:
InstanceRegistry
PeerEurekaNodes
2. 处理客户端状态修改请求
现在我们分析第一个请求,先找到处理器,我们知道SpringCloud用的是jersey框架,与SpringMVC框架不同的是SpringMVC用的是Controller作为处理器,而jersey用的是Resource,所以我们找到InstanceResource:
至于服务端接收到http请求后,通过URI如何找到对应的处理器,交给处理器对应的方法处理,这部分的源码没有看,感觉比较复杂,但核心思路应该和SpringMVC差不多,当然这个是jersey框架的东西不是本篇重点
处理状态修改请求,找到path为status的处理方法:
//InstanceResource.java
@PUT //看到是PUT请求
@Path("status")
public Response statusUpdate(
@QueryParam("value") String newStatus,
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
@QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
//入参:
//newStatus:需要修改的状态,
//isReplication:是否是复制请求
// (true则代表此次请求是Server端之间进行复制同步的请求,后面会看到)
//lastDirtyTimestamp:客户端最新的修改时间
try {
//首先根据微服务名称和instanceId看下注册表中有没有这个实例(InstanceInfo)
if (registry.getInstanceByAppAndId(app.getName(), id) == null) {
logger.warn("Instance not found: {}/{}", app.getName(), id);
//如果没有返回NOT_FOUND,404
return Response.status(Status.NOT_FOUND).build();
}
//有的话进行状态更新
boolean isSuccess = registry.statusUpdate(app.getName(), id,
InstanceStatus.valueOf(newStatus), lastDirtyTimestamp,
"true".equals(isReplication));
if (isSuccess) {
logger.info("Status updated: {} - {} - {}", app.getName(), id, newStatus);
//修改成功返回ok,200
return Response.ok().build();
} else {
logger.warn("Unable to update status: {} - {} - {}", app.getName(), id, newStatus);
//失败返回error,500
return Response.serverError().build();
}
} catch (Throwable e) {
logger.error("Error updating instance {} for status {}", id,
newStatus);
//整个执行过程中发生异常了,也返回500
return Response.serverError().build();
}
}
2.1 根据微服务名称和instanceId从注册表中获取实例
先看registry.getInstanceByAppAndId方法,根据微服务名称和instanceId查询注册表中的实例:
//AbstractInstanceRegistry.java
public InstanceInfo getInstanceByAppAndId(String appName, String id) {
return this.getInstanceByAppAndId(appName, id, true);
}
//AbstractInstanceRegistry.java
public InstanceInfo getInstanceByAppAndId(String appName, String id, boolean includeRemoteRegions) {
//registry就是我们服务端本地的注册表,双层map
//外层map,key是微服务名称,value是内层map
//内层map,key是InstanceInfo的Id,value是Lease续约对象,包装了InstanceInfo
//先根据微服务名获取内层map
Map<String, Lease<InstanceInfo>> leaseMap = registry.get(appName);
Lease<InstanceInfo> lease = null;
if (leaseMap != null) {
//再根据instanceId获取续约对象
lease = leaseMap.get(id);
}
if (lease != null
&& (!isLeaseExpirationEnabled() || !lease.isExpired())) {
//lease不空,并且:
//isLeaseExpirationEnabled:续约过期是否开启,底层其实是判断是否开启了
// 自我保护机制,如果开启了自我保护机制,这里就会返回false,关闭续约过期
// 则!isLeaseExpirationEnabled()值就是true,不会进行第二个条件判断了
// 也就意味着只要注册表中有这个实例信息,就直接返回,无论它是否过期
//相反如果没有开启自我保护机制,则会开启续约过期,就需要检查当前实例
//的续约信息有没有过期,只会返回没有过期的实例信息
//把lease对象包装成InstanceInfo返回
return decorateInstanceInfo(lease);
} else if (includeRemoteRegions) {
//上面只要没有找到,如果includeRemoteRegions为true
//则会尝试去远程region的注册表中查找
//从远程Region中获取有没有该实例的InstanceInfo
for (RemoteRegionRegistry remoteRegistry : this.regionNameVSRemoteRegistry.values()) {
//regionNameVSRemoteRegistry也是一个map
//key是regionName,value是RemoteRegionRegistry,远程注册表
//根据微服务名称获取Application
Application application = remoteRegistry.getApplication(appName);
if (application != null) {
//根据instanceId获取对应的InstanceInfo
return application.getByInstanceId(id);
}
}
}
return null;
}
服务端的注册表:
续约对象Lease:
类描述:描述{@link T}的基于时间的可用性
。 目的是避免由于非正常关闭而导致实例在{@link AbstractInstanceRegistry}中累积,这在AWS环境中并不罕见。
2.1.1 自我保护机制
先看一下isLeaseExpirationEnabled方法,是否开启了续约过期,其实就是判断自我保护模式是否开启了,如果启动了自我保护模式会忽略实例过期
:
//PeerAwareInstanceRegistryImpl.java
public boolean isLeaseExpirationEnabled() {
if (!isSelfPreservationModeEnabled()) {
// The self preservation mode is disabled, hence allowing the instances to expire.
// 自我保护模式被禁用,因此允许实例过期。
return true;
}
// 自我保护模式开启了,就走这
// numberOfRenewsPerMinThreshold:预期每分钟收到的续约心跳数的阈值
// getNumOfRenewsInLastMin():当前最后一分钟收到的续约心跳数
// 如果getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold
// 没有低于阈值,则不会开启自我保护机制
return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
//PeerAwareInstanceRegistryImpl.java
public boolean isSelfPreservationModeEnabled() {
//检查自我保护模式是否开启。配置文件中的一个配置。
return serverConfig.shouldEnableSelfPreservation();
}
关于 预期每分钟收到的续约心跳数 的计算,以后会看到。
2.1.2 把lease对象包装成InstanceInfo返回
继续看如果符合条件获取到了Lease续约对象,把lease对象包装成InstanceInfo返回,decorateInstanceInfo方法:
//AbstractInstanceRegistry.java
private InstanceInfo decorateInstanceInfo(Lease<InstanceInfo> lease) {
//获取持有者InstanceInfo
InstanceInfo info = lease.getHolder();
// client app settings
// 心跳续约的两个参数默认值
int renewalInterval = LeaseInfo.DEFAULT_LEASE_RENEWAL_INTERVAL;
int leaseDuration = LeaseInfo.DEFAULT_LEASE_DURATION;
// TODO: clean this up
if (info.getLeaseInfo() != null) {
//如果instanceInfo专门设置了,用专门设置的
renewalInterval = info.getLeaseInfo().getRenewalIntervalInSecs();
leaseDuration = info.getLeaseInfo().getDurationInSecs();
}
info.setLeaseInfo(LeaseInfo.Builder.newBuilder()
//注册时间戳
.setRegistrationTimestamp(lease.getRegistrationTimestamp())
//最后一次收到续约心跳的时间(实际上是最后一次收到续约心跳的时间+续约过期范围时间)
.setRenewalTimestamp(lease.getLastRenewalTimestamp())
//服务启动的时间戳
.setServiceUpTimestamp(lease.getServiceUpTimestamp())
//客户端指定的续约心跳的间隔时间
.setRenewalIntervalInSecs(renewalInterval)
//续约过期的范围时间,超过该时间还没收到心跳则认为过期了
.setDurationInSecs(leaseDuration)
//注销注册的时间戳
.setEvictionTimestamp(lease.getEvictionTimestamp()).build());
info.setIsCoordinatingDiscoveryServer();
return info;
}
2.2 状态修改
//PeerAwareInstanceRegistryImpl.java
public boolean statusUpdate(final String appName, final String id,
final InstanceStatus newStatus, String lastDirtyTimestamp,
final boolean isReplication) {
if (super.statusUpdate(appName, id, newStatus, lastDirtyTimestamp, isReplication)) {
//先调用父类方法,修改当前本地注册表中对应实例的状态,修改成功后
//调用下面方法,将这个状态修改的操作同步给集群中的其他Server,完成数据同步
replicateToPeers(Action.StatusUpdate, appName, id, null, newStatus, isReplication);
return true;
}
return false;
}
2.2.1 本地状态修改
先看本地状态更新:
//AbstractInstanceRegistry.java
public boolean statusUpdate(String appName, String id,
InstanceStatus newStatus, String lastDirtyTimestamp,
boolean isReplication) {
try {
read.lock();//更新操作为什么上读锁?
//后面服务端很多流程分析,都会看到修改操作,加上了读锁
//先简单提一下,流程全部分析完,最后还会单独说一下
//读锁对应有一个写锁,读锁之间不互斥,写锁之间、读写锁之间是互斥的。
//如果一个线程持有读锁,其他线程再去获取读锁都是可以的立即获取到的
// 但是如果获取写锁就会阻塞了
//相反,如果一个线程持有写锁,其他线程无论想获取读锁还是写锁都会阻塞。
//所以这里修改操作用读锁,而读取操作却用写锁
//首要原因是因为读的操作只有一个地方,而写的操作有很多地方
//其次是希望写的操作之间不会互斥,可以同时进行,而读的操作进行的时
// 候是不允许其他线程写的,保证读的时候数据的稳定性。
//写的操作可以多个线程同时进行,所以它本身肯定需要保证线程安全
//状态更新计数器+1
STATUS_UPDATE.increment(isReplication);
//根据微服务名称,从注册表中获取内层map
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> lease = null;
if (gMap != null) {
//根据instanceId获取续约对象
lease = gMap.get(id);
}
if (lease == null) {
return false;
} else {
//刷新续约时间
//既然服务端已经收到了客户的请求,本身也相当于一次续约心跳
lease.renew();
//获取持有者 instanceInfo
InstanceInfo info = lease.getHolder();
// Lease is always created with its instance info object.
// This log statement is provided as a safeguard, in case this invariant is violated.
if (info == null) {
logger.error("Found Lease without a holder for instance id {}", id);
}
if ((info != null) && !(info.getStatus().equals(newStatus))) {
// 状态不一样才进行更新
// Mark service as UP if needed
if (InstanceStatus.UP.equals(newStatus)) {
// 状态是UP,如果是第一次启动,则记录一下启动的时间戳
lease.serviceUp();
}
// This is NAC overriden status
// 保存到一个维护覆盖状态的map,key是instanceId
overriddenInstanceStatusMap.put(id, newStatus);
// Set it for transfer of overridden status to replica on
// replica start up 设置它以便在副本启动时将覆盖状态转移到副本
// 为覆盖状态赋值
info.setOverriddenStatus(newStatus);
//复制时间戳
long replicaDirtyTimestamp = 0;
//直接修改status状态,而不标记dirty
info.setStatusWithoutDirty(newStatus);
//如果客户端传过来的lastDirtyTimestamp不为空(客户端的最新修改时间)
if (lastDirtyTimestamp != null) {
//赋值给replicaDirtyTimestamp
replicaDirtyTimestamp = Long.valueOf(lastDirtyTimestamp);
}
// If the replication's dirty timestamp is more than the existing one, just update
// it to the replica's.
// 比较一下客户端传过来的最新修改时间是不是比我服务端记录的还新
// 保存更新的
if (replicaDirtyTimestamp > info.getLastDirtyTimestamp()) {
info.setLastDirtyTimestamp(replicaDirtyTimestamp);
}
//设置操作类型为修改(增量更新时看到过)
info.setActionType(ActionType.MODIFIED);
//将其加入到最近更新队列!!!这是一个线程安全的先进先出的队列
recentlyChangedQueue.add(new RecentlyChangedItem(lease));
//更新服务端的最新修改时间
//lastUpdatedTimestamp时间戳,这个时间戳是服务端专门用的
info.setLastUpdatedTimestamp();
//让一些缓存失效,不看了
invalidateCache(appName, info.getVIPAddress(), info.getSecureVipAddress());
}
return true;
}
} finally {
//最后锁释放
read.unlock();
}
}
- 状态更新计数器+1
//EurekaMonitors.java
/**
* Increment the counter for the given statistic based on whether this is
* because of replication from other eureka servers or it is a eureka client
* initiated action.
*
* 根据这是由于从其他eureka服务器进行复制还是由于eureka客户端
* 启动的操作而增加给定统计信息的计数器。
*/
public void increment(boolean isReplication) {
//服务端修改整体计数器
counter.incrementAndGet();
if (!isReplication) {
//isReplication判断是客户端提交的修改请求,还是Server端之间同步的更新请求
//不是复制的情况,专门的计数器在+1
myZoneCounter.incrementAndGet();
}
}
- 刷新续约时间
//Lease.java
/**
* Renew the lease, use renewal duration if it was specified by the
* associated {@link T} during registration, otherwise default duration is
* {@link #DEFAULT_DURATION_IN_SECS}.
* 续订租约,如果在注册过程中由关联的{@link T}指定了续约期限,则使用续约期限,
* 否则默认续约期限为{@link #DEFAULT_DURATION_IN_SECS}。
*/
public void renew() {
//此时间是下一次收到续约心跳的截止时间,如果超过这个时间没有收到心跳,则认为挂了
lastUpdateTimestamp = System.currentTimeMillis() + duration;
}
- 状态是UP,如果是刚启动,则记录一下启动的时间戳
//Lease.java
/**
* Mark the service as up. This will only take affect the first time called,
* subsequent calls will be ignored.
*
* 将服务标记为已启动。 这只会影响第一次调用,以后的调用将被忽略。
*/
public void serviceUp() {
if (serviceUpTimestamp == 0) {
serviceUpTimestamp = System.currentTimeMillis();
}
}
- 设置覆盖状态
//InstanceInfo.java
/**
* Sets the overridden status for this instance.Normally set by an external
* process to disable instance from taking traffic.
*
* 设置此实例的覆盖状态。通常由外部进程设置,以禁止实例获取流量。
*/
public synchronized void setOverriddenStatus(InstanceStatus status) {
if (this.overriddenStatus != status) {
this.overriddenStatus = status;
}
}
2.2.2 状态更新操作同步给集群中其他Server
本地状态更新成功后,复制同步给集群中的其他Server:
//PeerAwareInstanceRegistryImpl.java
public boolean statusUpdate(final String appName, final String id,
final InstanceStatus newStatus, String lastDirtyTimestamp,
final boolean isReplication) {
if (super.statusUpdate(appName, id, newStatus, lastDirtyTimestamp, isReplication)) {
//本地更新成功后,复制同步给集群中的其他Server
//记住这个Action
replicateToPeers(Action.StatusUpdate, appName, id, null, newStatus, isReplication);
return true;
}
return false;
}
//PeerAwareInstanceRegistryImpl.java
/**
* Replicates all eureka actions to peer eureka nodes except for replication
* traffic to this node.
* 将所有eureka操作复制到对等eureka节点,但复制到该节点的流量除外。
*/
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) {//是否是Server之间的复制请求,此时还是客户端发起的请求,所以是false
numberOfReplicationsLastMin.increment();
}
// If it is a replication already, do not replicate again as this will create a poison replication
// 如果已经是复制,则不要再次复制,因为这将创建有毒复制
// peerEurekaNodes代表的就是当前Eureka Server的集群
if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
return;
}
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
//遍历集群中所有的节点
// If the url represents this host, do not replicate to yourself.
// 如果URL代表此主机,请不要复制到您自己。
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
//将对eureka的操作行为复制给其他eureka节点
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
} finally {
tracer.stop();
}
}
将对eureka的操作行为复制给其他eureka节点:
//PeerAwareInstanceRegistryImpl.java
/**
* Replicates all instance changes to peer eureka nodes except for
* replication traffic to this node.
* 将所有实例更改复制到对等eureka节点,但复制到该节点的流量除外。
*/
private void replicateInstanceActionsToPeers(Action action, String appName,
String id, InstanceInfo info, InstanceStatus newStatus,
PeerEurekaNode node) {
//此时action = Action.StatusUpdate
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:
//状态更新
//从注册表获取instanceInfo信息,之前跟过
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);
}
}
可以看到此方法维护了所有Eureka Server之间复制同步的各种类型操作。
节点同步状态更新操作 :
/**
* Send the status update of the instance.
*/
public void statusUpdate(final String appName, final String id,
final InstanceStatus newStatus, final InstanceInfo info) {
long expiryTime = System.currentTimeMillis() + maxProcessingDelayMs;
//batchingDispatcher执行器,底层是将任务放入队列,有专门的后台线程循环从队列取任务执行
batchingDispatcher.process(
//参数一:任务ID
taskId("statusUpdate", appName, id),
//参数二:真正要处理的任务
new InstanceReplicationTask(targetHost, Action.StatusUpdate, info, null, false) {
@Override
public EurekaHttpResponse<Void> execute() {
//直接看statusUpdate方法
return replicationClient.statusUpdate(appName, id, newStatus, info);
}
},
//参数三:到期时间
expiryTime
);
}
直接看statusUpdate方法:
//AbstractJerseyEurekaHttpClient.java
public EurekaHttpResponse<Void> statusUpdate(String appName, String id, InstanceStatus newStatus, InstanceInfo info) {
String urlPath = "apps/" + appName + '/' + id + "/status";
ClientResponse response = null;
try {
Builder requestBuilder = jerseyClient.resource(serviceUrl)
.path(urlPath)
.queryParam("value", newStatus.name())
.queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString())
.getRequestBuilder();
//注意这里和以前的客户端发起请求不一样,当前类是JerseyReplicationClient
//在实现的addExtraHeaders方法中,向请求头中添加了复制请求的标记
addExtraHeaders(requestBuilder);
//看到又是通过http方式提交put请求
response = requestBuilder.put(ClientResponse.class);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP PUT {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
//JerseyReplicationClient.java
protected void addExtraHeaders(Builder webResource) {
//看到向请求头添加了HEADER_REPLICATION,代表了是Server间的复制请求
webResource.header(PeerEurekaNode.HEADER_REPLICATION, "true");
}