【Spring Cloud】Eureka-Server 源码解读|周末学习

1,028 阅读24分钟

本文已参与周末学习计划,点击链接查看详情 链接

一、前言

看源码:抓大放小,先主流程,再细枝末节。

用技巧连蒙带猜:

  1. 看方法名(英文名)
  2. 看注释
  3. 单元测试入手

eureka 运行的核心的流程:

  1. 服务注册:eureka clienteureka server 注册的过程
  2. 服务发现:eureka clienteureka server 获取注册表的过程
  3. 服务心跳:eureka client 定时往 eureka server 发送续约通知(心跳)
  4. 服务实例摘除:服务下线、故障等
  5. 通信:HTTP
  6. 限流
  7. 自我保护:自动识别是否出现网络故障
  8. server 集群:eureka-server 之间相互注册,多级队列的任务批处理机制

eureka server :提供注册中心的功能。

前提需知:eureka-server 是一个 web 应用,可以打成 war包,在 tomcat 里启动。



(1)源码着手点

源码着手点可分为两点:

  1. eureka-serverbuild.gradle:各种依赖和构建所需的配置
  2. eureka-serverweb.xmlweb 应用最核心之处,定义各种 listenerfilter

1)从 build.gradle 可看出

文件路径:eureka-server/build.gradle

可以看到,依赖的模块有:

  1. eureka-client:在集群模式中,每个 server 也是一个 client,可以互相注册。

  2. eureka-core:注册中心的核心角色,接收服务注册请求,提供服务发现的功能,保持心跳(续约请求),摘除故障服务实例。

  3. jerseyRESTful HTTP 框架,eureka clienteureka server 之间进行通信,都是基于 jersey

    例如:eureka-client-jersey2eureka-core-jersey2

  4. 等等,其他可从 build.gradle 查阅


2)从 web.xml 可看出

文件路径:eureka-server/src/main/webapp/WEB-INF/web.xml

web.xml 可看出:

  1. listener 监听器: 负责 web 应用初始化; 例如,启动后台线程去加载配置文件

    对应 eureka-server 里,就是 com.netflix.eureka.EurekaBootStrap

  2. filter 过滤器:对请求进行处理


核心的 filter 有四个:

当然,每个 filterweb.xml 也定义了相对应的 URL 匹配。

  • StatusFilter:负责状态相关的处理逻辑
  • ServerRequestAuthFilter:授权认证相关的处理逻辑
  • RateLimitingFilter:负责限流相关的处理逻辑
  • GzipEncodingEnforcingFiltergzip,压缩相关的

最后一个是 jerseyServletContainer: 接收所有的请求,作为请求的入口

<filter>
    <filter-name>jersey</filter-name>
    <filter-class>com.sun.jersey.spi.container.servlet.ServletContainer</filter-class>
    ... ...
</filter>

(2)一些概念

读源码准备,Eureka 的一些概念:

  1. Register 服务注册
  2. Renew 服务续约
  3. Fetch Registries 获取服务注册列表信息
  4. Cancel 服务下线
  5. Eviction 服务剔除


二、从源码中学到了什么

凡凡觉得 “学到了” 的东西。

Tips:方法名、变量名日常值得学习。

(1)线程池的使用

学习他人怎么使用线程池,方便以后自己写架构时候,拿来就用。

源码中线程池有:

  1. 调度线程池
  2. 心跳线程池
  3. 缓存刷新线程池
  4. 节点同步注册表线程池

详细解析,如下:

  1. 调度线程池

    // 定位:com.netflix.discovery.DiscoveryClient.java
    private final ScheduledExecutorService scheduler;
    // 支持调度的线程池,核心线程数 2个,使用 google 工具包中线程工厂,后台线程
    scheduler = Executors.newScheduledThreadPool(
        2, new ThreadFactoryBuilder()
        .setNameFormat("DiscoveryClient-%d")
        .setDaemon(true)
        .build());
    
  2. 心跳线程池

    // 定位:com.netflix.discovery.DiscoveryClient.java
    private final ThreadPoolExecutor heartbeatExecutor;
    // 支持心跳的线程池: 核心线程数1个,默认最大线程数5个,且线程存活时间为0,后台线程
    heartbeatExecutor = new ThreadPoolExecutor(
        1, 5, 0, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>(),
        new ThreadFactoryBuilder()
        .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
        .setDaemon(true)
        .build()
    
  3. 缓存刷新线程池

    // 定位:com.netflix.discovery.DiscoveryClient.java
    private final ThreadPoolExecutor cacheRefreshExecutor;
    // 支持缓存刷新的线程池: 核心线程数1个,默认最大线程数5个,且线程存活时间为0,后台线程
    cacheRefreshExecutor = new ThreadPoolExecutor(
        1, 5, 0, TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>(),
        new ThreadFactoryBuilder()
        .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
        .setDaemon(true)
        .build()
    ); 
    
  4. 节点同步注册表线程池

    // 定位:com.netflix.eureka.cluster.PeerEurekaNodes.java
    private ScheduledExecutorService taskExecutor;
    taskExecutor = Executors.newSingleThreadScheduledExecutor(
        new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r, "Eureka-PeerNodesUpdater");
                thread.setDaemon(true);
                return thread;
            }
        }
    }
    

(2)ConcurrentHashMap 使用

// 定位:com.netflix.eureka.registry.ResponseCacheImpl.java
// 1. 只读缓存
// 过期策略:被动过期
private final ConcurrentMap<Key, Value> readOnlyCacheMap = 
    new ConcurrentHashMap<Key, Value>();

// 2. 读写缓存:使用 guava 的 cache
// 过期策略:主动过期
private final LoadingCache<Key, Value> readWriteCacheMap = 
    CacheBuilder.newBuilder()
    .initialCapacity(1000)
    .expireAfterWrite(180, TimeUnit.SECONDS) // 过期时间:默认 180 秒
    .removalListener(new RemovalListener<Key, Value>() {
        @Override
        public void onRemoval(RemovalNotification<Key, Value> notification) {
            Key removedKey = notification.getKey();
            if (removedKey.hasRegions()) {
                Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
                regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
            }
        }
    })
    .build(new CacheLoader<Key, Value>() {
        @Override
        public Value load(Key key) throws Exception {
            if (key.hasRegions()) {
                Key cloneWithNoRegions = key.cloneWithoutRegions();
                regionSpecificKeys.put(cloneWithNoRegions, key);
            }
            Value value = generatePayload(key);
            return value;
        }
    });

