您的位置:  首页 > 技术杂谈 > 正文

领导让我研究 Eureka 源码 | 启动过程

2021-10-19 19:00 https://my.oschina.net/u/4499317/blog/5282007 悟空聊架构 次阅读 条评论

Eureka 源码之启动过程

大家好,我悟空。

最近在倒腾 Eureka 源码,因为大环境太卷了,必须得卷点源码才行,另外呢,能够读懂开源项目的源码、解决项目中遇到的问题是实力的象征,是吧?如果只是会用些中间件,那是不够的,和 CRUD 区别不大。

话不多说,源码走起。本篇是 Eureka 源码分析的开篇,后续会持续分享源码解析的文章。

首先呢,Eureka 服务的启动入口在这里:EurekaBootStrap.java 的 contextInitialized 方法。

关于源码的获取直接到官网下载就好了。https://github.com/Netflix/eureka

本文已收录到我的 github:https://github.com/Jackson0714/PassJava-Learning

一、初始化环境

启动类是 EurekaBootStrap.java,在这个路径下:

\eureka\eureka-core\src\main\java\com\netflix\eureka\EurekaBootStrap.java

启动时序图:

初始化环境时序图

启动代码:

@Override
public void contextInitialized(ServletContextEvent event) {
    initEurekaEnvironment();
    initEurekaServerContext();
    // 省略非核心代码
}

初始化环境的方法 initEurekaEnvironment(),点进去看下这个方法做了什么。

String dataCenter = ConfigurationManager.getConfigInstance().getString(EUREKA_DATACENTER);

就是获取配置管理类的一个单例。单例的实现方法用的是 双重检测 + volatile

public static AbstractConfiguration getConfigInstance() {
        if (instance == null) {
            synchronized (ConfigurationManager.class) {
                if (instance == null) {
                    instance = getConfigInstance(false));
                }
            }
        }
        return instance;
    }

instance 变量定义成了 volatile,保证可见性。

static volatile AbstractConfiguration instance = null;

线程 A 修改后,会将变量的值刷到主内存中,线程 B 会将主内存中的值刷回到自己的线程内存中,也就是说线程 A 改了后,线程 B 可以看到改了后的值。

可以参考之前我写的文章:反制面试官 - 14 张原理图 - 再也不怕被问 volatile

启动的整体流程如图:

二、初始化上下文

初始化上下文的时序图如下:

初始化上下文的时序图

还是在 EurekaBootStrap.java 类中 contextInitialized 方法中,第二步调用了 initEurekaServerContext() 方法。

initEurekaServerContext 里面主要的操作分为六步:

第一步就是加载 配置文件。

2.1 加载 eureka-server 配置文件

基于接口的方式,获取配置项。

initEurekaServerContext 方法创建了一个 eurekaServerConfig 对象:

EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig();

EurekaServerConfig 是一个接口,里面定义了很多获取配置项的方法。和定义常量来获取配置项的方式不同。比如获取 AccessId 和 SecretKey。

String getAWSAccessId();
String getAWSSecretKey();

还有另外一种获取配置项的方式:Config.get(Constants.XX_XX),这种方式和上面的接口的方式相比:

  • 常量的方式较容易取错变量。因为常量的定义都是大写,很可能拿到 XX_XY 变量,而接口的方法是驼峰命名的,更容易辨识,对于相似的变量,取一个辨识度更高的方法名即可。
  • 常量的方式不易于修改。假如修改了常量名称,则需要全局搜索用到的地方,都改掉。如果是用接口的方式,则只需要修改接口方法中引用常量的地方即可,对于调用接口方法的地方是透明的。

2.1.1 创建默认的 eureka server 配置

new DefaultEurekaServerConfig (),会创建出一个默认的 server 配置,构造方法会调用 init 方法:

public DefaultEurekaServerConfig() {
    init();
}

2.2.2 加载配置文件

private void init() {
    String env = ConfigurationManager.getConfigInstance().getString(
            EUREKA_ENVIRONMENT, TEST);
    ConfigurationManager.getConfigInstance().setProperty(
            ARCHAIUS_DEPLOYMENT_ENVIRONMENT, env);

    String eurekaPropsFile = EUREKA_PROPS_FILE.get();
    try {
        // ConfigurationManager
        // .loadPropertiesFromResources(eurekaPropsFile);
        ConfigurationManager
                .loadCascadedPropertiesFromResources(eurekaPropsFile);
    } catch (IOException e) {
        logger.warn(
                "Cannot find the properties specified : {}. This may be okay if there are other environment "
                        + "specific properties or the configuration is installed with a different mechanism.",
                eurekaPropsFile);
    }
}

