SpringCloud源码解析之eureka-server客户端


Eureka Server功能

提供以下功能,同来与Eureka Client的交互:服务注册、接收心跳、服务剔除、服务下线、集群同步、获取注册表中服务实例信息


一、源码类图分析

在这里插入图片描述

1.1、下面的InstanceRegistry类

继承了PeerAwareInstanceRegistryImpl实现类,使其适配Spring Cloud的使用环境,但实现的主要实现还是PeerAwareInstanceRegistryImpl实现类;

1.2、上面InstanceRegistry接口

InstanceRegistry是Server中注册表管理的核心接口,是在内存中管理注册到Server中的服务实例信息,它分别继承了LeaseManager和LookupService接口;LeaseManager的主要功能是对注册到Server中的服务实例的租约进行管理,LookupService主要功能是对服务实例进行检索;

1.3、LeaseManager
  • LeaseManager对注册到Server中的服务进行管理,分别有服务注册、服务下线、服务续约及服务剔除;
  • LeaseManager管理的是Lease对象,Lease代表一个Eureka Client服务实例信息的租约,它提供了对租约中时间属性的各项操作;
  • Lease持有的类是代表服务实例信息的instanceInfo;
  • Lease中租约默认有效时长为90s;
1.4、PeerAwareInstanceRegistry

继承了InstanceRegistry接口,主要是添加了Server集群间的同步操作,在PeerAwareInstanceRegistryImpl实现类继承了AbstractInstanceRegistry,在对本地注册表操作的基础上添加对其他peer节点同步复制,使Server集群中的注册表信息保持一致

二、服务注册

1、源码入口:ApplicationResource类中的addInstance()方法
2、主要任务:
  1. 接收由 Client 注册请求中携带的InstanceInfo信息写入到 Server 注册表中,以便其他 Client
    进行服务发现;
  2. 再将 Server 注册表中的InstanceInfo同步到其他 Server ;

Server 注册表registry的结构是ConcurrentHashMap<String, Map<String, Lease>>是一个双层Map,
外层 ConcurrentHashMap 的key为服务名称,value为一个内层Map;
内层 Map 的key为 instanceId 服务id,value为一个Lease(续约)对象;

3、源码分析:

在这里插入图片描述
这里值得注意的是,处理注册时,isReplication参数指的是当前这个注册是客户端向服务端的注册,还是服务端与服务端之间的Replicate
在这里插入图片描述
继续进入registry.register()方法中
在这里插入图片描述
在这里插入图片描述
进入父类PeerAwareInstanceRegistryImpl的register()方法

在这里插入图片描述
再进入到AbstractInstanceRegistry的register()方法中
在这里插入图片描述

Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
// 如果Server存在注册表
if (existingLease != null && (existingLease.getHolder() != null)) {
    // 获取到已经存在注册表中InstanceInfo最新修改时间
    Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
    // 获取到Client注册请求中的对InstanceInfo最新修改时间
    Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
    // LastDirtyTimestamp 如果存在的大于注册的,则说明存在的比注册的要新(时间戳越大越新)
    if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
    	// 最终都会将最新的赋值给注册请求的
        registrant = existingLease.getHolder();
    }
}
// 第一次注册时existingLease为null,这时租约是不存在的
else {
synchronized (lock) {
		//this.expectedNumberOfClientsSendingRenews:期望客户端发送心跳的数量
	   if (this.expectedNumberOfClientsSendingRenews > 0) {
	       //这里是一个自我保护机制
	       // expectedNumberOfClientsSendingRenews 默认为1 ; 这里加1的目的是为了提高系统的安全性
	       this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews + 1;
	       //下面有该方法的说明
	       updateRenewsPerMinThreshold();
	   }
  }
   logger.debug("No previous lease information found; it is new registration");
}

