图解+源码讲解 Eureka Server 启动流程分析

1,916 阅读12分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

图解+源码讲解 Eureka Server 启动流程分析

读书是易事,思索是难事,但两者缺一,便全无用处 —— 富兰克林 相关文章
eureka-server 项目结构分析
图解+源码讲解 Eureka Server 启动流程分析
图解+源码讲解 Eureka Client 启动流程分析
图解+源码讲解 Eureka Server 注册表缓存逻辑
图解+源码讲解 Eureka Client 拉取注册表流程
图解+源码讲解 Eureka Client 服务注册流程
图解+源码讲解 Eureka Client 心跳机制流程
图解+源码讲解 Eureka Client 下线流程分析
图解+源码讲解 Eureka Server 服务剔除逻辑
图解+源码讲解 Eureka Server 集群注册表同步机制

从哪里开始启动分析

    eureka-server 项目中的 web.xml
image.png     web项目加载过程顺序如下:

  1. 启动一个WEB项目的时候,WEB容器会去读取它的配置文件web.xml,读取和两个结点。
  2. 紧急着,容创建一个ServletContext(servlet上下文),这个web项目的所有部分都将共享这个上下文。
  3. 容器将转换为键值对,并交给servletContext。
  4. 容器创建中的类实例,创建监听器

    WEB工程加载顺序与元素节点在文件中的配置顺序无关。即不会因为 filter 写在 listener 的前面而会先加载 filter。
WEB容器的加载顺序是:ServletContext -> context-param -> listener -> filter -> servlet 并且这些元素可以配置在文件中的任意位置

<!--因为这里面没有context-param 这个节点所以就读取了listener节点-->
<!--最重要的监听器【在eureka-core里,就是负责eureka-server的初始化的】-->
<listener>
  <listener-class>com.netflix.eureka.EurekaBootStrap</listener-class>
</listener>

核心类 EurekaBootStrap

类结构

image.png
    这个EurekaBootStrap 类实现了 ServletContextListener ,当容器启动的时候就调用 contextInitialized 方法,并且把Servlet的容器事件对象传给EurekaBootStrap

类方法与属性

image.png

入口函数调用流程

    :package com.netflix.eureka;
    :public class EurekaBootStrap implements ServletContextListener
    这个EurekaBootStrap 类实现了 ServletContextListener ,当容器启动的时候就调用 contextInitialized 方法,并且把Servlet的容器事件对象传给EurekaBootStrap,所以从这里开始启动

// 初始化Eureka,包括与其他Eureka同步并发布注册表
@Override
public void contextInitialized(ServletContextEvent event) {
        /** 初始化 eureka 环境变量*/
        initEurekaEnvironment();
        /** 初始化 eureka 容器上下文*/
        initEurekaServerContext();
        /** 获取当前的ServetContext上下文*/
        ServletContext sc = event.getServletContext();
        /** 将 serverContext 设置到 ServletContext 中 */
        sc.setAttribute(EurekaServerContext.class.getName(), serverContext);
}

image.png
     图中的 contextInitialized#initEurekaServerContext(); 方法是比较重要的主要的核心逻辑都在这里面了

初始化环境变量 initEurekaEnvironment()

    1. eureka 服务在启动的时候会调用该方法进行 initEurekaEnvironment()调用该方法时初始化eureka server 的环境,如果没有进行数据中心的配置的话那么就会走默认的环境,并且将该环境设置为测试环境

protected void initEurekaEnvironment() throws Exception {
    /** EUREKA_DATACENTER = "eureka.datacenter" */
    String dataCenter = ConfigurationManager.getConfigInstance().getString(EUREKA_DATACENTER);
    // 如果配置了数据中心那么就走数据中心,没有配置数据中心那么就设置默认的
    if (dataCenter == null) {
        ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, DEFAULT);
    } else {
        ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_DATACENTER, dataCenter);
    }
    // 获取eureka运行环境如果是空的话那么就是设置为测试环境,如果不为空那么就走设置的环境
    String environment = ConfigurationManager.getConfigInstance().getString(EUREKA_ENVIRONMENT);
    if (environment == null) {
        ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_ENVIRONMENT, TEST);
    }
}

初始化 eureka 容器上下文 initEurekaServerContext()

主要做了那些事情

1. 初始化配置文件利用接口的方式

主要功能就是加载eureka-server.properties的文件中的配置

EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig();

    调用了 DefaultEurekaServerConfig#中的init()方法,

