简介
Eureka是Netflix开放源码、上手简单的一款注册中心,符合CAP理论中的AP,也就是它不能保证强一致性,这和强一致性的Zookeeper不同。
角色
Eureka可以作为Server端,接受其他微服务系统的注册、下线等功能。同时,Eureka集群的节点,也可以作为Client的角色,向其他Eureka进行注册等功能。
Server端的启动原理
在这部分,讲会介绍Eureka Server端的启动原理、剔除的定时任务等。
Server端在使用时,需要在启动类上添加注解 @EnableEurekaServer
,我们来看这个注解源码:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({EurekaServerMarkerConfiguration.class})
public @interface EnableEurekaServer {
// 省略
}
该注解没有定义任何的属性等内容,但是注意到该注解上方的@Import,通过这个注解,导入了一个配置类EurekaServerMarkerConfiguration
, 我们再来跟踪这个类:
@Configuration(
proxyBeanMethods = false
)
public class EurekaServerMarkerConfiguration {
public EurekaServerMarkerConfiguration() {
}
@Bean
public EurekaServerMarkerConfiguration.Marker eurekaServerMarkerBean() {
return new EurekaServerMarkerConfiguration.Marker();
}
class Marker {
Marker() {
}
}
}
这个类其实也很好理解,定义了一个内部类Marker,,并且通过@Bean注解向Spring容器注入了Marker实例。那注入的这个实例有什么用?我们带着这根疑问,继续往下。
下面进入正文,在我们依赖的库里,找到这个文件spring.factories
内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration
通过该文件,指定了Eureka的启动主类。点击查看该类,由于该类比较长,这里我们先看该类的声明:
@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 {
// 省略
}
通过该源码,可以看到,该类上面使用了较多的注解。我们关心的是@ConditionalOnBean
和@Import
。分别来说明这两个注解的用途。
- @ConditionalOnBean
只有当Spring容器中存在ConditionalOnBean注解指定的对象实例时候,使用了ConditionalOnBean注解的类,才会被Spring实例化。
在上面这段代码中,@ConditionalOnBean
后面需要的实例是上文提到过得Marker
对象。由于Marker对象,通过@EnableEurekaServer注解已经完成了实例化过程(@Bean注解实现注入),所以这个条件为true,EurekaServerAutoConfiguration对象可以被Spring创建。
- @Import
使用该注解,可以快速的将指定的类,实例化到Spring容器中。
所以在创建EurekaServerAutoConfiguration这个对象实例之前,其实先创建了@Import指定的EurekaServerInitializerConfiguration。
再继续来看被提前初始化的导入类EurekaServerInitializerConfiguration
:
@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();
}
}
来看start()方法, 由于该类继承自SmartLifecycle,SmartLifecycle继承自Lifecycle。所以在实例化EurekaServerInitializerConfiguration
这个类的时候,会调用其start()方法。(具体的调用栈为:
AbstractApplicationContext.refresh -> finishRefresh -> getLifecycleProcessor().onRefresh() —> DefaultLifecycleProcessor.startBeans() —> phases.get(key).start() —> doStart(this.lifecycleBeans, member.name, this.autoStartupOnly) -> bean.start())
所以当EurekaServerInitializerConfiguration
创建完成之后,其start方法也会被调用起来。该方法主要完成两件事:
- 完成必要的初始化工作,下面讨论的重点
- 发布事件,不重要
来看初始化工作的源码:
# EurekaServerBootstrap
public void contextInitialized(ServletContext context) {
try {
initEurekaEnvironment();
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);
}
}
该方法,主要完成两件事:
- 初始化环境,主要设置ConfigurationManager对象的一些属性值,不重要
- 初始化上下文,下面会重点介绍。源码如下:
# EurekaServerBootstrap
protected void initEurekaServerContext() throws Exception {
// For backward compatibility
JsonXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
XStream.PRIORITY_VERY_HIGH);
XmlXStream.getInstance().registerConverter(new V1AwareInstanceInfoConverter(),
XStream.PRIORITY_VERY_HIGH);
if (isAws(this.applicationInfoManager.getInfo())) {
this.awsBinder = new AwsBinderDelegate(this.eurekaServerConfig,
this.eurekaClientConfig, this.registry, this.applicationInfoManager);
this.awsBinder.start();
}
EurekaServerContextHolder.initialize(this.serverContext);
log.info("Initialized server context");
// 从集群中的其他peer节点,拉去注册表
int registryCount = this.registry.syncUp(); // [1]
//
this.registry.openForTraffic(this.applicationInfoManager, registryCount); // [2]
// Register all monitoring statistics.
EurekaMonitors.registerAllStats();
}
这段代码,比较长,中间出现了有关aws相关的内容,忽略即可,着重来看其中[1]和[2]的两行代码: 先看initEurekaServerContext方法中[1]处的代码实现:
# PeerAwareInstanceRegistryImpl
@Override
public int syncUp() {
// Copy entire entry from neighboring DS node
int count = 0;
// 默认会尝试拉取5次
for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); i++) {
if (i > 0) {
try {
// 每次间隔默认30秒
Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
} catch (InterruptedException e) {
logger.warn("Interrupted during registry transfer..");
break;
}
}
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;
}
syncUp()方法,定义自PeerAwareInstanceRegistry接口,这里的实现是在其实现类PeerAwareInstanceRegistryImpl中。该方法,会去拉取Eureka集群中其他peer中当前已经注册了的服务列表。
再来看initEurekaServerContext方法中[2],源码如下:
# PeerAwareInstanceRegistryImpl
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {
// 本次期望的注册服务发送的心跳数量
this.expectedNumberOfClientsSendingRenews = count;
// 更新剔除下线的阈值
updateRenewsPerMinThreshold(); // [1]
logger.info("Got {} instances from neighboring DS node", count);
logger.info("Renew threshold is: {}", numberOfRenewsPerMinThreshold);
this.startupTime = System.currentTimeMillis();
if (count > 0) {
this.peerInstancesTransferEmptyOnStartup = false;
}
// aws相关
DataCenterInfo.Name selfName = applicationInfoManager.getInfo().getDataCenterInfo().getName();
boolean isAws = Name.Amazon == selfName;
if (isAws && serverConfig.shouldPrimeAwsReplicaConnections()) {
logger.info("Priming AWS connections for all replicas..");
primeAwsReplicas(applicationInfoManager);
}
logger.info("Changing status to UP");
// 更新实例状态为:UP
applicationInfoManager.setInstanceStatus(InstanceStatus.UP);
//
super.postInit();
}
该方法主要完成三件事:
- 准备好服务保护相关的数值:心跳数和剔除下线的阈值。
- 在postInit方法中创建、并启动剔除服务下线的定时任务。下面重点来说。
先看下上面openForTraffic方法中[1]的具体实现:
# AbstractInstanceRegistry
protected void updateRenewsPerMinThreshold() {
this.numberOfRenewsPerMinThreshold = (int) (this.expectedNumberOfClientsSendingRenews
* (60.0 / serverConfig.getExpectedClientRenewalIntervalSeconds())
* serverConfig.getRenewalPercentThreshold());
}
该方法计算出,一分钟内,触发自我保护阈值的续约服务的数量值
这里先解释一个重要的概念:服务保护
Eureka提供了几个服务保护相关的配置项,当符合这些要求时,即使当Eureka在超出了时间范围内,无法接收到已注册服务的续约请求后,也不会从自己的容器中,剔除这些服务。这就叫服务的自我保护,对一个大的微服务系统来说,偶尔由于网络抖动原因,导致的少部分服务不能及时续约,是可能存在的。
相关的配置项如下, 可在Eureka的Server端配置:
eureka:
server:
enable-self-preservation: true # 开启自我保护
renewal-percent-threshold: 0.85 # 掉线服务少于该阈值时候, 不再剔除超时服务
# 默认触发自我保护的阈值为85%
eviction-interval-timer-in-ms: 30000 # 无效服务剔除服务的定时任务,轮询间隔时间
# 默认30s扫描一次
上面说了,符合条件的情况下,那么这个条件是什么呢?
假设有100个微服务,只要在一分钟内及时续约的服务低于注册服务总数的85%这个默认阈值的时候,后续不能及时续约的服务,Eureka也不会从注册表中剔除该服务。(通俗的讲,就是开始有几个服务挂了,Eureka就认为是挂了,但是一分钟内挂的服务越来越多了,Eureka就会怀疑是网络故障,于是后面没及时续约的服务,Eureka便睁只眼闭只眼的放过了)。
这里可以进行一定的优化:
-
当注册的服务数量比较少的时候,可以关闭自我保护机制。比如10个微服务,那么挂掉3个,就会触发自我保护机制,但实际上有很大可能,这些服务真的挂掉了。
-
当注册的服务数量很多的时候,可以开启自我保护机制。比如100个微服务,那么默认需要挂掉15个才会触发自我保护。通常由于网络原因,导致几个微服务不能正常及时续约也是很正常的。
protected void postInit() {
renewsLastMin.start();
if (evictionTaskRef.get() != null) {
evictionTaskRef.get().cancel();
}
// 创建一个任务实例
evictionTaskRef.set(new EvictionTask());
// 启动任务
evictionTimer.schedule(evictionTaskRef.get(),
serverConfig.getEvictionIntervalTimerInMs(),
serverConfig.getEvictionIntervalTimerInMs());
}
上面这段代码创建了一个定时任务,并启动了该定时任务。默认延时30秒启动,每间隔30秒执行一次定时任务。
这里默认30秒的时间相对可能过长,如果需要提高系统响应的及时性,可以将该数值缩短,避免客户端调用到未及时剔除掉的服务,该参数的配置如下:
eureka:
server:
eviction-interval-timer-in-ms: 1000 # 无效服务剔除服务的定时任务,轮询间隔时间
看下这个定时任务类的源码:
# AbstractInstanceRegistry.EvictionTask
class EvictionTask extends TimerTask {
private final AtomicLong lastExecutionNanosRef = new AtomicLong(0l);
@Override
public void run() {
try {
long compensationTimeMs = getCompensationTimeMs();
logger.info("Running the evict task with compensationTime {}ms", compensationTimeMs);
evict(compensationTimeMs);
} catch (Throwable e) {
logger.error("Could not run the evict task", e);
}
}
long getCompensationTimeMs() {
long currNanos = getCurrentTimeNano();
long lastNanos = lastExecutionNanosRef.getAndSet(currNanos);
if (lastNanos == 0l) {
return 0l;
}
long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos);
long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs();
return compensationTime <= 0l ? 0l : compensationTime;
}
long getCurrentTimeNano() { // for testing
return System.nanoTime();
}
}
该类是AbstractInstanceRegistry的内部类,继承自TimerTask,是个任务,在run方法中调用了evict(..)方法,再来看下这个方法的核心实现:
public void evict(long additionalLeaseMs) {
// 如果开启、 且触发了自我保护机制, 则不会再去清理注册表中未及时续约的服务
if (!isLeaseExpirationEnabled()) { // [1]
return;
}
// 过期租约列表
List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
// 这里遍历所有已经注册的服务列表, 找出所有过期了的服务, 并添加到过期集合中
// registry结构:Map<appName, <instanceId, InstanceInfo>>
// 这里的registry,是Eureka实现呢高性能的几个关键数据结构之一,还会细说。
// Lease:服务信息InstanceInfo的持有类,还会细说
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();
// 过期则加入到expiredLeases集合中
if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
expiredLeases.add(lease);
}
}
}
}
// 获取注册服务的数量
int registrySize = (int) getLocalRegistrySize();
// 获取注册服务的阈值,即前面说的自我保护的阈值,默认85%
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++) {
// 每次随机的从过期列表expiredLeases中,随机获取一个过期服务
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);
}
}
}
上面代码[1]处的代码简单看下:
# PeerAwareInstanceRegistryImpl
@Override
public boolean isLeaseExpirationEnabled() {
// 如果未开启自我保护, 直接返回true
if (!isSelfPreservationModeEnabled()) {
// The self preservation mode is disabled, hence allowing the instances to expire.
return true;
}
// 最后一分钟的续约数大于根据阈值计算出来的最低续约数, 则返回true; 否则返回false
// 其实就是判断是否触发了自我保护, 没触发则返回true
return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}
该方法其实就是判断自我保护有没有开启、或者开启了有没有被触发。否,则返回true;是,则返回false。
总结
本篇主要介绍了Eureka Server端的启动流程和定时剔除任务相关的源码,并总结了日常的优化经验。源码简单概括如下: Eureka Server端在启动时候,主要完成了三件事:
- 向集群中的其他节点拉去注册表信息。
- 启动定时剔除任务,将过期服务从注册表中清除。
- 自我保护状态及数值的计算。