/**
*  serverConfig.getExpectedClientRenewalIntervalSeconds():期望客户端多长时间给服务端发送一次续约,默认是30秒
*  serverConfig.getRenewalPercentThreshold():设置开启自我保护的阈值,默认0.85
*/
protected void updateRenewsPerMinThreshold() {
// 计算公式:Client数量 * (60/续约间隔时间) * 阈值因子
// 默认设置时:2 * (60 / 30)* 0.85 = 3.4
// 这里更新的是 renewsPerMinThreshold的值:
// Renews threshold:Eureka Server 期望每分钟收到的心跳数量
// Renews(last min):Eureka Server 实际在最近一分钟内收到的心跳数量
// 如果Renews(last min) < Renews threshold,则启用自我保护
// 前面加1提升系统的安全性说明:提高了接收门槛,如果达不到要求则开启自我保护机制
     this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews
             * (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
             * serverConfig.getRenewalPercentThreshold());
 }

在这里插入图片描述
再回到父类PeerAwareInstanceRegistryImpl的register()方法
在这里插入图片描述
跟进replicateToPeers()方法中:
在这里插入图片描述

跟进replicateInstanceActionsToPeers()方法中:
在这里插入图片描述
再跟进node.register(info)中:

public void register(final InstanceInfo info) throws Exception {
	// expiryTime = 当前时间 + 过期时间(默认90秒)
    long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
    // 在expiryTime时间过期之前,要完成register任务
    batchingDispatcher.process(
            taskId("register", info),
            new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
                public EurekaHttpResponse<Void> execute() {
                	//下图进入到熟悉register环节
                    return replicationClient.register(info);
                }
            },
            expiryTime
    );
}

private static int getLeaseRenewalOf(InstanceInfo info) {
	// 如果没有配置过续约时间,默认为90秒,否则取配置的,最后变为毫秒返回
	return (info.getLeaseInfo() == null ? Lease.DEFAULT_DURATION_IN_SECS : info.getLeaseInfo().getRenewalIntervalInSecs()) * 1000;
}

向其他的peer注册服务实例信息
在这里插入图片描述

二、接收服务心跳

在 Client 完成服务注册之后,它需要定时向Server发送心跳请求(默认30s一次),以维持在Server中租约有效性;核心逻辑在 AbstractInstanceRegistry 的 renew() 方法中;

1、源码入口:InstanceResource 的 renewLease() 方法
2、主要任务:
  1. Client 向 Server 发送心跳,Server 接收到心跳后,修改注册表中的状态;
  2. 将心跳同步到其他服务端;
3、源码分析:

在这里插入图片描述
由于registry是PeerAwareInstanceRegistry,由最上面的类图可知它实现了InstanceRegistry接口,而它对应的实现方法在InstanceRegistry中,先进入InstanceRegistry.renew()方法中:
在这里插入图片描述
这个方法前面是发布事件,跟进最后的super.renew(appName, serverId, isReplication)方法中
在这里插入图片描述

public boolean renew(final String appName, final String id, final boolean isReplication) {
    if (super.renew(appName, id, isReplication)) {
    	// 同步到其他server
        replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
        return true;
    }
    return false;
}

再次跟进super.renew(appName, id, isReplication)进入到核心逻辑 AbstractInstanceRegistry 的 renew() 方法中:

public boolean renew(String appName, String id, boolean isReplication) {
	// 计数
    RENEW.increment(isReplication);
    // 根据appName获取服务集群的租约Lease集合
    Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
    Lease<InstanceInfo> leaseToRenew = null;
    // map不为空时,根据id获取对应的主机(Lease续约对象)
    if (gMap != null) {
        leaseToRenew = gMap.get(id);
    }
    // 租约不存在,直接返回false
    if (leaseToRenew == null) {
        RENEW_NOT_FOUND.increment(isReplication);
        return false;
    } else {
        // 否则说明在 Server 的注册表有当前续约对象
        InstanceInfo instanceInfo = leaseToRenew.getHolder();
        if (instanceInfo != null) {
            // 根据覆盖状态规则得到服务实例的最终覆盖状态
            InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
                    instanceInfo, leaseToRenew, isReplication);
            // 若状态是UNKNOWN,表示心跳失败,取消续约,返回false
            if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
                RENEW_NOT_FOUND.increment(isReplication);
                return false;
            }
            // 如果说该instanceinfo的实例状态与其最新的可覆盖状态不同
            if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
            	// 将instanceinfo的实例状态 设置成最新的可覆盖状态
                instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
            }
        }
        // 统计每分钟续租的次数 用于自我保护
        renewsLastMin.increment();
        // 更新租约中的有效时间
        leaseToRenew.renew();
        return true;
    }
}
	// 更新租约中的有效时间
    public void renew() {
        lastUpdateTimestamp = System.currentTimeMillis() + duration;
    }