前两行是设置环境名称,后面几行是关键语句:获取配置文件,并放到 ConfigurationManager 单例中。

来看下 EUREKA_PROPS_FILE.get(); 做了什么。首先 EUREKA_PROPS_FILE 是这样定义的:

private static final DynamicStringProperty EUREKA_PROPS_FILE = DynamicPropertyFactory
        .getInstance().getStringProperty("eureka.server.props",
                "eureka-server");

用单例工厂 DynamicPropertyFactory 设置了默认值 eureka-server,然后 EUREKA_PROPS_FILE.get() 就会从缓存里面这个默认值。

然后再调用 loadCascadedPropertiesFromResources 方法,来加载配置文件。

首先会拼接默认的配置文件:

String defaultConfigFileName = configName + ".properties";

然后获取默认配置文件的配置项:

Properties props = getPropertiesFromFile(url);

然后再拼接当前环境的配置文件

String envConfigFileName = configName + "-" + environment + ".properties";

然后获取环境的配置文件的配置项并覆盖之前的默认配置项。

props.putAll(envProps);

putAll 方法就是将这些属性放到一个 map 中。

然后这些配置项统一都交给 ConfigurationManager 来管理:

config.loadProperties(props);

其实就是加载这个文件:

eureka-server.properties

打开这个文件后,发现里面有几个 demo 配置项,不过都被注释了。

2.1.3 真正的配置项在哪?

上面可以看到 eureka-server.properties 都是空的,那配置项都配置在哪呢?

我们之前说过,DefaultEurekaServerConfig 是实现了 EurekaServerConfig 接口的,如下所示:

public class DefaultEurekaServerConfig implements EurekaServerConfig

在 EurekaServerConfig 接口里面定义很多 get 方法,而 DefaultEurekaServerConfig 实现了这些 get 方法,来看下怎么实现的:

@Override
public int getWaitTimeInMsWhenSyncEmpty() {
    return configInstance.getIntProperty(
            namespace + "waitTimeInMsWhenSyncEmpty", (1000 * 60 * 5)).get();
}

里面的类似这样的 getXX 的方法,都有一个 default value,比如上面的是 1000 * 60 * 5,所以我们可以知道,配置项是在 DefaultEurekaServerConfig 类中定义的。

configInstance 这个单例又是 DynamicPropertyFactory 类型的,而在创建 configInstance 单例的时候,ConfigurationManager 还做了一些事情:将配置文件中的配置项放到 DynamicPropertyFactory 单例中,这样的话,DefaultEurekaServerConfig 中的 get 方法就可以获取到配置文件中的配置项了。具体的代码在 DynamicPropertyFactory 类中的 initWithConfigurationSource 方法中。

结合上面的加载配置文件的分析,可以得出结论:如果配置文件中没有配置,则用 DefaultEurekaServerConfig 定义的默认值。

2.1.4 加载配置文件小结

(1)创建一个 DefaultEurekaServerConfig 对象,实现了 EurekaServerConfig 接口,里面有很多获取配置项的方法。

(2)DefaultEurekaServerConfig 构造函数中调用了 init 方法。

(3)init 方法会加载 eureka-server.properties 配置文件,把里面的配置项都放到一个 map 中,然后交给 ConfigurationManager 来管理。

(4)DefaultEurekaServerConfig 对象里面有很多 get 方法,里面通过 hard code 定义了配置项的名称,当调用 get 方法时,调用的是 DynamicPropertyFactory 的获取配置项的方法,这些配置项如果在配置文件中有,则用配置项的。配置文件中的配置项是通过 ConfigurationManager 赋值给 DynamicPropertyFactory 的。

(5)当要获取配置项时,就调用对应的 get 方法,如果配置文件没有配置,则用默认值。

2.2 构造实例信息管理器

2.2.1 初始化服务实例的配置 instanceConfig

创建了一个 ApplicationInfoManager 对象,服务配置管理器,Application 可以理解为一个 Eureka client,作为一个应用程序向 Eureka 服务注册的。

applicationInfoManager = new ApplicationInfoManager(
        instanceConfig, new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get());

创建这个对象时,传了 instanceConfig,这个就是 eureka 实例的配置。这个 instanceConfig 和之前讲过的 EurekaServerConfig 很像,都是实现了一个接口,通过接口的 getXX 方法来获取配置信息。