/**
 * 服务端配置为 eureka server 提供必备的信息以及一些行为准则,默认从 eureka-serevr.properties 文件加载,
 * 借助 ConfigationManager 管理配置并支持多级配置、配置属性覆盖,同时通过 EurekaServerConfig 接口暴露配访问取能力,
 * 说白了就是 DefaultEurekaServerConfig 这个方法里面把 eureka-serevr.properties 配置文件中的所有信息都读取出来放在了 ConfigurationManager 这个类里面,
 * 因为 DefaultEurekaServerConfig 继承了EurekaServerConfig 所以实现了里面获取配置信息的方法,DefaultEurekaServerConfig 
 * 实际上这里面方法都是通过 ConfigurationManager 这个信息获取的,因为之前都放到了 ConfigurationManager 这里面
 */
private void init() {
    // 如果设置了运行环境那么就获取设置的运行环境否则就是测试环境
    String env = ConfigurationManager.getConfigInstance().getString(EUREKA_ENVIRONMENT, TEST);
    ConfigurationManager.getConfigInstance().setProperty(ARCHAIUS_DEPLOYMENT_ENVIRONMENT, env);
    // 获取 eureka-server服务中配置的 eureka.server.props 属性的名字,默认为 eureka-server
    String eurekaPropsFile = EUREKA_PROPS_FILE.get();
        // 加载到配置信息到 ConfigurationManager 中
    ConfigurationManager.loadCascadedPropertiesFromResources(eurekaPropsFile);
}

2. 构造 Eureka-server 内部的客户端用于向其他服务节点注册

// 初始化 ApplicationInfoManager
ApplicationInfoManager applicationInfoManager = null;
//  初始化eureka内部的一个eureka-client用来和其他的eureka-server进行相互注册和通信的
if (eurekaClient == null) {
    // 默认是测试环境所以不是云环境,所以走 new MyDataCenterInstanceConfig() 这个方法
    // 将 eureka-client.properties 中的配置文件信息加载到 ConfigurationManager 中进行统一管理,
    // 也是实现了EurekaInstanceConfig 接口通过这个接口获取 eureka-client 客户端中的数据
    EurekaInstanceConfig instanceConfig = new MyDataCenterInstanceConfig();
    /**
     * 利用上面初始化好的客户端配置 EurekaInstanceConfig 构造出 InstanceInfo 这个实例
     * 直接基于EurekaInstanceConfig 和 InstnaceInfo,构造了一个ApplicationInfoManager,后面会基于这个ApplicationInfoManager对服务实例进行一些管理
     */
    applicationInfoManager = new ApplicationInfoManager(instanceConfig, new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get());
    /**
     * 通过 DynamicPropertyFactory configInstance 创建 DefaultEurekaTransportConfig 传输配置,并且创建了 eureka-client 配置,通过EurekaClientConfig接口进行访问
     */
    EurekaClientConfig eurekaClientConfig = new DefaultEurekaClientConfig();
    /***
     * 基于ApplicationInfoManager(包含了服务实例的信息、配置,作为服务实例管理的一个组件),eureka client相关的配置,一起构建了一个EurekaClient,
     * 但是构建的时候,用的是EurekaClient的子类,DiscoveryClient
     */
    eurekaClient = new DiscoveryClient(applicationInfoManager, eurekaClientConfig);
} else {
    applicationInfoManager = eurekaClient.getApplicationInfoManager();
}

    此处就是构建了一下 server 内部的客户端 eureka-client 用来和其他的 eureka-server节点进行通信,比如进行节点注册、发送心跳等等【后续会详细写客户端的构造以及启动流程】

3. 构造能感知服务实例的注册表

PeerAwareInstanceRegistry registry;
/**
* 创建一个空的实例注册表,里面包含了一个定时任务(检测改变队列的情况任务放入调度池中,默认是30s执行一次),还有三个队列,
* 一个注册实例的队列、一个下线实例的队列、一个最近改变的队列
*/
registry = new PeerAwareInstanceRegistryImpl(eurekaServerConfig,eurekaClient.getEurekaClientConfig(), serverCodecs,eurekaClient);
/**
* 2.3 PeerEurekaNodes,处理eureka server集群初始化
*/
PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
    registry,eurekaServerConfig,eurekaClient.getEurekaClientConfig(),serverCodecs,applicationInfoManager);

    PeerAwareInstanceRegistry:可以感知eureka server集群的服务实例注册表,eureka client(作为服务实例)过来注册的注册表,而且这个注册表是可以感知到eureka server集群的。假如有一个eureka server集群的话,这里包含了其他的eureka server中的服务实例注册表的信息的

public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry{
    public PeerAwareInstanceRegistryImpl(EurekaServerConfig serverConfig, EurekaClientConfig clientConfig, ServerCodecs serverCodecs, EurekaClient eurekaClient) {
        super(serverConfig, clientConfig, serverCodecs);
    }
}

    看一下这个重要的super方法,创建一个空的实例注册表,里面包含了一个定时任务(检测改变队列的情况任务放入调度池中,默认是30s执行一次),还有三个队列,一个注册实例的队列、一个下线实例的队列、一个最近改变的队列