继续跟同步到其他 Server 的代码:
在这里插入图片描述
跟进replicateToPeers()方法中:
在这里插入图片描述
跟进replicateInstanceActionsToPeers()方法中:
在这里插入图片描述
跟进node.heartbeat()方法中:
在这里插入图片描述
关注sendHeartBeat()方法,跟进:
在这里插入图片描述
String urlPath = “apps/” + appName + ‘/’ + id;
这里的心跳发送的是put请求,在请求发送时会将服务名称以及instanceId发送给集群中的 Server 端;
Server端会根据发过来的instanceId来查找自己的服务端列表,又回到了入口的地方;
如果没有找到说明心跳失败,会返回NOT_FOUND信息。
在这里插入图片描述

三、服务下线

在 Client 应用销毁时,会向 Server 发送服务下线请求,Server 在注册表中清除该应用的相关信息。

1、源码入口:InstanceResource 的 cancelLease() 方法
2、主要任务:
  1. Client 向 Server 发送下架请求,Server 接收到请求后,将其从注册表中删除;
  2. 同步到其他服务端;
3、源码分析:

在这里插入图片描述
跟进cancel()方法中:
在这里插入图片描述
继续跟进super.cancel(appName, serverId, isReplication)中:
在这里插入图片描述
1、先跟进super.cancel(appName, id, isReplication);
在这里插入图片描述

@Override
public boolean cancel(String appName, String id, boolean isReplication) {
    return internalCancel(appName, id, isReplication);
}
protected boolean internalCancel(String appName, String id, boolean isReplication) {
   try {
   	   // 获取读锁,防止被其他线程进行修改
       read.lock();
       CANCEL.increment(isReplication);
       // 根据appName获取服务实例的集群
       Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
       Lease<InstanceInfo> leaseToCancel = null;
       // 移除服务实例的租约
       if (gMap != null) {
        	// map不为null,移除key为instanceId的value(Lease对象)
           leaseToCancel = gMap.remove(id);
       }
       // 将服务实例信息添加到最近下线服务实例统计队列
       recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
       // 将此instanceId对应的信息从状态map中移除
       InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
       
       // 租约不存在,返回false
       if (leaseToCancel == null) {
           CANCEL_NOT_FOUND.increment(isReplication);
           return false;
       } else {
       	   // 否则设置租约的下线时间
           leaseToCancel.cancel();
           // 获取到下架的InstanceInfo
           InstanceInfo instanceInfo = leaseToCancel.getHolder();
           String vip = null;
           String svip = null;
           if (instanceInfo != null) {
           	   // 添加最近租约变更记录队列,标识ActionType DELETED ,用于Eureka Client 增量式获取注册表信息
               instanceInfo.setActionType(ActionType.DELETED);
               recentlyChangedQueue.add(new RecentlyChangedItem(leaseToCancel));
               instanceInfo.setLastUpdatedTimestamp();
               vip = instanceInfo.getVIPAddress();
               svip = instanceInfo.getSecureVipAddress();
           }
           // 设置缓存过期
           invalidateCache(appName, vip, svip);   
       }
   } finally {
   	   // 释放读锁
       read.unlock();
   }

	// 下线一个服务 则延长 期望客户端给服务端发送一次续约的时间
   synchronized (lock) {
       if (this.expectedNumberOfClientsSendingRenews > 0) {
           // Since the client wants to cancel it, reduce the number of clients to send renews.
           this.expectedNumberOfClientsSendingRenews = this.expectedNumberOfClientsSendingRenews - 1;
           updateRenewsPerMinThreshold();
       }
   }
   // 下线成功
   return true;
}

2、再跟进replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);

在这里插入图片描述
同步给Server集群下线该服务;
在这里插入图片描述