2.2.2 构造服务实例 instanceInfo

另外一个参数是 EurekaConfigBasedInstanceInfoProvider,这个 Provider 是用来构造 instanceInfo(服务实例)。

怎么构造出来的呢?用到了设计模式中的构造器模式,而用到的配置信息就是从 EurekaInstanceConfig 里面获取到的。

InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder(vipAddressResolver);
builder.setXX
    ...
instanceInfo = builder.build();

setXX 的代码如下所示:

setXX 示例

2.2.3 小结

(1)初始化服务实例的配置 instanceConfig 。

(2)用构造器模式初始化服务实例 instanceInfo。

(3)将 instanceConfig 和 instanceInfo 传给了 ApplicationInfoManager,交由它来管理。

2.3 初始化 eureka-client

2.3.1 初始化 eureka-client 配置

eurekaClient 是包含在 eureka-server 服务中的,用来跟其他 eureka-server 进行通信的。为什么还会有其他 eureka-server,因为在集群环境中,是会有多个 eureka 服务的,而服务之间是需要相互通信的。

初始化 eureka-client 代码:

EurekaClientConfig eurekaClientConfig = new DefaultEurekaClientConfig();
eurekaClient = new DiscoveryClient(applicationInfoManager, eurekaClientConfig);

第一行又是初始化了一个配置,和之前初始化 server config,instance config 的地方很相似。也是通过接口方法里面的 DynamicPropertyFactory 来获取配置项的值。

eureka-client 也有一个加载配置文件的方法:

Archaius1Utils.initConfig(CommonConstants.CONFIG_FILE_NAME);

这个文件就是 eureka-client.properties。

初始化配置的时候还初始化了一个 DefaultEurekaTransportConfig(),可以理解为传输的配置。

2.3.2 初始化 eurekaClient

再来看下第二行代码,创建了一个 DiscoveryClient 对象,赋值给了 eurekaClient。

创建 DiscoveryClient 对象的过程非常复杂,我们来细看下。

(1)拿到 eureka-client 的 config 、transport 的 config、instance 实例信息。

(2)判断是否要获取注册表信息,默认会获取。

if (config.shouldFetchRegistry())

如果在配置文件中定义了 fetch-registry: false,则不会获取,单机 eureka 情况下,配置为 false,因为自己就包含了注册表信息,而且也不需要从其他 eureka 实例上获取配置信息。当在集群环境下,才需要获取注册表信息。

(3)判断是否要把自己注册到其他 eureka 上,默认会注册。

if (config.shouldRegisterWithEureka())

单机情况下,配置 register-with-eureka: false。

(4)创建了一个支持任务调度的线程池。

scheduler = Executors.newScheduledThreadPool(2,
        new ThreadFactoryBuilder()
                .setNameFormat("DiscoveryClient-%d")
                .setDaemon(true)
                .build());

(5)创建了一个支持心跳检测的线程池。

heartbeatExecutor = new ThreadPoolExecutor(
        1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>(),
        new ThreadFactoryBuilder()
                .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
                .setDaemon(true)
                .build()
);  // use direct handoff

(6)创建了一个支持缓存刷新的线程池。

cacheRefreshExecutor = new ThreadPoolExecutor(
        1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>(),
        new ThreadFactoryBuilder()
                .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
                .setDaemon(true)
                .build()
);  // use direct handoff

(7)创建了一个支持 eureka client 和 eureka server 进行通信的对象

eurekaTransport = new EurekaTransport();

(8)初始化调度任务

initScheduledTasks();

这个里面就会根据 fetch-registry 来判断是否需要开始调度执行刷新注册表信息,默认 30 s 调度一次。这个刷新的操作是由一个 CacheRefreshThread 线程来执行的。

同样的,也会根据 register-with-eureka 来判断是否需要开始调度执行发送心跳,默认 30 s 调度一次。这个发送心跳的操作由一个 HeartbeatThread 线程来执行的。

然后还创建了一个实例信息的副本,用来将自己本地的 instanceInfo 实例信息传给其他服务。什么时候发送这些信息呢?

又创建了一个监听器 statusChangeListener,这个监听器监听到状态改变时,就调用副本的 onDemandUpdate() 方法,将 instanceInfo 传给其他服务。

2.4 处理注册相关的流程

2.4.1 注册对象