(3)循环队列

com.netflix.eureka.registry.AbstractInstanceRegistry.java 里定义两个循环队列。

// 最近注册的队列
private final CircularQueue<Pair<Long, String>> recentRegisteredQueue;
// 最近取消的队列
private final CircularQueue<Pair<Long, String>> recentCanceledQueue;
private class CircularQueue<E> extends ConcurrentLinkedQueue<E> {
    private int size = 0;

    public CircularQueue(int size) {
        this.size = size;
    }
    
    @Override
    public boolean add(E e) {
        this.makeSpaceIfNotAvailable();
        return super.add(e);
    }
    
    private void makeSpaceIfNotAvailable() {
        if (this.size() == size) {
            this.remove();
        }
    }
    
    public boolean offer(E e) {
        this.makeSpaceIfNotAvailable();
        return super.offer(e);
    }
}

(4)锁的使用

锁的使用有:

  1. 读写锁 ReentrantReadWriteLock
  2. synchronized 使用
  3. ReentrantLock 使用

详细使用,如下:

  1. 读写锁 ReentrantReadWriteLock
// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock read = readWriteLock.readLock();
private final Lock write = readWriteLock.writeLock();

public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    try {
        // 读锁
        read.lock();
    } finally {
        // 释放
        read.unlock();
    }
}
  1. synchronized 使用
// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
protected final Object lock = new Object();

public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    synchronized (lock) {
        ... ...
    }
}
  1. ReentrantLock 使用
// 定位:com.netflix.discovery.DiscoveryClient.java
private final Lock fetchRegistryUpdateLock = new ReentrantLock();

// 尝试加锁
if (fetchRegistryUpdateLock.tryLock()) {
    try {
        ... ...
    } finally {
        // 释放锁
        fetchRegistryUpdateLock.unlock();
    }
} else {
    // 获取锁失败,打日志
    logger.warn("Cannot acquire update lock, aborting getAndUpdateDelta");
}

(5)定时器使用

发现 eureka 中大量使用了 Timer 定时器:

  • Timer 属于 JDK 比较早期版本的实现,它可以实现固定周期的任务,以及延迟任务。

  • Timer 会起动一个异步线程去执行到期的任务,任务可以只被调度执行一次,也可以周期性反复执行多次。

Timer 是如何使用的,示例代码如下:

Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
    @Override
    public void run() {
        // do something
    }
}, 10000, 1000);  // 10s 后调度一个周期为 1s 的定时任务

可以看出,任务是由 TimerTask 类实现,TimerTask 是实现了 Runnable 接口的抽象类,Timer 负责调度和执行 TimerTask

接下来看下 Timer 的内部构造:

public class Timer {
    // 小根堆,run操作 O(1)、新增 O(logn)、cancel O(logn)
    private final TaskQueue queue = new TaskQueue();
    // 创建另外线程,任务处理,会轮询 queue
    private final TimerThread thread = new TimerThread(queue);
    public Timer(String name) {
        thread.setName(name);
        thread.start();
    }
}

Timer 它是存在不少设计缺陷的,所以并不推荐用户使用:

  • Timer 是单线程模式。如果某个 TimerTask 执行时间很久,会影响其他任务的调度。

  • Timer 的任务调度是基于系统绝对时间的,如果系统时间不正确,可能会出现问题。

  • TimerTask 如果执行出现异常,Timer 并不会捕获,会导致线程终止,其他任务永远不会执行。



三、直接怼源码

(1)初始化

eureka-server 初始化时,就会触发这个监听器:

eureka-server/src/main/webapp/WEB-INF/web.xml 定义了

<listener>
    <listener-class>com.netflix.eureka.EurekaBootStrap</listener-class>
</listener>

而这个监听器的执行初始化的方法,contextInitialized() 方法,源码如下:

// 定位:eureka-core/src/main/java/com/netflix/eureka/EurekaBootStrap.java
@Override
public void contextInitialized(ServletContextEvent event) {
    try {
        // 1. 初始化环境变量
        initEurekaEnvironment();
        // 2. 初始化上下文
        initEurekaServerContext();

        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);
    }
}

目前源码着手点,可分为两点:

  1. 初始化环境变量 initEurekaEnvironment()

  2. 初始化上下文 initEurekaServerContext()

1)初始化环境变量 initEurekaEnvironment()

主要过程步骤如下:

  1. 创建 ConcurrentCompositeConfiguration :代表了所谓的配置,包括了 eureka 需要的所有的配置

    1.1 在创建时候,先调用了 clear() 方法

    1.2 之后,fireEvent() 发布了一个事件(EVENT_CLEAR

  2. ConcurrentCompositeConfiguration config 实例加入了一堆别配置

    2.1 初始化数据中心的配置,如果没有配置的话,就是 DEFAULT data center

    2.2 初始化 eureka 运行的环境,如果没有配置,默认设置为 test 环境

进入方法,查看对应源码:

// 定位:eureka-core/src/main/java/com/netflix/eureka/EurekaBootStrap.java
protected void initEurekaEnvironment() throws Exception {
    logger.info("Setting the eureka configuration..");

    // 1. 重要的是:
    String dataCenter = ConfigurationManager.getConfigInstance().getString(EUREKA_DATACENTER);
    
    // 2. 其他别的配置
    ... ...
}

进入 ConfigurationManager.getConfigInstance(),可看到:

// 定位:com.netflix.config.ConfigurationManager.java
public static AbstractConfiguration getConfigInstance() {
    if (instance == null) {
        synchronized (ConfigurationManager.class) {
            if (instance == null) {
                instance = getConfigInstance(Boolean.getBoolean(DynamicPropertyFactory.DISABLE_DEFAULT_CONFIG));
            }
        }
    }
    return instance;
}

初始化配置管理器(ConfigurationManager):采用 双检查锁(double check)和volatile单例模式 来实现线程安全。

进入 getConfigInstance() 方法:

// 定位:com.netflix.config.ConfigurationManager.java
private static AbstractConfiguration getConfigInstance(boolean defaultConfigDisabled) {
    if (instance == null && !defaultConfigDisabled) {
        // 1. 创建 ConcurrentCompositeConfiguration
        instance = createDefaultConfigInstance();
        registerConfigBean();
    }
    return instance;
}

private static AbstractConfiguration createDefaultConfigInstance() {
    // 创建主要配置
    ConcurrentCompositeConfiguration config = new ConcurrentCompositeConfiguration();  
    
    // 加载其他配置
    ... ...
        
    return config;
}
// 定位:com.netflix.config.ConcurrentCompositeConfiguration.java
// 1.1. 创建主要配置时,会先清理
public ConcurrentCompositeConfiguration() {
    clear();
}
public final void clear() {
    // 1.2 发布事件
    fireEvent(EVENT_CLEAR, null, null, true);
    
    ... ... 
        
    fireEvent(EVENT_CLEAR, null, null, false);
    ... ...
}

2)初始化上下文 initEurekaServerContext()