四、服务剔除

在 Client 完成服务注册之后,既没有续约,也没有下线(服务崩溃或者网络异常),这时服务的状态就处于一个不可知的状态,不能保证可以连通该服务实例,所以需要进行该服务的剔除。

1、核心源码:AbstractInstanceRegistry 的 evict() 方法
2、源码分析:
2.1、寻找入口:

在这里插入图片描述
在这里插入图片描述
进入EurekaServerAutoConfiguration
在这里插入图片描述
进入EurekaServerInitializerConfiguration.class,跟进eurekaServerBootstrap.contextInitialized()方法:
在这里插入图片描述
跟进eurekaServerBootstrap.contextInitialized()方法:
在这里插入图片描述
跟进initEurekaServerContext()方法:
在这里插入图片描述
再跟进this.registry.openForTraffic(this.applicationInfoManager, registryCount)方法中:
在这里插入图片描述
defaultOpenForTrafficCount默认值为1,用于确定何时取消租约的值,对于独立租约,默认值为1,对于对等复制的eurekas,应设置为0
在这里插入图片描述
跟进openForTraffic()方法中:
在这里插入图片描述
跟进super.postInit()方法中:

 protected void postInit() {
     renewsLastMin.start();
     // 注意这里有一个evictionTaskRef,清除任务的引用,通过get()方法来获取一个清除任务
     if (evictionTaskRef.get() != null) {
     	 // 如果有清除任务则执行cancel()方法,为了在新建定时清除任务之前,先取消掉其他未运行的
         evictionTaskRef.get().cancel();
     }
     // 新建定时清除任务,放入到evictionTaskRef中
     evictionTaskRef.set(new EvictionTask());
     // 开始定时任务 serverConfig.getEvictionIntervalTimerInMs()在配置文件中默认为60s,服务多少秒未心跳被剔除的时间
     evictionTimer.schedule(evictionTaskRef.get(),
             serverConfig.getEvictionIntervalTimerInMs(),
             serverConfig.getEvictionIntervalTimerInMs());
 }
 
 // 取消此定时任务。如果任务是一次性安排的,但尚未运行或尚未计划,它将不会执行。如果任务已安排重复执行,则也不会执行了。
public boolean cancel() {
    synchronized(lock) {
        boolean result = (state == SCHEDULED);
        // 变更状态为CANCELED
        state = CANCELLED;
        return result;
    }
}

2.2、进入定时任务:

为了关注清除任务到底是怎样的一个任务,跟进new EvictionTask()中:
在这里插入图片描述
计算的是补偿时间:long compensationTimeMs = getCompensationTimeMs();
什么是补偿时间,进入getCompensationTimeMs()方法中:

 /** 计算补偿时间,该补偿时间定义为自上次迭代以来此任务实际执行的时间,与配置的执行时间之比。 
   *   当时间变化(例如由于时钟偏斜或gc)导致实际驱逐任务的执行时间晚于所需时间(根据配置的周期)时,此功能很有用。
   */
long getCompensationTimeMs() {
	//获取当前时间
    long currNanos = getCurrentTimeNano();
    //获取上一次清除开始的时间,并赋值这一次的开始时间 第一次执行获取是0
    long lastNanos = lastExecutionNanosRef.getAndSet(currNanos);
    // 如果lastNanos 是0,代表的是第一次开始,则不需要补偿
    if (lastNanos == 0l) {
         return 0l;
    }
     // 本次开始执行的时间 距离 上一次开始执行的时间: 也就是上一次清除实际用时多少时间
     // 特别注意:elapsedMs 是上一次实际清除用的时间(因为这一次还没有开始)
     long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos);
     // 减去配置的定时时间间隔,如果大于0,说明上一次执行任务的耗时超过了定时时间间隔
     // 导致此次执行任务的时间晚于应该执行的时间,所以要加上这个补偿时间
     // 补偿时间 = 实际清除时间 - 清除间隔
     long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs();
     // 如果计算后的补偿时间是负数(小于0),则说明不需要补偿,否则需要补偿
     return compensationTime <= 0l ? 0l : compensationTime;
}
2.3、核心代码evict()方法:

