图解 Eureka 源码之启动过程 #yyds干货盘点#

智者不为愚者谋,勇者不为怯者死。这篇文章主要讲述图解 Eureka 源码之启动过程 #yyds干货盘点#相关的知识,希望能为你提供帮助。
Eureka 源码之启动过程

图解 Eureka 源码之启动过程 #yyds干货盘点#

文章图片

大家好,我是悟空。
最近在倒腾 Eureka 源码,因为大环境太卷了,必须得卷点源码才行,另外呢,能够读懂开源项目的源码、解决项目中遇到的问题是实力的象征,是吧?如果只是会用些中间件,那是不够的,和 CRUD 区别不大。
话不多说,源码走起。本篇是 Eureka 源码分析的开篇,后续会持续分享源码解析的文章。
首先呢,Eureka 服务的启动入口在这里:EurekaBootStrap.java 的 contextInitialized 方法。
图解 Eureka 源码之启动过程 #yyds干货盘点#

文章图片

一、初始化环境启动类是 EurekaBootStrap.java,在这个路径下:
\\eureka\\eureka-core\\src\\main\\java\\com\\netflix\\eureka\\EurekaBootStrap.java

启动时序图:
图解 Eureka 源码之启动过程 #yyds干货盘点#

文章图片

启动代码:
@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
启动的整体流程如图:
二、初始化上下文初始化上下文的时序图如下:
图解 Eureka 源码之启动过程 #yyds干货盘点#

文章图片

还是在 EurekaBootStrap.java 类中 contextInitialized 方法中,第二步调用了 initEurekaServerContext() 方法。
initEurekaServerContext 里面主要的操作分为六步:
第一步就是加载 配置文件。
2.1 加载 eureka-server 配置文件
基于接口的方式,获取配置项。
initEurekaServerContext 方法创建了一个 eurekaServerConfig 对象:
EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig();

图解 Eureka 源码之启动过程 #yyds干货盘点#

文章图片

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 源码之启动过程 #yyds干货盘点#

文章图片

打开这个文件后,发现里面有几个 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 构造实例信息管理器
图解 Eureka 源码之启动过程 #yyds干货盘点#

文章图片

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 的代码如下所示:
图解 Eureka 源码之启动过程 #yyds干货盘点#

文章图片

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 实例信息传给其他服务。什么时候发送这些信息呢?
【图解 Eureka 源码之启动过程 #yyds干货盘点#】又创建了一个监听器 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、异常1An 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 源码之启动过程 #yyds干货盘点#

文章图片

作者简介:悟空,8年一线互联网开发和架构经验,用故事讲解分布式、架构设计、Java 核心技术。《JVM性能优化实战》专栏作者,开源了《Spring Cloud 实战 PassJava》项目,公众号:悟空聊架构。本文已收录至 www.passjava.cn

    推荐阅读