创建了一个 PeerAwareInstanceRegistryImpl 对象,通过名字可以知道是可以感知集群实例注册表的实现类。通过官方注释可以知道这个类的作用:

  • 处理所有的拷贝操作到其他节点,让他们保持同步。复制的操作包含 注册,续约,摘除,过期和状态变更。

  • 当 eureka server 启动后,它尝试着从集群节点去获取所有的注册信息。如果获取失败了,当前 eureka server 在一段时间内不会让其他应用获取注册信息,默认 5 分钟。

  • 自我保护机制:如果应用丢失续约的占比在一定时间内超过了设定的百分比,则 eureka 会报警,然后停止执行过期应用。

registry = new PeerAwareInstanceRegistryImpl(
        eurekaServerConfig,
        eurekaClient.getEurekaClientConfig(),
        serverCodecs,
        eurekaClient
);

PeerAwareInstanceRegistryImpl 继承 AbstractInstanceRegistry 抽象类,构造函数主要做了以下事情:

  • 初始化 server config 和 client config 的配置信息。
this.serverConfig = serverConfig;
this.clientConfig = clientConfig;
  • 初始化摘除的队列,队列长度为 1000。
this.recentCanceledQueue = new CircularQueue<Pair<Long, String>>(1000);
  • 初始化注册的队列。
this.recentRegisteredQueue = new CircularQueue<Pair<Long, String>>(1000);

2.5 初始化上下文

2.5.1 集群节点帮助类

创建了一个 PeerEurekaNodes,它是一个帮助类,来管理集群节点的生命周期。

PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
        registry,
        eurekaServerConfig,
        eurekaClient.getEurekaClientConfig(),
        serverCodecs,
        applicationInfoManager
);

2.5.2 默认上下文

创建了一个 DefaultEurekaServerContext 默认上下文。

serverContext = new DefaultEurekaServerContext(
        eurekaServerConfig,
        serverCodecs,
        registry,
        peerEurekaNodes,
        applicationInfoManager
);

2.5.3 创建上下文的持有者

创建了一个 holder,用来持有上下文。其他地方想要获取上下文,就通过 holder 来获取。用到了单例模式。

EurekaServerContextHolder.initialize(serverContext);

holder 的 initialize() 初始化方法是一个线程安全的方法。

public static synchronized void initialize(EurekaServerContext serverContext) {
    holder = new EurekaServerContextHolder(serverContext);
}

定义了一个静态的私有的 holder 变量

private static EurekaServerContextHolder holder;

其他地方想获取 holder 的话,就通过 getInstance() 方法来获取 holder。

public static EurekaServerContextHolder getInstance() {
    return holder;
}

然后想要获取上下文的就调用 holder 的 getServerContext() 方法。

public EurekaServerContext getServerContext() {
    return this.serverContext;
}

2.5.4 初始化上下文

调用 serverContext 的 initialize() 方法来初始化。

public void initialize() throws Exception {
    logger.info("Initializing ...");
    peerEurekaNodes.start();
    registry.init(peerEurekaNodes);
    logger.info("Initialized");
}

peerEurekaNodes.start();

这个里面就是启动了一个定时任务,将集群节点的 URL 放到集合里面,这个集合不包含本地节点的 url。每隔一定时间,就更新 eureka server 集群的信息。

registry.init(peerEurekaNodes);

这个里面会初始化注册表,将集群中的 注册信息获取下,然后放到注册表里面。

2.6 其他

2.6.1 从相邻节点拷贝注册信息

int registryCount = registry.syncUp();

2.6.2 eureka 监控

EurekaMonitors.registerAllStats();

2.7 编译报错的解决方案

1、异常1

An exception occurred applying plugin request [id: 'nebula.netflixoss', version: '3.6.0']

解决方案

plugins {
  id 'nebula.netflixoss' version '5.1.1'
}

异常 2、

eureka-server-governator Plugin with id 'jetty' not found.

参考 https://blog.csdn.net/Sino_Crazy_Snail/article/details/79300058

三、总结

来一份 Eureka 启动的整体流程图

Eureka 启动过程

写了两本 PDF,回复 分布式 或 PDF 下载。

我的知识星球已开通,回复 知识星球 加入。

我是悟空,努力变强,变身超级赛亚人!

  • 0
    感动
  • 0
    路过
  • 0
    高兴
  • 0
    难过
  • 0
    搞笑
  • 0
    无聊
  • 0
    愤怒
  • 0
    同情
热度排行
友情链接