主要过程步骤如下:

  1. 第一步:加载 eureka-server 文件配置
  2. 第二步:初始化 eureka-server 内部的 eureka-client(用来跟其他的 eureka-server 节点进行注册和通信)
  3. 第三步:处理注册相关的事情
  4. 第四步:处理 peer 相关节点
  5. 第五步:完成 eureka-server 上下文(context)的构建
  6. 第六步:处理一些善后的事情,从相邻的 eureka 节点拷贝注册信息
  7. 第七步:注册所有监控
protected void initEurekaServerContext() throws Exception {
    // 第一步:加载 eureka-server 文件配置
    EurekaServerConfig eurekaServerConfig = new DefaultEurekaServerConfig();

    // 第二步:初始化 eureka-server 内部的 eureka-client
    //(跟其他的 eureka-server 节点进行注册和通信)
    ApplicationInfoManager applicationInfoManager = null;

    // 第三步:处理注册相关的事情
    PeerAwareInstanceRegistry registry;

    // 第四步:处理 peer 相关节点
    PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes();

    // 第五步:完成 eureka-server 上下文(context)的构建
    serverContext = new DefaultEurekaServerContext();

    // 第六步:处理一些善后的事情,从相邻的 eureka 节点拷贝注册信息
    int registryCount = registry.syncUp();
    registry.openForTraffic(applicationInfoManager, registryCount);

    // 第七步:注册所有监控
    EurekaMonitors.registerAllStats();
}

下面进入主要流程:


第一步:加载 eureka-server 文件配置

加载 eureka-server.properties 的过程:

  1. 创建了一个DefaultEurekaServerConfig 对象,创建时会调用 init() 方法

    DefaultEurekaServerConfig 提供的获取配置项的各个方法,都是通过硬编码的配置项名称,从DynamicPropertyFactory 中获取配置项的值,DynamicPropertyFactory 又是从ConfigurationManager 那儿获取来的,所以也包含了所有配置项的值。

// 1. init()
private void init() {
    // 拿到当前的环境:eureka.environment
    String env = ConfigurationManager.getConfigInstance().getString(
        EUREKA_ENVIRONMENT, TEST);
    // 设置当前属性
    ConfigurationManager.getConfigInstance().setProperty(
        ARCHAIUS_DEPLOYMENT_ENVIRONMENT, env);
    // 加载 eureka 配置文件的名字: eureka-server
    String eurekaPropsFile = EUREKA_PROPS_FILE.get();
    // 2. 加载配置:eureka-server.properties 
    ConfigurationManager.loadCascadedPropertiesFromResources(eurekaPropsFile);
}
  1. eureka-server.properties 中的配置加载到了一个 Properties 对象中,然后将 Properties 对象中的配置放到 ConfigurationManager 中去,此时 ConfigurationManager 中去就有了所有的配置了。

    eureka-server.properties默认没有配置,所以读取的都是默认值。


第二步:初始化 eureka-server 内部的 eureka-client
ApplicationInfoManager applicationInfoManager = null;