/**
 * 创建了一个空的实例注册表
 */
protected AbstractInstanceRegistry(EurekaServerConfig serverConfig, EurekaClientConfig clientConfig, ServerCodecs serverCodecs) {
    this.serverConfig = serverConfig;
    this.clientConfig = clientConfig;
    this.serverCodecs = serverCodecs;
    // 最近取消的队列
    this.recentCanceledQueue = new CircularQueue<Pair<Long, String>>(1000);
    // 最近注册的队列
    this.recentRegisteredQueue = new CircularQueue<Pair<Long, String>>(1000);
    // 续约最大分钟
    this.renewsLastMin = new MeasuredRate(1000 * 60 * 1);
    /**
     * 检测改变队列的情况任务放入调度池中
     * serverConfig.getDeltaRetentionTimerIntervalInMs() 默认是30s执行一次
     */
    this.deltaRetentionTimer.schedule(getDeltaRetentionTask(), serverConfig.getDeltaRetentionTimerIntervalInMs(), serverConfig.getDeltaRetentionTimerIntervalInMs());
}

    定时任务的主要作用细节,检测增量任务,最近有变化的服务实例,比如说,新注册、下线的,或者是别的什么什么,在Registry构造的时候,有一个定时调度的任务,默认是30秒一次,看一下,服务实例的变更记录,是否在队列里停留了超过180s(3分钟),如果超过了3分钟,就会从队列里将这个服务实例变更记录给移除掉。也就是说,这个queue,就保留最近3分钟的服务实例变更记录。

private TimerTask getDeltaRetentionTask() {
    return new TimerTask() {
        @Override
        public void run() {
            Iterator<RecentlyChangedItem> it = recentlyChangedQueue.iterator();
            while (it.hasNext()) {
                /**
                 * 如果系统当前时间 - 配置的180s 大于最后一次的更新时间那么就将该实例移除
                 */
                if (it.next().getLastUpdateTime() < System.currentTimeMillis() - serverConfig.getRetentionTimeInMSInDeltaQueue()) {
                    it.remove();
                } else {
                    break;
                }
            }
        }

    };
}

4. 构造上下文,进行上下文初始化

/**
 * 2.3 创建了 PeerEurekaNodes 对象,处理 eureka server 集群初始化需要的配置信息
 */
PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
        registry,// 注册表
        eurekaServerConfig, // 服务端配置
        eurekaClient.getEurekaClientConfig(), // 客户端配置
        serverCodecs,applicationInfoManager// 应用管理器
);

/**
 * 3. 将上面构造好的所有的东西,都一起来构造一个EurekaServerContext,
 * 代表了当前这个eureka server的一个服务器上下文,包含了服务器需要的所有的东西。
 */
serverContext = new DefaultEurekaServerContext(
        eurekaServerConfig, // 服务端配置
        serverCodecs,
        registry,// 注册表
        peerEurekaNodes,// 上面创建好的 peerEurekaNodes 对象信息
        applicationInfoManager); // 应用管理器
/***
 * 将这个东西放在了一个 EurekaServerContextHolder 中,以后谁如果要使用这个EurekaServerContext,直接从这个holder中获取就可以了。
 */
EurekaServerContextHolder.initialize(serverContext);
/**
 *4. EurekaServerContext 初始化构造出来的 server 上下文
 */
serverContext.initialize();// 重要方法

    主要是这个服务上下文的初始化方法,看看里面都做了什么东西

public void initialize() {
    logger.info("Initializing ...");
    /**
     * 这里呢,就是将eureka server集群给启动起来,其实就是更新一下eureka server集群的信息,
     * 让当前的eureka server感知到所有的其他的eureka server。然后搞一个定时调度任务,就一个后台线程,每隔一定的时间,更新eureka server集群的信息
     */
    peerEurekaNodes.start();
    /**
     * 基于eureka server集群的信息,来初始化注册表,肯定是将eureka server集群中所有的eureka server的注册表的信息,都抓取过来,
     * 放到自己本地的注册表里去,跟eureka server集群之间的注册表信息互换有关联的
     */
    registry.init(peerEurekaNodes);
    logger.info("Initialized");
}

    peerEurekaNodes.start()上面构造好的 PeerEurekaNodes 里面的start 方法,像是开启了一个守护线程,进行服务列表的地址的更新