终于进入到了核心代码evict()方法中:
在这里插入图片描述
分块分析

// 自我保护相关,如果出现该状态,不允许剔除服务
if (!isLeaseExpirationEnabled()) {
    return;
}

进入isLeaseExpirationEnabled()方法中:

@Override
public boolean isLeaseExpirationEnabled() {
    if (!isSelfPreservationModeEnabled()) {
        // The self preservation mode is disabled, hence allowing the instances to expire.
        return true;
    }
    // numberOfRenewsPerMinThreshold:计算出来的自我保护机制生效应该接收的心跳数 一般情况都大于0
    // getNumOfRenewsInLastMin():最近一分钟实际接收到的心跳数
    // 如果getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold 说明自我保护机制不生效 允许剔除服务
    // 如果getNumOfRenewsInLastMin() < numberOfRenewsPerMinThreshold 说明自我保护机制生效 不允许剔除服务
    return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}

再往下分析

// 首先创建一个集合用于存放所有过期的Lease对象
List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
// 遍历注册表register 一次性获取所有的过期租约
for (Entry<String, Map<String, Lease<InstanceInfo>>> groupEntry : registry.entrySet()) {
    Map<String, Lease<InstanceInfo>> leaseMap = groupEntry.getValue();
    if (leaseMap != null) {
        for (Entry<String, Lease<InstanceInfo>> leaseEntry : leaseMap.entrySet()) {
            Lease<InstanceInfo> lease = leaseEntry.getValue();
            // 判断当前循环的Lease是否过期
            if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
            	// 如果过期就放入过期集合
                expiredLeases.add(lease);
            }
        }
    }
}

进入isExpired()方法中看一下:

public boolean isExpired(long additionalLeaseMs) {
  // evictionTimestamp > 0 
  // 说明 client 可以被清除了,并且已经记录到了 evictionTimestamp 这个变量中
   // 或者: 当前时间戳 > (最新更新时间 + 清除间隔 + 补偿时间)
    return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
}

再往下分析

// 计算最大允许剔除的租约的数量,获取注册表租约总数 即客户端总数
int registrySize = (int) getLocalRegistrySize();
// 计算注册表租约的阀值 与自我保护相关
// serverConfig.getRenewalPercentThreshold()为清除阈值 默认为0.85
// registrySizeThreshold = 客户端总数 * 0.85
int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
// 获取到清除极限数量 = 所有客户端数量 - 阈值数量  例如:客户端数为100,这里就是  100-85,本次清除的极限为15
int evictionLimit = registrySize - registrySizeThreshold;

// 由于清除时尽量不要让自我保护生效,所以清除的数量应该是总共失联客户端数量与清除极限之间取最小值
// 例如:100个客户端,阈值是0.85,极限清除为15个
// 如果expiredLeases集合(过期的)中有10个,则最终清理掉10个就可以了
// 如果expiredLeases集合(过期的)中有20个,则最终清理掉15个就可以了
// 但是不能清理掉20个,如果清理的超过了阈值极限自我保护将会生效,而自我保护生效就不能进行清除任务
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
// 如果清除数量大于0
if (toEvict > 0) {
    // 逐个随机剔除,取随机数
    Random random = new Random(System.currentTimeMillis());
    for (int i = 0; i < toEvict; i++) {
        // Pick a random item (Knuth shuffle algorithm) 洗牌算法 开始
        int next = i + random.nextInt(expiredLeases.size() - i);
        // 将集合中下标为i和next元素进行交换
        Collections.swap(expiredLeases, i, next);
        Lease<InstanceInfo> lease = expiredLeases.get(i);
        // Pick a random item (Knuth shuffle algorithm) 洗牌算法 结束

        String appName = lease.getHolder().getAppName();
        String id = lease.getHolder().getId();
        // 逐个剔除
        EXPIRED.increment();
        internalCancel(appName, id, false);
    }
}

跟进internalCancel()方法,就是从注册表(map)中删除掉,并且更改一些状态,在上面的服务下线中有说明
在这里插入图片描述


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值