if (eurekaClient == null) {
    // 1. 将 eureka-client.properties 配置加载到 ConfigurationManager
    EurekaInstanceConfig instanceConfig = isCloud(ConfigurationManager.getDeploymentContext())
        ? new CloudInstanceConfig()
        : new MyDataCenterInstanceConfig();

    // 2. 这期间会构建 InstanceInfo(服务实例的实例本身信息)
    // 基于 EurekaInstanceConfig 和 InstanceInfo 构建了 ApplicationInfoManager
    // ApplicationInfoManager:是对服务实例进行管理
    applicationInfoManager = new ApplicationInfoManager(
        instanceConfig, new EurekaConfigBasedInstanceInfoProvider(instanceConfig).get());

    // 3. 创建 eureka-client 配置项
    EurekaClientConfig eurekaClientConfig = new DefaultEurekaClientConfig();
    
    // 4. 创建 EurekaClient,DiscoveryClient 是其子类
    eurekaClient = new DiscoveryClient(applicationInfoManager, eurekaClientConfig);
} else {
    applicationInfoManager = eurekaClient.getApplicationInfoManager();
}
  1. 加载服务实例相关配置:将 eureka-client.properties 配置加载到 ConfigurationManager,并提供 EurekaInstanceConfig

    详细可看:PropertiesInstanceConfig

    问题:这里不是初始化 eureka-server 嘛?为什么要加载 eureka-client

    eureka-server 同时也是一个 eureka-client,因为他可能要向其他的 eureka-server 去进行注册,从而组成一个 eureka-server 的集群。

  2. 基于 EurekaInstanceConfigInstanceInfo 构建了 ApplicationInfoManager

  3. 创建 eureka-client 配置项

  4. 重点:创建 EurekaClientDiscoveryClient 是其子类

    // 定位:com.netflix.discovery.DiscoveryClient.java
    DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider) {
        ... ...
    
        // 1. 读取 EurekaClientConfig,包括 TransportConfig
        clientConfig = config;
        transportConfig = config.getTransportConfig();
    
        // 2. 保存 EurekaInstanceConfig 和 InstanceInfo
        instanceInfo = myInfo;
        ... ...
    
        // 3. 是否需要抓取注册表
        if (config.shouldFetchRegistry()) {... ...}
    
        // 3. 是否需要注册到 eureka
        if (config.shouldRegisterWithEureka()) {... ...}
    
        try {
            // 4. 支持调度的线程池,核心线程数 2个,使用 google 工具包中线程工厂
            scheduler = Executors.newScheduledThreadPool(... ...);
    
            // 5. 支持心跳的线程池: 核心线程数1个,默认最大线程数5个,且线程存活时间为0
            heartbeatExecutor = new ThreadPoolExecutor(... ...);
    
            // 6. 支持缓存刷新的线程池: 核心线程数1个,默认最大线程数5个,且线程存活时间为0
            cacheRefreshExecutor = new ThreadPoolExecutor(... ...); 
    
            // 7. 创建 EurekaTransport:
            // 支持底层的 eureka-client 跟 eureka-server 进行网络通信的组件
            eurekaTransport = new EurekaTransport();
            ... ...
        }
        // 8. 抓取注册表
        // 这里代码写的不好,难以懂得
        if (clientConfig.shouldFetchRegistry() && !fetchRegistry(false)) {
            fetchRegistryFromBackup();
        }
    
        // 9. 初始化调度任务
        initScheduledTasks();
        ... ...
    }
    

    步骤如下:

    1. 读取 EurekaClientConfig,包括 TransportConfig
    2. 保存 EurekaInstanceConfigInstanceInfo
    3. 是否要注册以及抓取注册表,如果不要的话,释放一些资源
    4. 支持调度的线程池
    5. 支持心跳的线程池
    6. 支持缓存刷新的线程池
    7. 创建EurekaTransport:支持底层的 eureka-clienteureka-server 进行网络通信的组件
    8. 抓取注册表
    9. 初始化调度任务
      1. 如果要抓取注册表的话,就会注册一个定时任务,按照设定的抓取的间隔(默认是30s),在调度线程池去执行 CacheRefreshThread
      2. 如果要向 eureka-server 进行注册的话,启动定时任务,每隔一定时间发送心跳,执行 HeartbeatThread
      3. 创建了服务实例副本传播器,将自己作为一个定时任务进行调度
      4. 创建了服务实例的状态变更的监听器,如果配置了监听,那么就会注册监听器
第三步:处理注册相关的事情
// 注册表: 感知 eureka-server 集群的服务实例注册表
PeerAwareInstanceRegistry registry;
if (isAws(applicationInfoManager.getInfo())) {
    registry = new AwsInstanceRegistry(... ...);
    ... ...
} else {
    // 实现类
    registry = new PeerAwareInstanceRegistryImpl(
        eurekaServerConfig,
        eurekaClient.getEurekaClientConfig(),
        serverCodecs,
        eurekaClient
    );
}
第四步:处理 peer 相关节点
// PeerEurekaNodes : 代表了eureka server集群
// peers 是多个相同的实例组成的一个集群,peer 是 peers 集群中的一个实例
PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(... ...);

初始化方法上有一行注释,说明了 peer 的意思 :

/**
Initializes Eureka, including syncing up with other Eureka peers and publishing the registry.
*/
第五步:完成 eureka-server 上下文的构建
// 1. 创建上下文
serverContext = new DefaultEurekaServerContext(.. ...);
// 2. 将上下文放到 holder 中,以供其他使用
EurekaServerContextHolder.initialize(serverContext);
// 3. 初始化上下文
serverContext.initialize();

这里重要提一下 serverContext.initialize();

public void initialize() throws Exception {
    logger.info("Initializing ...");
    // 1. 用于更新 eureka-server 集群的信息
    // 让当前的 eureka-server 能感知到其他 eureka-server
    // 原理:后台搞了一个定时调度任务,定时更新 eureka-server 集群信息
    peerEurekaNodes.start();
    // 2. 初始化注册表
    // 将 eureka-server 集群中所有的 eureka-server 的注册表的信息抓取过来,存放到本地的注册表里
    registry.init(peerEurekaNodes);
    logger.info("Initialized");
}
第六步:处理善后的事情,从相邻的 eureka 节点拷贝注册信息
// 从相邻的一个 eureka-server 节点拷贝注册表的信息
// 如果拷贝失败,就找下一个
int registryCount = registry.syncUp();
第七步:注册所有监控
EurekaMonitors.registerAllStats();


(2)eureka-server 如何完成服务注册?

从单元测试入手,一步步看 eurek-server 完成服务注册。

环境准备,如图:

2021-05-3114-02-55.png

eureka-coreeureka-server 模块下: 把 eureka-server 下的 resourceseureka-client.properties 拷贝到 eureka-coreresources

那么就可以愉快的 DEBUG 了,定位代码如下:

// 定位:eureka-core 模块,test包下: 
// com.netflix.eureka.resources.ApplicationResourceTest.java
@Test
public void testGoodRegistration() throws Exception {
    // 1. 生成服务实例
    InstanceInfo noIdInfo = InstanceInfoGenerator.takeOne();
    // 2. 发送 HTTP 请求 - 添加实例
    Response response = applicationResource.addInstance(noIdInfo, false+"");
    assertThat(response.getStatus(), is(204));
}

InstanceInfo,服务实例,里面最主要数据:

  • hostname(主机名)、ip 地址、端口号、url 地址
  • lease(租约)的信息:心跳的间隔时间、最近心跳时间、服务注册时间、服务启动时间

灵魂提问:说白了,注册啥玩意?

  1. 告诉注册中心,client 的信息
  2. 把注册中心的信息同步缓存到本地

主要关心第2步骤:(ApplicationResource.addInstance()

// 定位:
public Response addInstance(InstanceInfo info,
              @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
    // 校验一堆 InstanceInfo 参数
    ... ...

    // 处理数据中心,地区等信息
    ... ...

    // 真正注册,并判断是否是集群模式
    registry.register(info, "true".equals(isReplication));

    // 成功,就返回 HTTP 状态码 204
    return Response.status(204).build();
}

注册,如下代码:

// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
public void register(final InstanceInfo info, final boolean isReplication) {
    int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
    if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
        leaseDuration = info.getLeaseInfo().getDurationInSecs();
    }
    
    // 1. 调用父级方法,进行注册
    super.register(info, leaseDuration, isReplication);
    
    // 2. 向同级注册
    replicateToPeers(Action.Register, info.getAppName(), 
                     info.getId(), info, null, isReplication);
}
  1. 注册 AbstractInstanceRegistry.register()
