Eureka源码解读与优化实践(一)

622 阅读8分钟

简介

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

image.png

内容如下:

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端在启动时候,主要完成了三件事:

  • 向集群中的其他节点拉去注册表信息。
  • 启动定时剔除任务,将过期服务从注册表中清除。
  • 自我保护状态及数值的计算。