2023-08-07
原文作者:Ressmix 原文地址:https://www.tpvlog.com/article/239

从本章开始,我将正式对Eureka的源码进行分析。我会首先讲解Eureka-Server的启动流程,在这个过程中,我会对涉及到的Eureka的一些核心组件进行讲解,理解这些组件的概念是学习后面源码的基础。

注意,Eureka源码中有很多和AWS亚马逊云服务相关的组件,这些我都会全部略过。

一、代码结构

从上一章我们已经知道,eureka-server本质就是对eureka-clienteureka-core进行了一层封装,方便以web应用的方式在web容器中进行部署。我们可以看下eureka-server模块的基本结构,本质就是个java web应用:

    C:\USERS\RESSMIX\DESKTOP\EUREKA\EUREKA-SERVER
    └─src
        ├─main
        │  ├─resources
        │  └─webapp
        │      └─WEB-INF
        └─test
            └─java
                └─com
                    └─netflix
                        └─eureka
                            └─resources

1.1 web.xml

它的web.xml里面的内容也很简单:

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app version="2.5"
             xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
        http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
      <listener>
        <listener-class>com.netflix.eureka.EurekaBootStrap</listener-class>
      </listener>
    
      <filter>
        <filter-name>statusFilter</filter-name>
        <filter-class>com.netflix.eureka.StatusFilter</filter-class>
      </filter>
      <filter>
        <filter-name>requestAuthFilter</filter-name>
        <filter-class>com.netflix.eureka.ServerRequestAuthFilter</filter-class>
      </filter>
      <filter>
        <filter-name>rateLimitingFilter</filter-name>
        <filter-class>com.netflix.eureka.RateLimitingFilter</filter-class>
      </filter>
      <filter>
        <filter-name>gzipEncodingEnforcingFilter</filter-name>
        <filter-class>com.netflix.eureka.GzipEncodingEnforcingFilter</filter-class>
      </filter>
      <filter>
        <filter-name>jersey</filter-name>
        <filter-class>com.sun.jersey.spi.container.servlet.ServletContainer</filter-class>
        <init-param>
          <param-name>com.sun.jersey.config.property.WebPageContentRegex</param-name>
          <param-value>/(flex|images|js|css|jsp)/.*</param-value>
        </init-param>
        <init-param>
          <param-name>com.sun.jersey.config.property.packages</param-name>
          <param-value>com.sun.jersey;com.netflix</param-value>
        </init-param>
    
        <!-- GZIP content encoding/decoding -->
        <init-param>
          <param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name>
          <param-value>com.sun.jersey.api.container.filter.GZIPContentEncodingFilter</param-value>
        </init-param>
        <init-param>
          <param-name>com.sun.jersey.spi.container.ContainerResponseFilters</param-name>
          <param-value>com.sun.jersey.api.container.filter.GZIPContentEncodingFilter</param-value>
        </init-param>
      </filter>
    
      <filter-mapping>
        <filter-name>statusFilter</filter-name>
        <url-pattern>/*</url-pattern>
      </filter-mapping>
      <filter-mapping>
        <filter-name>requestAuthFilter</filter-name>
        <url-pattern>/*</url-pattern>
      </filter-mapping>
    
      <!-- Uncomment this to enable rate limiter filter.
      <filter-mapping>
        <filter-name>rateLimitingFilter</filter-name>
        <url-pattern>/v2/apps</url-pattern>
        <url-pattern>/v2/apps/*</url-pattern>
      </filter-mapping>
      -->
    
      <filter-mapping>
        <filter-name>gzipEncodingEnforcingFilter</filter-name>
        <url-pattern>/v2/apps</url-pattern>
        <url-pattern>/v2/apps/*</url-pattern>
      </filter-mapping>
      <filter-mapping>
        <filter-name>jersey</filter-name>
        <url-pattern>/*</url-pattern>
      </filter-mapping>
    
      <welcome-file-list>
        <welcome-file>jsp/status.jsp</welcome-file>
      </welcome-file-list>
    </web-app>

上述web.xml的核心一共包含以下部分:

  • EurekaBootStrap: 这个Listener是在web应用启动时执行的,里面包含了Eureka-Server启动时的各种初始化逻辑,位于eureka-core模块中;
  • Filter: 一共包含五个核心Filter:jerseyStatusFilterServerRequestAuthFilterRateLimitingFilterGzipEncodingEnforcingFilter,任何一个请求都会经过这些filter的处理,这个5个filter都在eureka-core模块中;
  • status.jsp: eureka-server的控制台首页页面。

注意:上述配置中,名称为jersey的Filter,这是Jersey框架的一个核心Filter,拦截所有请求交给框架层做处理,类似于Spring MVC中的DispatcherServlet

1.2 启动方式

Eureka-Server的启动比较简单,我们可以将其打成war包放到web容器里,但是这种方式不适合我们在IDE中进行调试。我们可以选择另一种方式:

首先,修改 EurekaClientServerRestIntegrationTest.startServer() 方法:

    private static void startServer() throws Exception {
        server = new Server(8080);
        WebAppContext webAppCtx = new WebAppContext(new File("./eureka-server/src/main/webapp").getAbsolutePath(), "/");
        webAppCtx.setDescriptor(new File("./eureka-server/src/main/webapp/WEB-INF/web.xml").getAbsolutePath());
        webAppCtx.setResourceBase(new File("./eureka-server/src/main/resources").getAbsolutePath());
        webAppCtx.setClassLoader(Thread.currentThread().getContextClassLoader());
        server.setHandler(webAppCtx);
    
        server.start();
    
        eurekaServiceUrl = "http://localhost:8080/v2";
    }

接着,在EurekaClientServerRestIntegrationTest.setUp()方法的最后加入:

    Thread.sleep(Long.MAX_VALUE);

这就是让Eureka-Server启动之后就hang住,然后我们就可以用Eureka-Client去注册它。

二、Eureka-Server启动流程

Eureka-Server的启动类是com.netflix.eureka.EurekaBootStrap,可以看到本质就是个Servlet监听器:

202308072151152971.png

Web容器启动时会自动执行监听器的contextInitialized方法:

    public class EurekaBootStrap implements ServletContextListener {
    
        //...
    
        @Override
        public void contextInitialized(ServletContextEvent event) {
            try {
                // 初始化Eureka-Server运行环境
                initEurekaEnvironment();
                // 初始化Eureka-Server上下文
                initEurekaServerContext();
    
                // 将Eureka-Server上下文保存到ServletContext中,方便后续使用
                ServletContext sc = event.getServletContext();
                sc.setAttribute(EurekaServerContext.class.getName(), serverContext);
            } catch (Throwable e) {
                logger.error("Cannot bootstrap eureka server :", e);
                throw new RuntimeException("Cannot bootstrap eureka server :", e);
            }
        }
    }

2.1 initEurekaEnvironment

initEurekaEnvironment方法会初始化Eureka-Server的配置环境,内部调用了ConfigurationManager这个配置管理器获取Eureka依赖的配置文件信息,其核心就是做了两件事:

  1. 初始化数据中心的配置,如果没有配置的话,就是DEFAULT(这个配置国内几乎没有用);
  2. 初始化Eureka-Server运行环境,如果没有配置的话,默认就设置为test环境;
    protected void initEurekaEnvironment() throws Exception {
        logger.info("Setting the eureka configuration..");
    
        // 数据中心配置,国内基本不会用到这个配置
        String dataCenter = ConfigurationManager.getConfigInstance().getString(EUREKA_DATACENTER);
        if (dataCenter == null) {
            logger.info("Eureka data center value eureka.datacenter is not set, defaulting to default");
            ConfigurationManager.getConfigInstance()
                .setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, DEFAULT);
        } else {
            ConfigurationManager.getConfigInstance()
                .setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, dataCenter);
        }
    
        // 运行环境配置,不同的服务器环境( 例如,PROD / TEST 等) 读取不同的配置文件
        String environment = ConfigurationManager.getConfigInstance().getString(EUREKA_ENVIRONMENT);
        if (environment == null) {
            ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_ENVIRONMENT, TEST);
            logger.info("Eureka environment value eureka.environment is not set, defaulting to test");
        }
    }

ConfigurationManager是一个配置管理器,负责管理所有的配置,采用单例模式实现,位于Netflix Config项目中,感兴趣的读者可以拉取Netflix Config的源码看一下,其实它就是一个经典的 double check+volatile 的双重锁检查实现。后续章节,我将详细分析ConfigurationManager。

2.2 initEurekaServerContext

initEurekaServerContext方法,目的就是对Eureka-Server上下文进行初始化,生成一个初始化完成的EurekaServerContext对象。

由于Eureka-Server同时也是一个Eureka-Client,因为它可能要向其它的eureka server去进行注册,组成一个eureka server的集群。所以Eureka Server把自己也当做是一个Eureka Client,也就是一个服务实例,所以初始化过程会伴随Eureka Client的初始化。

下面是initEurekaServerContext方法的代码,我做了一些简化,省略了无关代码:

    protected void initEurekaServerContext() throws Exception {
        // 代表了Eureka-Server配置,默认会从eureka-server.properties配置文件中的读取
        EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig();
    
        // ...
    
        // 应用信息管理器,用于对应用进行管理,后续我会讲解Application这个概念
        ApplicationInfoManager applicationInfoManager = null;
    
        // 创建eureka客户端
        if (eurekaClient == null) {
            // 应用实例配置,默认会从eureka-client.properties配置文件中的读取
            EurekaInstanceConfig instanceConfig = new MyDataCenterInstanceConfig();
    
            // 构造应用信息管理器
            applicationInfoManager = new ApplicationInfoManager(
                instanceConfig, new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get());
    
            // 代表了Eureka-Client配置,默认会从eureka-client.properties配置文件中的读取
            EurekaClientConfig eurekaClientConfig = new DefaultEurekaClientConfig();
            eurekaClient = new DiscoveryClient(applicationInfoManager, eurekaClientConfig);
        } else {
            applicationInfoManager = eurekaClient.getApplicationInfoManager();
        }
    
        // 创建应用实例信息注册表,后续我会讲解PeerAwareInstanceRegistry这个概念
        PeerAwareInstanceRegistry registry = new PeerAwareInstanceRegistryImpl(
                eurekaServerConfig,
                eurekaClient.getEurekaClientConfig(),
                serverCodecs,
                eurekaClient
        );
    
        // 创建Eureka-Server集群节点集合,,后续我会讲解PeerEurekaNodes这个概念
        PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
            registry,
            eurekaServerConfig,
            eurekaClient.getEurekaClientConfig(),
            serverCodecs,
            applicationInfoManager
        );
    
        // 创建Eureka-Server上下文对象,这个对象提供Eureka-Server内部各组件对象的初始化、关闭、获取等方法
        serverContext = new DefaultEurekaServerContext(
            eurekaServerConfig,
            serverCodecs,
            registry,
            peerEurekaNodes,
            applicationInfoManager
        );
    
        // Eureka-Server上下文持有者,通过它可以很方便的获取到Eureka-Server上下文
        EurekaServerContextHolder.initialize(serverContext);
    
        // 初始化Eureka-Server上下文
        serverContext.initialize();
        logger.info("Initialized server context");
    
        // 从集群中的邻近Eureka-Server拉取注册表信息
        int registryCount = registry.syncUp();
        registry.openForTraffic(applicationInfoManager, registryCount);
    
        // 配合Netflix Servo实现监控信息采集,这个可以忽略
        EurekaMonitors.registerAllStats();
    }

上述代码就是Eureka Server的整体启动流程,其中有一些非常核心的对象,理解它们是后续源码分析的基础,本章我先概述下,后续针对每一个组件详细讲解:

  • ApplicationInfoManager: 应用信息管理器,主要用于对应用实例(InstanceInfo)进行管理,包含实例状态、配置、实例状态变更监听等等;
  • InstanceInfo: 代表一个应用实例,比如我们一个Spring Boot应用ServiceA,它启动后就是一个应用实例。Eureka-Client 可以向 Eureka-Server 注册 该应用实例的信息,注册成功后,就可以被其它Eureka-Client发现了;
  • EurekaInstanceConfig: 应用实例配置,比如应用名称、服务端口、IP等等,默认从eureka-client.properties文件中读取相关配置;
  • EurekaClientConfig: Eureka-Client配置,比如连接的Eureka-Server地址、拉取注册表频率、心跳频率等等,也是默认从eureka-client.properties中读取相关配置项。注意,EurekaClientConfig更侧重Eureka-Client这个角色,这是它与EurekaInstanceConfig主要区别;
  • EurekaServeronfig: Eureka-Server配置,默认从eureka-server.properties文件中读取相关配置;
  • EurekaClient: 代表Eureka客户端,用于与 Eureka-Server 交互,通过EurekaClient可以向 Eureka-Server注册、续约、取消自身应用实例(InstanceInfo),同时查询应用集合(Applications)及应用实例信息(InstanceInfo);
  • PeerAwareInstanceRegistry: 应用实例注册表,同时提供了 Eureka-Server 集群内注册信息的 同步 功能;
  • PeerEurekaNodes: 代表了Eureka-Server 集群节点集合;
  • EurekaServerContext: 代表当前这个Eureka-Server的上下文,提供Eureka-Server 内部各组件对象的 初始化关闭获取 等方法;

整个流程如下图所示,后续章节,我将逐步讲解其中每一个部分:

202308072151157992.png

三、总结

本章,我对Eureka-Server的整体启动流程作了讲解,可以看到eureka-server模块其实就是一个壳,对 eureka-clienteureka-core进行了封装,作为web应用进行部署。

Eureka-Server的核心启动流程就是EurekaBootStrap.contextInitialized,它最终创建一个EurekaServerContext上下文对象,并对其中的核心组件进行了初始化。从下一章开始,我将深入其中的细节,对Eureka-Server启动过程中的各个组件进行详细讲解。

阅读全文