// 定位:
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    try {
        // 使用读锁,多个服务实例,可以同时来注册
        read.lock();
        
        // 通过 AppName 获取 Map
        Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
       
       ... ...
       
        // 通过 instanceId,从 gMap 中获取服务实例对应的租约
       ... ...
       
    } finally {
        // 释放读锁
        read.unlock();
    }
}

内存注册表,就是:Map<String, Lease<InstanceInfo>>

每个服务的实例信息保存起来



(3)eureka-server 注册表多级缓存机制

代码如下:

// 定位:com.netflix.eureka.resources.ApplicationResource.java
public Response getApplication(...) {
    ... ...

    // 构建缓存 key
    Key cacheKey = new Key(
        Key.EntityType.Application,
        appName,
        keyType,
        CurrentRequestVersion.get(),
        EurekaAccept.fromString(eurekaAccept)
    );

    // 重要:缓存机制
    String payLoad = responseCache.get(cacheKey);
    
    // 返回
}

进入 responseCache.get(cacheKey),代码如下:

// 定位:com.netflix.eureka.registry.ResponseCacheImpl.java
String get(final Key key, boolean useReadOnlyCache) {
    // 重要,获取值
    Value payload = getValue(key, useReadOnlyCache);
    if (payload == null || payload.getPayload().equals(EMPTY_PAYLOAD)) {
        return null;
    } else {
        return payload.getPayload();
    }
}
// 重要:多级缓存
Value getValue(final Key key, boolean useReadOnlyCache) {
    Value payload = null;
    try {
        if (useReadOnlyCache) {
            // 1. 先从只读缓存中获取
            final Value currentPayload = readOnlyCacheMap.get(key);
            if (currentPayload != null) {
                payload = currentPayload;
            } else {
                // 1.2. 只读缓存中没有,从读写缓存中获取
                payload = readWriteCacheMap.get(key);
                // 并将数据放到只读缓存中
                readOnlyCacheMap.put(key, payload);
            }
        } else {
            // 2. 从读写缓存中获取
            payload = readWriteCacheMap.get(key);
        }
    } catch (Throwable t) {
        logger.error("Cannot get value for key :" + key, t);
    }
    return payload;
}

总结下,如图:

多级缓存.png



(4)eureka-server 注册表多级缓存过期机制

过期机制,分为两种:

  1. 主动过期:主动通知过期,服务实例发生注册、下线、故障的时候
  2. 定时过期:
    1. 定时器:定时调度
    2. 过期时效判断

只读缓存和读写缓存过期,详解:

  1. 只读缓存(readOnlyCacheMap

    1. 执行一个定时调度器(TimeTask),默认 30秒。
    2. readOnlyCacheMapreadWriteCacheMap 中的数据进行一个 Hash 比对:
      • 不一致,将readWriteCacheMap 数据同步到 readOnlyCacheMap 中。
// 定位:com.netflix.eureka.registry.ResponseCacheImpl.java
private final java.util.Timer timer = new java.util.Timer("Eureka-CacheFillTimer", true);

if (shouldUseReadOnlyResponseCache) {
    timer.schedule(getCacheUpdateTask(),
   new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)  + responseCacheUpdateIntervalMs),
                   responseCacheUpdateIntervalMs);
}
  1. 读写缓存(readWriteCacheMap

过期分为,两种:

  1. 缓存组件过期,使用 guavacache
// 定位:com.netflix.eureka.registry.ResponseCacheImpl.java
// 1. 读写缓存:使用 guava 的 cache
// 过期策略:主动过期
private final LoadingCache<Key, Value> readWriteCacheMap = 
    CacheBuilder.newBuilder()
    .initialCapacity(1000)
    .expireAfterWrite(180, TimeUnit.SECONDS) // 过期时间:默认 180 秒
   ......
  1. 主动通知:服务实例发生变化,注册、下线、故障的时候
// 例如:注册的时候
// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
    ... ...
    // 服务实例注册信息变化的时候,就要更新这个缓存
    invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
    ... ...
}

总结,如图:

多级缓存过期.png



(5)自动故障感知以及服务实例自动摘除

eureka-server 凭借心跳来感知 eureka-client 是否存活着。

如果在一定的时间内没有收到心跳,那么就认定 eureka-client 已经宕机了,此时会修改对应 client 状态,并摘除。

eureka 想要能定时检测心跳,那么肯定会有一个定时任务,进行判断。

eureka-server 启动就会开启自动感知,代码如下:

// 定位:com.netflix.eureka.EurekaBootStrap.java
// 1. 初始化 eureka时,打开注册表
protected void initEurekaServerContext() throws Exception {
    ... ...
    // 打开注册表,并接收请求
    registry.openForTraffic(applicationInfoManager, registryCount);
    
    ... ...
}

// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
// 2. 
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {

    ... ...
    // 重点:
    super.postInit();
}


// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
// 3. 
protected void postInit() {
    // 1. 启动调度器:每 60s 跑一次,统计一分钟内心跳次数
    renewsLastMin.start();

    ... ...

    // 2. 默认每隔 60s,调度一次:判断服务实例的租约是否过期了
    evictionTimer.schedule(new EvictionTask(), 60, 60);
}

接下来,分析 new EvictionTask() 调度任务:

// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
class EvictionTask extends TimerTask {
    private final AtomicLong lastExecutionNanosRef = new AtomicLong(0l);
    @Override
    public void run() {
        try {
            // 获取补偿时间,compensation:补偿
            long compensationTimeMs = getCompensationTimeMs();
            evict(compensationTimeMs);
        } catch (Throwable e) {
            logger.error("Could not run the evict task", e);
        }
    }
    
