spring eureka源码学习(下)
自我保护机制、过期、全量获取、增量获取、覆盖状态
6. 自我保护机制
当Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,Eureka Server就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该Eureka Server节点会自动退出自我保护模式。
- 什么时候使用自我保护机制?
- Eureka Server如何判断“短时间内丢失过多客户端”?
- 如何探知网络故障恢复,又如何得知在什么时候退出自我保护机制?
- 自我保护机制有什么缺陷?
要理解以上问题,首先需要知道这样两个概念:
- 期望每分钟最小续租次数:当每分钟心跳次数小于该值,会开启自我保护机制,触发自我保护,不再过期续约。
- 期望每分钟最大续租次数:每分钟心跳次数一直小于等于该值,原因后面解释。
- 续租百分比:用于确定期望每分钟最小续租次数,默认0.85
如何计算:
- 期望每分钟最大续租次数 = 2 * 当前注册的应用实例数。
每个实例会半分钟心跳一次,所以一分钟每个实例会心跳两次,那么最大续租次数肯定会小于等于所有实例一分钟内心跳的总次数。 - 期望每分钟最小续租次数 = 期望每分钟最大续租次数 * 续租百分比。
前两个问题已经回答了,其实就是在所有实例总的每分钟内心跳次数小于期望最小每分钟最小心跳次数的时候会开启自我保护机制,所以在其中肯定有一个定时任务,在每个单位时间内计算总的实例的心跳次数,看是否满足条件。
第三个问题,同理,当发现心跳次数大于期望每分钟最小续租次数的时候就退出保护机制。
第四个问题,有什么缺陷?个人认为当开启自我保护机制时,某些正常运作的服务可能会更新失效,导致使用的时候读到旧的服务信息。此时需要检查出异常到底是因为Eureka-Server出了问题还是Eureka-client出了问题,检查手段也就是判断是否按要求进入了自我保护机制,这是自我保护机制防止自己出现问题而保护大量服务不下线的一种“谦虚”手段,所以在生产环境还是关闭自我保护机制比较好。
7. 过期
由于某些微服务因为网络故障导致不可用,注册中心需要定期检查心跳时长是否超过规定阈值,如果判断某些服务已经过期则需要将其剔除。
剔除的逻辑就是先遍历所有的服务,计算上一次续租的时间与当前时间间隔判断是否过期,如果过期就加入到待剔除集合中,然后按照阈值计算出要剔除的服务的总数,再遍历所有过期的服务,按照洗牌算法公平的按批次选出剔除的服务。
8. 服务发现
服务发现分为全量获取和增量获取,两者的区别是全量获取用于在客户端注册初始化阶段获取一次完整的应用实例,而增量获取则是定时周期任务,周期性的从服务中心获取变化实例信息更新本地缓存。完整流程如下图所示。

几个问题:
- 为什么Eureka-Server挂了之后,客户端依然可以调通?
- Eureka-Client是怎样感知增量获取成功的?
- 上述方式有什么缺点?怎么解决这个问题?
1. 为什么Eureka-Server挂了之后,客户端依然可以调通?
Eureka保证了分布式基础理论CAP中的AP,实现方式是通过缓存。Eureka-Server中使用的是一种响应式缓存,为什么叫做“响应式”,缓存又是怎么设计的?
#响应式缓存
public interface ResponseCache {
//根据缓存键获取缓存
String get(Key key);
//获取缓存并压缩
byte[] getGZIP(Key key);
//过期缓存
void invalidate(String appName, @Nullable String vipAddress, @Nullable String secureVipAddress);
//已废弃,下同
AtomicLong getVersionDelta();
//已废弃,下同
AtomicLong getVersionDeltaWithRegions();
}
#缓存键
public class Key {
//键的枚举类型
public enum KeyType {
JSON, XML
}
/**
* An enum to define the entity that is stored in this cache for this key.
*/
public enum EntityType {
Application, VIP, SVIP
}
/**
* 实体名
*/
private final String entityName;
/**
* TODO[0009]:RemoteRegionRegistry
*/
private final String[] regions;
/**
* 请求参数类型
*/
private final KeyType requestType;
/**
* 请求 API 版本号
*/
private final Version requestVersion;
/**
* hashKey
*/
private final String hashKey;
/**
* 实体类型
*
* {@link EntityType}
*/
private final EntityType entityType;
/**
* {@link EurekaAccept}
*/
private final EurekaAccept eurekaAccept;
public Key(EntityType entityType, String entityName, KeyType type, Version v, EurekaAccept eurekaAccept, @Nullable String[] regions) {
this.regions = regions;
this.entityType = entityType;
this.entityName = entityName;
this.requestType = type;
this.requestVersion = v;
this.eurekaAccept = eurekaAccept;
hashKey = this.entityType + this.entityName + (null != this.regions ? Arrays.toString(this.regions) : "")
+ requestType.name() + requestVersion.name() + this.eurekaAccept.name();
}
public Key(EntityType entityType, String entityName, KeyType type, Version v, EurekaAccept eurekaAccept, @Nullable String[] regions) {
this.regions = regions;
this.entityType = entityType;
this.entityName = entityName;
this.requestType = type;
this.requestVersion = v;
this.eurekaAccept = eurekaAccept;
hashKey = this.entityType + this.entityName + (null != this.regions ? Arrays.toString(this.regions) : "")
+ requestType.name() + requestVersion.name() + this.eurekaAccept.name();
}
@Override
public int hashCode() {
String hashKey = getHashKey();
return hashKey.hashCode();
}
@Override
public boolean equals(Object other) {
if (other instanceof Key) {
return getHashKey().equals(((Key) other).getHashKey());
} else {
return false;
}
}
}
缓存键的作用就是封装一个Client的获取基本信息实体类,以便存入缓存中。
响应式缓存的设计采用了二级缓存模式,其中一级缓存是只读缓存readOnlyCache,内容由二级缓存更新,二级缓存是读写缓存readWriteCache。

当缓存出现不一致的情况时,以二级缓存为主,同时过期并更新一级缓存。二级缓存会设置过期时间,定时任务到时会主动过期二级缓存。
2. Eureka-Client是怎样感知增量获取成功的?
增量获取注册实例时会获取到最近变化应用实例集合A,获取之后与本地缓存B合并,生成客户端本次增量获取的一致性哈希码C,在Server端也会有一个一致性哈希码,这个码不是一直不变化的,在有应用实例状态信息变更的情况下会重新计算一致性哈希码。如果客户端在做过一次增量获取后和Server端的一致性哈希码一致就表示此次增量获取成功;若不相等,客户端会再发起一次全量获取。那么问题来了,一致性哈希码是如何计算出来的?
#一致性哈希码
一致性哈希码 = 状态(status)_数量(count)
例如:有2个应用A和B,各自有两个实例,B的某个实例是下线状态,那么此时的一致性哈希码就是DOWN_1_UP_3
#增量获取成功判定
Client端合并最近变化应用实例和本地缓存,计算一致性哈希码,Server端比较
3. 上述方式有什么缺点?
不能真正的保证数据一致性,如果Server端两个实例状态互换,但是Client端最近变化队列+本地缓存真实记录的是原来的状态,此时还是会认为增量获取成功,比如Server端A原来是UP/B原来是DOWN,Server端这两个实例状态发生了交换(一个上线一个下线),但是Client端获取到的还是原来的信息(有可能从缓存中获取的),那这个时候是感知不到的。这个问题其实就是Eureka保证AP而舍弃C产生的副作用。
如何解决呢?
- 修改一致性哈希码计算方法,增加应用实例ID属性。
- 设置间隔N做一次全量获取。