public void start() {
    // 创建了一个任务执行器
    taskExecutor = Executors.newSingleThreadScheduledExecutor("Eureka-PeerNodesUpdater"...);
    /**
     * 先解析配置文件中的其他eureka server的url地址,其实就是做了url服务地址的更新,删除旧的,添加新的
     */
    updatePeerEurekaNodes(resolvePeerUrls());
    // 启动一个后台的线程,默认是每隔10分钟,就是基于配置文件中的 url 来刷新 eureka server 列表
    Runnable peersUpdateTask = new Runnable() {
        @Override
        public void run() {
            updatePeerEurekaNodes(resolvePeerUrls());
        }
    };
    //serverConfig.getPeerEurekaNodesUpdateIntervalMs() 默认是 10 * 60 * 1000 = 10分钟
    taskExecutor.scheduleWithFixedDelay(peersUpdateTask, serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
                                        serverConfig.getPeerEurekaNodesUpdateIntervalMs(), 
                                        TimeUnit.MILLISECONDS);
    for (PeerEurekaNode node : peerEurekaNodes) {
        logger.info("Replica node URL:  {}", node.getServiceUrl());
    }
}

    registry.init(peerEurekaNodes),注册中心初始化的方法,看看里面都做了什么

@Override
public void init(PeerEurekaNodes peerEurekaNodes) throws Exception {
    this.peerEurekaNodes = peerEurekaNodes;
    // 初始化缓存
    initializedResponseCache();
    // 续约相关
    scheduleRenewalThresholdUpdateTask();
    ...
}

    初始化缓存的这个方法还是比较重要的,initializedResponseCache(),这里面用了Map缓存机制,比如读写Map缓存以及只读Map缓存等机制进行缓存的,后面会详细讲解的这块内容比较多

@Override
public synchronized void initializedResponseCache() { // synchronized 进行了修饰
    if (responseCache == null) {
        responseCache = new ResponseCacheImpl(serverConfig, serverCodecs, this);
    }
}

5. 从其他 eureka-server 节点进行获取服务注册列表

    在eureka-server启动初始化的时候,当前这个eureka-server会从任何一个其他的eureka-server拉取注册表过来放在自己本地,作为初始的注册表。将自己作为一个eureka client,找任意一个eureka server来拉取注册表,将拉取到的注册表放到自己本地去

int registryCount = registry.syncUp();

@Override
public int syncUp() {
    // Copy entire entry from neighboring DS node
    int count = 0;
    /**
     * serverConfig.getRegistrySyncRetries() 默认是5 最多可以重试5次
     */
    for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); i++) {
        if (i > 0) {
            // serverConfig.getRegistrySyncRetryWaitMs() 默认是 30s,先睡30 s
            Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
        }
        /**
         * 获取所有实例信息
         */
        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;
}

    register(instance, instance.getLeaseInfo().getDurationInSecs(), true) 这个是主要的注册到自己本地缓存的方法,后续会讲解的,因为这个方法也很复杂,涉及到获取实例配置信息接口访问等操作

6. 定时剔除没有心跳的服务

    自动检查服务实例是否故障宕机的入口,后续也会拿出一篇文章进行 讲解

registry.openForTraffic(applicationInfoManager, registryCount);

7. 注册监控器

/**
 * 跟eureka自身的监控机制相关联的
 */
EurekaMonitors.registerAllStats();

    到此 eureka 服务端初始化就完成了

小结

  1. 初始化eureka环境变量、以及配置数据中心 initEurekaEnvironment()
  2. 初始化eureka server 服务端配置,创建了DefaultEurekaServerConfig对象,该对像里面有一个init方法,该方法就是将配置好的eureka-server.properties中的配置文件加载到ConfigurationManager中,方便于EurekaServerConfig进行通过接口的方式获取配置文件中的信息
  3. 初始化客户端,通过 new MyDataCenterInstanceConfig() 这个对象将eureka-client.properties中的配置属性加载到ConfigurationManager中去,并通过EurekaInstanceConfig接口进行访问,因为 MyDataCenterInstanceConfig 实现了 EurekaInstanceConfig接口,并通过 DiscoveryClient 创建好了客户端
  4. 创建服务注册表(PeerAwareInstanceRegistry)、创建eureka server 集群(PeerEurekaNodes),通过这两个对象以及上面创建的eurekaServerConfig、applicationInfoMaager以及编码方式共同创建了EurekaServerContext,代表了当前这个eureka server的一个服务器上下文中的所有东西,将这个东西放在了一个 EurekaServerContextHolder 中,以后谁如果要使用这个EurekaServerContext,直接从这个holder中获取就可以了,之后进行initialize()操作,比如抓取注册表等操作
  5. 初始eureka 服务端上下文操作
  6. 从相邻的一个eureka server节点拷贝注册表的信息
  7. 注册eureka自身的监控机制相关联的