    // 1. 获取补偿时间
    // 为什么要补偿时间:可能 JVM GC问题或者系统时钟问题,导致没有刚好 60s 执行任务
    long getCompensationTimeMs() {
        // 先获取当前时间
        long currNanos = getCurrentTimeNano();
        // getAndSet:设置新值,返回旧值
        // 例如:
        // 第一次:10:00:00 ,就会将 10:00:00 设置到 lastExecutionNanosRef 这个里面去
        // 第二次:10:01:00 设置到 lastExecutionNanosRef 去
        long lastNanos = lastExecutionNanosRef.getAndSet(currNanos);
        if (lastNanos == 0l) {
            return 0l;
        }

        // 过去秒数 = 10:01:00 - 10:00:00 = 60s
        long elapsedMs = TimeUnit.NANOSECONDS.toMillis(currNanos - lastNanos);

        // 用这个 60s - 配置的 60s = 0s
        long compensationTime = elapsedMs - serverConfig.getEvictionIntervalTimerInMs();
        return compensationTime <= 0l ? 0l : compensationTime;
    }

    long getCurrentTimeNano() {
        return System.nanoTime();
    }
}

// 2. 驱逐服务实例
public void evict(long additionalLeaseMs) {
    // 是否主动删除掉故障的服务实例
    if (!isLeaseExpirationEnabled()) {
        logger.debug("DS: lease expiration is currently disabled.");
        return;
    }
    // 2.1. 过期服务实例列表,判断是否过期
    List<Lease<InstanceInfo>> expiredLeases = new ArrayList<>();
    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();

                // 租约是否过期
                // 对每个服务实例的租约判断一下,如果一个服务实例上一次的心跳时间到现在为止
                // 超过了 90 × 2 = 180s 的话,才会认为这个服务实例过期了,故障了
                if (lease.isExpired(additionalLeaseMs) && lease.getHolder() != null) {
                    expiredLeases.add(lease);
                }
            }
        }
    }

    // 2.2. 计算摘除任务:不能一次性拆除过多的服务实例
    // 假设现在一共是 20 个服务实例,现在有 6个服务实例不可用了,
    
    ... ...
    // 计算出来:可以摘除 3个

    if (toEvict > 0) {
        // 要摘除的服务实例一共有 6 个,但是最多只能摘除 3个服务实例
        // 在 6个 服务实例中,随机选择3个服务实例来摘除
        Random random = new Random(System.currentTimeMillis());
        for (int i = 0; i < toEvict; i++) {
            // 随机选择
            int next = i + random.nextInt(expiredLeases.size() - i);
            // ... ...

            // 摘除:调用服务下线方法
            internalCancel(appName, id, false);
        }
    }
}

重点,Eureka BUG

判断租约是否过期:isExpired()

// 定位:com.netflix.eureka.lease.Lease.java
/**
Note that due to renew() doing the 'wrong" thing and setting lastUpdateTimestamp to + duration more than what it should be, the expiry will actually be 2 * duration. This is a minor bug and should only affect instances that ungracefully shutdown. Due to possible wide ranging impact to existing usage, this will  not be fixed.
*/
public boolean isExpired(long additionalLeaseMs) {

    // 当前时间是否 > 上一次心跳的时间 + 90s(续约持续时间) + 92s(补偿时间)
    return (evictionTimestamp > 0 || System.currentTimeMillis() > (lastUpdateTimestamp + duration + additionalLeaseMs));
}
// 举例:
// 当前时间:20:02:32
// 判断:20:02:32 > 19:56:20 + 90 + 92s = 19:59:32

// 先不看补偿时间,假如说,当前时间比上次心跳的时间差了超过 90s, 说明,90s之内,都没有更新过心跳
// 就说明那个服务实例在 90s 内没有更新过心跳
// 此时就认为那个服务实例可能已经宕机了

// 19:55:00 -> 如果到了 19:56:30, 没有心跳,就该认为服务实例故障了
// 19:56:30 -> 如果到了 19:56:30, 没有心跳,还不会认为服务实例宕机了
// 20:00:00 -> 19:56:30 + 90s = 19:58:00

// 所以需要等待 2个 90s之后,都没有心跳,才会认为这个服务实例挂掉了,才会下线, 90s,180s
// 3分钟没有心跳,才会认为服务实例宕机了,下线了

// 源码层面有 bug ,90 * 2 = 180s,才会服务实例下线
// 失效多级缓存, 30s 才能同步, 服务30s才会重新抓取增量注册表

// 一个服务实例挂掉之后,可能要过几分钟,4 5分钟,才能让其他的服务感知到


(6)eureka-server 网络故障时自我保护机制

在某个时间段,大量的服务实例过期(没有收到心跳),那么 euerka-server 是否要把这些实例都移除掉呢?

答案是:否 因为,可能是 eureka-server 所在的机器出现了网络故障(网络分区),导致接收不到 client 的心跳。

总图,如下:

服务自我保护机制.png

在剔除服务时候时候,会判断是否开启服务自我保护,代码如下:

// 定位: com.netflix.eureka.registry.AbstractInstanceRegistry.java
public void evict(long additionalLeaseMs) {
    // 是否主动删除掉故障的服务实例
    if (!isLeaseExpirationEnabled()) {
        logger.debug("DS: lease expiration is currently disabled.");
        return;
    }
}

public boolean isLeaseExpirationEnabled() {
    // 默认配置是 true
    if (!isSelfPreservationModeEnabled()) {
        // The self preservation mode is disabled, hence allowing the instances to expire.
        return true;
    }
    // 动态判断是否开启自我保护机制:
    // numberOfRenewsPerMinThreshold 期望心跳数:期望服务实例 1分钟 发送多少心跳
    // getNumOfRenewsInLastMin() 实际心跳数:上1分钟所有服务实例一共发送多少次心跳
    // 期望心跳数 < 实际心跳数:返回true,就可以清理服务实例
    // 期望心跳数 > 实际心跳数:返回false,不清理服务实例
    return numberOfRenewsPerMinThreshold > 0 && getNumOfRenewsInLastMin() > numberOfRenewsPerMinThreshold;
}

感觉这块有些问题,为什么统计所有服务实例的心跳数?如果某个服务频繁发送心跳,那么这个机制不就被破解了?


