启动
在 SpringBoot 中,一般需要加上@EnableXXX
才能启用某种功能的,那么这个注解就是源码分析的入口。那么看看@EnableEurekaServer
里面是什么。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EurekaServerMarkerConfiguration.class)
public @interface EnableEurekaServer {
}
这个注解很简单除了元注解意外就是一个@Import
注解了。打开EurekaServerMarkerConfiguration
看看。
@Configuration(proxyBeanMethods = false)
public class EurekaServerMarkerConfiguration {
@Bean
public Marker eurekaServerMarkerBean() {
return new Marker();
}
class Marker {
}
}
这个注解的作用是向 Spring 注入一个名字叫eurekaServerMarkerBean
的Marker
Bean 实例,但是这个Marker
类里面什么都没有。从类名上看,这是个标记类。
查看项目依赖,打开spring-cloud-netflix-eureka-server-2.2.1.RELEASE.jar
,可以找到一个META-INF/spring.factories
的文件。这里用到了SPI的机制。文件里面是这样的:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration
打开EurekaServerAutoConfiguration
这个类。
@Configuration(proxyBeanMethods = false)
@Import(EurekaServerInitializerConfiguration.class)
@ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)
@EnableConfigurationProperties({ EurekaDashboardProperties.class,
InstanceRegistryProperties.class })
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerAutoConfiguration implements WebMvcConfigurer {
// …………省略部分代码
}
在这里发现了刚才的Marker
类,并且有个@ConditionalOnBean
,说明只有当 Spring 发现容器中存在Marker
的 bean 的时候,这个配置类才生效。
在这里又看到了一个@import
注解,进入到EurekaServerInitializerConfiguration
类,这个类的作用是初始化 Eureka服务器的配置。
@Configuration(proxyBeanMethods = false)
public class EurekaServerInitializerConfiguration
implements ServletContextAware, SmartLifecycle, Ordered {
@Override
public void start() {
new Thread(() -> {
try {
// 初始化工作
eurekaServerBootstrap.contextInitialized(
EurekaServerInitializerConfiguration.this.servletContext);
log.info("Started Eureka Server");
publish(new EurekaRegistryAvailableEvent(getEurekaServerConfig()));
EurekaServerInitializerConfiguration.this.running = true;
publish(new EurekaServerStartedEvent(getEurekaServerConfig()));
}
catch (Exception ex) {
// Help!
log.error("Could not initialize Eureka servlet context", ex);
}
}).start();
}
}
这里开启了一个新的线程去开启 Eureka 服务器。在这里可以看到之前提到的两个事件。初始化的工作在eurekaServerBootstrap.contextInitialized(EurekaServerInitializerConfiguration.this.servletContext);
这里面完成的。
public void contextInitialized(ServletContext context) {
try {
// 初始化 Eureka 环境参数
initEurekaEnvironment();
// 初始化 Eureka 服务上下文
initEurekaServerContext();
context.setAttribute(EurekaServerContext.class.getName(), this.serverContext);
}
catch (Throwable e) {
log.error("Cannot bootstrap eureka server :", e);
throw new RuntimeException("Cannot bootstrap eureka server :", e);
}
}
protected void initEurekaServerContext() throws Exception {
// …………省略部分代码
// 从其它 Eureka 节点同步客户端数据,返回同步了多少个节点回来
int registryCount = this.registry.syncUp();
this.registry.openForTraffic(this.applicationInfoManager, registryCount);
EurekaMonitors.registerAllStats();
}
public int syncUp() {
// 从其它 Eureka 服务器获取所有数据
int count = 0;
// 有个重试次数 serverConfig.getRegistrySyncRetries()
for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); i++) {
if (i > 0) {
try {
// 重试等待时间
Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
} catch (InterruptedException e) {
logger.warn("Interrupted during registry transfer..");
break;
}
}
// 这里就是 copy 数据的过程了
Applications apps = eurekaClient.getApplications();
for (Application app : apps.getRegisteredApplications()) {
for (InstanceInfo instance : app.getInstances()) {
try {
if (isRegisterable(instance)) {
register(instance, instance.getLeaseInfo().getDurationInSecs(), true);
count++;
}
} catch (Throwable t) {
logger.error("During DS init copy", t);
}
}
}
}
return count;
}
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
// …………省略部分代码
super.postInit();
}
protected void postInit() {
renewsLastMin.start();
// 在这里发现了服务端主动删除客户端的定时器了
if (evictionTaskRef.get() != null) {
evictionTaskRef.get().cancel();
}
evictionTaskRef.set(new EvictionTask());
evictionTimer.schedule(evictionTaskRef.get(),
serverConfig.getEvictionIntervalTimerInMs(),
serverConfig.getEvictionIntervalTimerInMs());
}
添加客户端实例
Eureka 采取的架构模式是 C/S 模式,即客户端/服务端模式,这种模式势必会涉及到通信。Eureka 也是采取了 MVC 模式,用的是jersey。在EurekaServerAutoConfiguration
这个配置类里可以看到相关代码。
@Bean
public FilterRegistrationBean<?> jerseyFilterRegistration(
javax.ws.rs.core.Application eurekaJerseyApp) {
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<Filter>();
bean.setFilter(new ServletContainer(eurekaJerseyApp));
bean.setOrder(Ordered.LOWEST_PRECEDENCE);
bean.setUrlPatterns(
Collections.singletonList(EurekaConstants.DEFAULT_PREFIX + "/*"));
return bean;
}
@Bean
public javax.ws.rs.core.Application jerseyApplication(Environment environment,
ResourceLoader resourceLoader) {
// …………省略部分代码
}
jersey 也有类似于 Spring MVC 一样的 Controller,只不过它这里称之为 Resource。所以要找到通信模块,所以要找到这样的 Resource。这些 Resource 在com.netflix.eureka.resources
包下面。
在com.netflix.eureka.resources.ApplicationResource
里面有个addInstance
方法,是添加实例的入口。启动 eureka 客户端的时候可以在这里打断点拦截到。
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication){
// …………省略部分代码
// isReplication 这个参数是用来标记是不是集群内部的操作
registry.register(info, "true".equals(isReplication));
return Response.status(204).build();
}
这里采用的事一个责任链模式,每一层的 registry 只做一类事情,采取继承的方式实现。进入registry.register(info, "true".equals(isReplication));
里面,发现这一层的 registry 是 Spring 提供的。
@Override
public void register(final InstanceInfo info, final boolean isReplication) {
handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
super.register(info, isReplication);
}
private void handleRegistration(InstanceInfo info, int leaseDuration,
boolean isReplication) {
log("register " + info.getAppName() + ", vip " + info.getVIPAddress()
+ ", leaseDuration " + leaseDuration + ", isReplication "
+ isReplication);
publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration,
isReplication));
}
在这一层的 registry 中其实没有做什么操作,只是 Spring 对 Eureka Client 的注册过程做了一个扩展功能,提前发布了一个注册事件,这个事件可以通过EurekaInstanceRegisteredEvent
监听到。打开这个事件类所在的包,可以看到 Spring 提供的关于 Eureka 的事件。
- EurekaInstanceRegisteredEvent。实例注册事件。
- EurekaInstanceRenewedEvent。实例心跳事件。
- EurekaInstanceCanceledEvent。实例取消注册事件。
- EurekaServerStartedEvent。eureka 服务启动成功事件。
- EurekaRegistryAvailableEvent。注册中心启动事件。
然后进入com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register
。
@Override
public void register(final InstanceInfo info, final boolean isReplication) {
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);
}
接着进入下一个责任链com.netflix.eureka.registry.AbstractInstanceRegistry
。
这个类里面有一个关键的成员变量registry,正是这里面存放了所有客户端实例和集群的信息。
这是一个同步容器ConcurrentHashMap
,Key 是 集群的名字,就是配置文件里的springapplication.name=client
,这里将这个名字全部大写了。里面的Map
存的事每个集群的实例信息,Key 是配置文件里配置的eureka.instance.instance-id
。
Lease
这个类的名字很有意思,叫租赁物,既然是租赁物,那么就会有过期时间,过期了就要续租。这个类里面有一些关键的属性。
public class Lease<T> {
enum Action {
Register, Cancel, Renew
};
// 默认续租时长
public static final int DEFAULT_DURATION_IN_SECS = 90;
// 真正的对象
private T holder;
// 回收时间,过期了就要回收租赁物
private long evictionTimestamp;
// 注册事件,就是从什么时候开始租的
private long registrationTimestamp;
// 恢复正常工作的时间,就是什么时候开始有效的
private long serviceUpTimestamp;
// 最后操作时间
private volatile long lastUpdateTimestamp;
// 续租时长
private long duration;
}
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();
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;
}
}
// 获取已经存在的该实例相同的名字,为重复注册准备
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
if (existingLease != null && (existingLease.getHolder() != null)) {
// 如果重复注册了,那么就取最近一个最近一次活跃的那个
Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
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 {
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();
}
}
心跳连接
在com.netflix.eureka.resources.InstanceResource
中有个renewLease
方法,这个方法是接收客户端请求心跳连接的(续租)的入口。
@PUT
public Response renewLease(
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
@QueryParam("overriddenstatus") String overriddenStatus,
@QueryParam("status") String status,
@QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
boolean isFromReplicaNode = "true".equals(isReplication);
boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
// …………省略部分代码
}
进入org.springframework.cloud.netflix.eureka.server.InstanceRegistry#renew
,发现也只是做了一个发布事件的功能。接着进入com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#renew
.然后进入com.netflix.eureka.registry.AbstractInstanceRegistry#renew
。
public boolean renew(String appName, String id, boolean isReplication) {
RENEW.increment(isReplication);
// 翻账单,找到租赁信息
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToRenew = null;
if (gMap != null) {
leaseToRenew = gMap.get(id);
}
if (leaseToRenew == null) {
// 如果没有找到账单,那就是没法续租
RENEW_NOT_FOUND.increment(isReplication);
logger.warn("DS: Registry: lease doesn't exist, registering resource: {} - {}", appName, id);
return false;
} else {
InstanceInfo instanceInfo = leaseToRenew.getHolder();
if (instanceInfo != null) {
// 看看实例的状态,看看租赁的东西是什么状态
InstanceStatus overriddenInstanceStatus = this.getOverriddenInstanceStatus(
instanceInfo, leaseToRenew, isReplication);
if (overriddenInstanceStatus == InstanceStatus.UNKNOWN) {
RENEW_NOT_FOUND.increment(isReplication);
return false;
}
if (!instanceInfo.getStatus().equals(overriddenInstanceStatus)) {
instanceInfo.setStatusWithoutDirty(overriddenInstanceStatus);
}
}
renewsLastMin.increment();
leaseToRenew.renew();
return true;
}
}
最后就是调用com.netflix.eureka.lease.Lease#renew
来续租。
public void renew() {
lastUpdateTimestamp = System.currentTimeMillis() + duration;
}
最后操作的时间 = 当前时间+续租时长,这个时间用来判断是否过期的。这里是个bug。最后操作时间应该是当前时间,这里不应该加上续租时长。
public boolean isExpired(long additionalLeaseMs) {
return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
}
这里是当当前时间 > 最后更新时间 + 续租时长,就认为已经过期了,但是最后更新时间之前已经加上了续租时长,相当于续租时长加了两次,默认 90 s过期就成了180s 过期了。
删除客户端/客户端主动退出
删除客户端
注意到 SpringCloud 中有一些配置,如:
eureka.client.instance.leaseExpirationDurationInSeconds=90
eureka.client.instance.leaseRenewalIntervalInSeconds=60
eureka.server.eviction-interval-timer-in-ms=60000
eureka.server.enable-self-preservation=false
这些配置中配置了心跳间隔和续租时长等,这些配置跟服务端删除客户端实例有关。
在启动阶段的源码,找到最后发现了服务端主动删除客户端的定时器。这里分析这个定时任务。
public void evict(long additionalLeaseMs) {
// 判断是否启用过期删除(长时间没有心跳删除)
if (!isLeaseExpirationEnabled()) {
return;
}
// 这里是先收集那些过期的客户端实例,只收集不清除
List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
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();
if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
expiredLeases.add(lease);
}
}
}
}
// 一次性删除一些客户端实例是由阈值限制的,
// 如果删除那些过期的实例之后,剩下的少于 85%(可配置),那是不被允许的,所以最多只能删除 15%
int registrySize = (int) getLocalRegistrySize();
int registrySizeThreshold = (int) (registrySize * serverConfig.getRenewalPercentThreshold());
int evictionLimit = registrySize - registrySizeThreshold;
// 取最小值
int toEvict = Math.min(expiredLeases.size(), evictionLimit);
if (toEvict > 0) {
// 随机获取要删除的实例,这样的目的是使得删除更加均匀,不至于一次删掉了某一个模块的所有实例
Random random = new Random(System.currentTimeMillis());
for (int i = 0; i < toEvict; i++) {
int next = i + random.nextInt(expiredLeases.size() - i);
Collections.swap(expiredLeases, i, next);
Lease<InstanceInfo> lease = expiredLeases.get(i);
String appName = lease.getHolder().getAppName();
String id = lease.getHolder().getId();
EXPIRED.increment();
// 移除实例
internalCancel(appName, id, false);
}
}
}
注意到这里移除某个实例并没有通知集群中的其它 Eureka 服务器,因为这是这个 Eureka 服务器自己内部的剔除逻辑,跟其它服务器无关,因为配置问题,其它服务器的剔除逻辑可能跟这个 Eureka不一样。
主动退出
如同注册一样,找到客户端主动退出的入口。
@DELETE
public Response cancelLease(
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {isReplication
boolean isSuccess = registry.cancel(app.getName(), id,
"true".equals(isReplication));
// …………省略部分代码
}
@Override
public boolean cancel(String appName, String serverId, boolean isReplication) {
// 发布客户端主动退出事件
handleCancelation(appName, serverId, isReplication);
return super.cancel(appName, serverId, isReplication);
}
@Override
public boolean cancel(final String appName, final String id,
final boolean isReplication) {
if (super.cancel(appName, id, isReplication)) {
replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
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;
}
return false;
}
一路点下去,发现回到了之前服务端清除客户端的方法。
protected boolean internalCancel(String appName, String id, boolean isReplication) {
try {
read.lock();
CANCEL.increment(isReplication);
// 获取客户端集群
Map<String, Lease<InstanceInfo>> gMap = registry.get(appName);
Lease<InstanceInfo> leaseToCancel = null;
if (gMap != null) {
// 从客户端集群移除这个示例,至此退出操作已经完成了
// 后面就是一些收尾工作,比如清楚缓存等
leaseToCancel = gMap.remove(id);
}
synchronized (recentCanceledQueue) {
recentCanceledQueue.add(new Pair<Long, String>(System.currentTimeMillis(), appName + "(" + id + ")"));
}
InstanceStatus instanceStatus = overriddenInstanceStatusMap.remove(id);
if (instanceStatus != null) {
logger.debug("Removed instance id {} from the overridden map which has value {}", id, instanceStatus.name());
}
if (leaseToCancel == null) {
CANCEL_NOT_FOUND.increment(isReplication);
logger.warn("DS: Registry: cancel failed because Lease is not registered for: {}/{}", appName, id);
return false;
} else {
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);
logger.info("Cancelled instance {}/{} (replication={})", appName, id, isReplication);
return true;
}
} finally {
read.unlock();
}
}
集群同步
在上面的代码中可以看到很多类似这样的代码,而且都是在客户端向服务端发送某个请求事件,服务端完成这个事件之后执行的。
replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
这个方法就是集群间同步的方法。
/**
* @param Action 是个枚举,列举了客户端的操作。
* Heartbeat 心跳, Register 注册, Cancel 退出,
* StatusUpdate 状态更新, DeleteStatusOverride 移除覆盖状态
*
*/
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) {
numberOfReplicationsLastMin.increment();
}
// 如果没有 eureka 服务端没有集群,或者这个操作的源头来自于集群内部,就不向其它服务实例同步,避免死循环
if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
return;
}
for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
// 剔除掉自身,不向自身发送同步的请求,避免死循环
if (peerEurekaNodes.isThisMyUrl(node.getServiceUrl())) {
continue;
}
// 这个方法里面就是根据不同的操作事件来触发不同的同步信息的方法
replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
}
} finally {
tracer.stop();
}
}
自我保护机制
Eureka 的自我保护机制是在剔除客户端注册信息时,如果开启了自我保护机制,或者满足自我保护机制时,则不去剔除客户端注册信息。
上面分析删除客户端的代码的时候有这样一个方法。
public boolean isLeaseExpirationEnabled() {
if (!isSelfPreservationModeEnabled()) {
return true;
}
return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
这个方法是判断是否启用租赁器过期,isSelfPreservationModeEnabled()
是读取配置文件里的配置,看是不是启用了自我保护机制,如果没有启用自我保护机制,就返回true,说明允许启用过期删除。
numberOfRenewsPerMinThreshold
这个变量是某个时间段内(默认60s)应该收到多个心跳才算正常。如果60s内收到的心跳大于这个值,那么久可以启用过期删除。如果比这个值小,说明服务器本身可能出现了问题,不能清除客户端注册信息,这就是自我保护机制,也是保证了服务端的可用性。这个值是通过计算出来的,公式如下:
protected void updateRenewsPerMinThreshold() {
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews
* (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
* serverConfig.getRenewalPercentThreshold());
}
应该收到的心跳数 = 期望发送心跳的客户端数量 * ( 60 / 期望客户端发送心跳的间隔时间 ) * 最少客户端数量阈值比。
比如服务器知道现在有100个客户端连接了,每个客户端30s发送一次心跳,一分内应该受到200个心跳数量,这个数量允许有误差,可以少 15%即必须有85%,那么算出来的阈值就是 100*(60/30)*0.85=170。那么如果后面服务删除客户端注册信息的时候,如果发现一分钟内心跳数量比170要少,此时就会触发自我保护机制。
这个阈值时会更新的,有这几种情况。
- 客户端注册时。
- 初始化服务端时。
- 客户端取消时。
- 每15分钟的定时任务。这个时间可以配置,参数是
renewal-threshold-update-interval-ms
。
缓存机制
Eureka 服务端采用三级缓存机制来提供客户端注册信息的增删改查。
- 只读缓存。
- 读写缓存。
- 真实数据。
实现在com.netflix.eureka.registry.ResponseCacheImpl
这个类中。看代码。
Value getValue(final Key key, boolean useReadOnlyCache) {
Value payload = null;
try {
// 是否启用只读缓存
if (useReadOnlyCache) {
// 从只读缓存中读取数据
final Value currentPayload = readOnlyCacheMap.get(key);
if (currentPayload != null) {
payload = currentPayload;
} else {
// 从读写缓存中读取数据
payload = readWriteCacheMap.get(key);
readOnlyCacheMap.put(key, payload);
}
} else {
payload = readWriteCacheMap.get(key);
}
} catch (Throwable t) {
logger.error("Cannot get value for key : {}", key, t);
}
return payload;
}
这是从缓存中获取客户端注册信息的方法。首先判断是否启用只读缓存,如果开启了就从只读缓存中读取数据,否则直接从读写缓存中获取。如果只读缓存中没有数据,那么也会从读写缓存中获取数据,并将数据放到只读缓存中。这里并没有看到从真实缓存中获取数据的代码,因为 eureka 保证读写换粗中一定能拿到数据。看看读写缓存的实现就知道了。
this.readWriteCacheMap = CacheBuilder.newBuilder()
.initialCapacity(serverConfig.getInitialCapacityOfResponseCache())
.expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS)
.removalListener(new RemovalListener<Key, Value>() {
@Override
public void onRemoval(RemovalNotification<Key, Value> notification) {
Key removedKey = notification.getKey();
if (removedKey.hasRegions()) {
Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
}
}
})
.build(new CacheLoader<Key, Value>() {
@Override
public Value load(Key key) throws Exception {
if (key.hasRegions()) {
Key cloneWithNoRegions = key.cloneWithoutRegions();
regionSpecificKeys.put(cloneWithNoRegions, key);
}
Value value = generatePayload(key);
return value;
}
});
读写缓存用 google 的 guava 缓存实现的,在CacheBuilder
类中可以看到介绍。重点看build
里面的load
方法里面的generatePayload(key)
。这个方法里是根据访问的角色来做不同的数据获取,然后判断是全量更新还是增量更新,最后还是从之前说到的ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
获取数据。
回到只读缓存,系统默认每30s会从读写缓存中同步数据,这是由一个简单的定时器完成的。定时参数可以修改response-cache-update-interval-ms
。
private TimerTask getCacheUpdateTask() {
return new TimerTask() {
@Override
public void run() {
for (Key key : readOnlyCacheMap.keySet()) {
try {
CurrentRequestVersion.set(key.getVersion());
Value cacheValue = readWriteCacheMap.get(key);
Value currentCacheValue = readOnlyCacheMap.get(key);
if (cacheValue != currentCacheValue) {
readOnlyCacheMap.put(key, cacheValue);
}
} catch (Throwable th) {
logger.error("Error while updating the client cache from response cache for key {}", key.toStringCompact(), th);
}
}
}
};
}
Eureka Server 设计这样的三级缓存是为了减少真实数据的读写压力,将读分散出去。由于缓存同步的原因,这样会造成一定的延迟,这也是说 Eureka 遵循的是 AP 原理的一个原因。