根据上面代码,仍然有两问题:

  1. 如果计算期望的一分钟心跳次数(numberOfRenewsPerMinThreshold)?
  2. 实际的上一分钟心跳次数是如何计算的?

1)如果计算期望的一分钟心跳次数?

计算期望的一分钟心跳次数,时机有:

  1. 初始化时,给个默认值

  2. 定时调度更新

  3. 服务实例状态发生改变:注册、下线、故障

  4. 初始化时,会给默认值。 EurekaBootStrap 是启动初始化的类,有一行 registry.openForTraffic(开启故障检查)的代码。

// 定位:com.netflix.eureka.EurekaBootStrap.java
protected void initEurekaServerContext() throws Exception {
    ... ...
    // 启动一个定时检查服务实例有没有宕机的后台线程任务
    registry.openForTraffic(applicationInfoManager, registryCount);
    ... ...
}

// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
@Override
public void openForTraffic(ApplicationInfoManager applicationInfoManager, int count) {

    // 心跳 30s 一次, 期望每分钟心跳 = 注册数量 * 2
    this.expectedNumberOfRenewsPerMin = count * 2;
    // 期望每分钟最小心跳 = 期望每分钟心跳 × 2
    this.numberOfRenewsPerMinThreshold =
        (int) (this.expectedNumberOfRenewsPerMin * 0.85);
    ... ...
}

这里硬编码 count * 2 不好。如果心跳间隔修改了,那么这块就是个 BUG。

  1. 定时调度器
// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
private Timer timer = new Timer(
            "ReplicaAwareInstanceRegistry - RenewalThresholdUpdater", true);
private void scheduleRenewalThresholdUpdateTask() {
    // 15分钟跑一次
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            updateRenewalThreshold();
        }
    }, 15 * 60 * 1000, 15 * 60 * 1000);
}

// 操作如下:
private void updateRenewalThreshold() {
    try {
        // 同步,从其他的 eureka-server 拉取注册表
        ... ...
            
        synchronized (lock) {
            // 根据拉取的注册表,再次计算一次
            if ((count * 2) > (serverConfig.getRenewalPercentThreshold() * numberOfRenewsPerMinThreshold)
                || (!this.isSelfPreservationModeEnabled())) {
                this.expectedNumberOfRenewsPerMin = count * 2;
                this.numberOfRenewsPerMinThreshold = (int) ((count * 2) * serverConfig.getRenewalPercentThreshold());
            }
        }
        ... ...
    } catch (Throwable e) {
        ... ...
    }
}
  1. 服务实例状态发生改变:注册、下线、故障
// 1. 注册时
// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {

    ... ...
    synchronized (lock) {
        if (this.expectedNumberOfRenewsPerMin > 0) {
            // 一个服务实例注册一次,就 + 2
            // 表示 30s 一次心跳,统计的是 1分钟
            this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
            // 期望每分钟心跳数 × 0.85
            this.numberOfRenewsPerMinThreshold =
                (int) (this.expectedNumberOfRenewsPerMin * 0.85);
        }
    }
    ... ...
}

// 2. 下线时
// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
public boolean cancel(final String appName, final String id,
                      final boolean isReplication) {
    if (super.cancel(appName, id, isReplication)) {
        // 同步给其他 server
        replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
        synchronized (lock) {
            if (this.expectedNumberOfRenewsPerMin > 0) {
                // 一个服务下线,就 - 2
                this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin - 2;
                this.numberOfRenewsPerMinThreshold =
                    (int) (this.expectedNumberOfRenewsPerMin * 0.85);
            }
        }
        return true;
    }
    return false;
}


// 3. 故障时
// 故障,没有找到,可能又是一个 BUG

2)实际的上一分钟心跳次数是如何计算的?

心跳方法: renew()

// 定位:com.netflix.eureka.registry.AbstractInstanceRegistry.java
private final MeasuredRate renewsLastMin;
public boolean renew(String appName, String id, boolean isReplication) {
    ... ...
    // 心跳次数 + 1
    renewsLastMin.increment();
}

// 定位:com.netflix.eureka.util.MeasuredRate.java
private final AtomicLong lastBucket = new AtomicLong(0);  // 上一分钟统计
private final AtomicLong currentBucket = new AtomicLong(0); // 当前分钟统计
this.timer = new Timer("Eureka-MeasureRateTimer", true);
public synchronized void start() {
    if (!isActive) {
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                try {
                    // 每分钟(60s)调度一次
                    // 保存上一分钟次数,同时更新本次为 0
                    lastBucket.set(currentBucket.getAndSet(0));
                } catch (Throwable e) {
                    logger.error("Cannot reset the Measured Rate", e);
                }
            }
        }, sampleInterval, sampleInterval);

        isActive = true;
    }
}
public void increment() {
    currentBucket.incrementAndGet();
}


(7)集群机制 - 注册表同步以及高可用

eureka-server 可实现集群高可用:是需要相互注册的,然后相互同步服务实例列表。

不同于主从模式,是 peer to peer 模式。

总图,如下:

集群同步.png

注册表同步,时机分为:

  1. eureka-server 初始化时
  2. 有服务注册、下线、心跳时,同步

  1. eureka-server 初始化时

eureka-server 启动时候,会处理 peer 节点,代码如下:

// 定位:com.netflix.eureka.EurekaBootStrap.java
protected void initEurekaServerContext() throws Exception {
    ... ... 
        
    // 第四步: 处理 peer 相关节点
    PeerEurekaNodes peerEurekaNodes = getPeerEurekaNodes(
        registry,
        eurekaServerConfig,
        eurekaClient.getEurekaClientConfig(),
        serverCodecs,
        applicationInfoManager
    );
    ... ... 
    // 第六步:从相邻的一个 eureka-server 节点拷贝注册表的信息
    int registryCount = registry.syncUp();
}

// 定位:com.netflix.eureka.cluster.PeerEurekaNodes.java
// 定时更新 eureka-server 集群信息
public void start() {
    ... ...
    try {
        // 配置文件中的 url 来刷新 eureka-server 列表
        updatePeerEurekaNodes(resolvePeerUrls());
        Runnable peersUpdateTask = new Runnable() {
            @Override
            public void run() {
                try {
                    updatePeerEurekaNodes(resolvePeerUrls());
                } catch (Throwable e) {
                    logger.error("Cannot update the replica Nodes", e);
                }

            }
        };
        // 10 分钟调度一次
        private ScheduledExecutorService taskExecutor;
        taskExecutor.scheduleWithFixedDelay(
            peersUpdateTask,
            serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
            serverConfig.getPeerEurekaNodesUpdateIntervalMs(),
            TimeUnit.MILLISECONDS
        );
    } catch (Exception e) {
        throw new IllegalStateException(e);
    }
    for (PeerEurekaNode node : peerEurekaNodes) {
        logger.info("Replica node URL:  " + node.getServiceUrl());
    }
}

@Override
public int syncUp() {
    // Copy entire entry from neighboring DS node
    int count = 0;
    for (int i = 0; ((i < serverConfig.getRegistrySyncRetries()) && (count == 0)); 
         i++) {
        if (i > 0) {
            try {
                // 如果第一次没有在自己本地的 eureka-client 中获取注册表
                // 说明自己本地的 eureka-client 还没有从任何其他的 eureka-server 上获取注册表
                // 所以此时重试,等待 30 秒
                Thread.sleep(serverConfig.getRegistrySyncRetryWaitMs());
            } catch (InterruptedException e) {
                logger.warn("Interrupted during registry transfer..");
                break;
            }
        }
        // 拿所有注册表,并处理(注册)
        ... ...
    }
    return count;
}

  1. 有服务注册、下线、心跳、故障时,同步
// 1. 注册
// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
public void register(final InstanceInfo info, final boolean isReplication) {
    ... ...
    // 注册
    super.register(info, leaseDuration, isReplication);
    // 向同级同步注册信息
    replicateToPeers(Action.Register, info.getAppName(), info.getId(), 
                     info, null, isReplication);
}


// 2. 下线
@Override
public boolean cancel(final String appName, final String id,
                      final boolean isReplication) {
    if (super.cancel(appName, id, isReplication)) {
        // 向同级同步注册信息
        replicateToPeers(Action.Cancel, appName, id, null, null, isReplication);
        ... ...
        return true;
    }
    return false;
}


// 3. 心跳
public boolean renew(final String appName, final String id, final boolean isReplication) {
    if (super.renew(appName, id, isReplication)) {
        // 向同级同步注册信息
        replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
        return true;
    }
    return false;
}

replicateToPeers() 方法中执行了 action ,如下:

// 定位:com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.java
private void replicateInstanceActionsToPeers(Action action, String appName,
String id, InstanceInfo info, InstanceStatus newStatus,  PeerEurekaNode node) {
    try {
        InstanceInfo infoFromRegistry = null;
        CurrentRequestVersion.set(Version.V2);
        switch (action) {
            case Cancel:
                ... ...
            case Heartbeat:
                ... ...
            case Register:
                ... ...
            case StatusUpdate:
                ... ...
            case DeleteStatusOverride:
                ... ...
        }
    } catch (Throwable t) {
        ... ...
    }
}


(8)集群之间注册表同步- 3层队列任务处理机制

eureka-server 同步任务批处理机制。

重点:

  1. 集群同步的机制eureka-clienteureka-server 发送请求,这个 eureka-server 会将请求同步到其他所有的 eureka-server 上去,其他的 eureka-server仅仅会在自己本地执行(isReplication 判别),不会再次同步了。
  2. 数据同步的异步批处理机制:三个队列,第一个队列(acceptorQueue):就是纯写入;第二个队列(processingOrder):是用来根据时间和大小,来拆分队列;第三个队列(batchWorkQueue):用来放批处理任务(异步批处理机制)

例如,注册,之后会调用 PeerEurekaNode.register(InstanceInfo info)

代码如下:

// 定位:com.netflix.eureka.cluster.PeerEurekaNode.java
private final TaskDispatcher<String, ReplicationTask> batchingDispatcher;
// 1. 接收请求,封装成任务
public void register(final InstanceInfo info) throws Exception {
    long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
    batchingDispatcher.process(
        // 封装成任务
        ... ...
    );
}

// 进入 batchingDispatcher.process()
// 定位:com.netflix.eureka.util.batcher.TaskDispatchers.java
// 2. 处理任务
public static <ID, T> TaskDispatcher<ID, T> createBatchingTaskDispatcher(...) {
        ... ...
        return new TaskDispatcher<ID, T>() {
        @Override
        public void process(ID id, T task, long expiryTime) {
            // 接收队列,纯写入
            acceptorExecutor.process(id, task, expiryTime);
        }

        @Override
        public void shutdown() {
            acceptorExecutor.shutdown();
            taskExecutor.shutdown();
        }
    };
}

// 定位:com.netflix.eureka.util.batcher.AcceptorExecutor.java
// 3. 接收任务,并处理
class AcceptorExecutor<ID, T> {
    ... ...
    // 处理
    void process(ID id, T task, long expiryTime) {
        // 将任务放入队列 
        acceptorQueue.add(new TaskHolder<ID, T>(id, task, expiryTime));
        acceptedTasks++;
    }
    ... ...
}

// 4. 创建后台线程
class AcceptorExecutor<ID, T> {
    private final Thread acceptorThread;
    AcceptorExecutor(...) {
        // 创建线程
        ThreadGroup threadGroup = new ThreadGroup("eurekaTaskExecutors");
        this.acceptorThread = new Thread(threadGroup, 
                                         new AcceptorRunner(), "TaskAcceptor-" + id);
        this.acceptorThread.setDaemon(true);
        this.acceptorThread.start();
    }
}

// 5. 打成一批来处理,默认 3个为一批
class AcceptorExecutor<ID, T> {
    void assignBatchWork() {
        ... ...
        
        batchWorkQueue.add(holders);
        ... ...
    }
}

// 定位:com.netflix.eureka.cluster.PeerEurekaNode.java
// 6. 处理批量请求
ReplicationTaskProcessor taskProcessor = 
    new ReplicationTaskProcessor(targetHost, replicationClient);

总结如图:

三级队列.png