SpringCloud 源码系列(5)— 注册中心Eureka 之 EurekaServer集群

1,379 阅读27分钟

系列文章:

SpringCloud 源码系列(1)— 注册中心Eureka 之 启动初始化

SpringCloud 源码系列(2)— 注册中心Eureka 之 服务注册、续约

SpringCloud 源码系列(3)— 注册中心Eureka 之 抓取注册表

SpringCloud 源码系列(4)— 注册中心Eureka 之 服务下线、故障、自我保护机制

Eureka Server 集群

在实际的生产环境中,可能有几十个或者几百个的微服务实例,Eureka Server 承担了非常高的负载,而且为了保证注册中心高可用,一般都要部署成集群的,下面就来看看 eureka server 的集群。

搭建 Eureka Server 集群

首先来搭建一个三个节点的 eureka-server 集群,看看效果。

1、集群配置

首先在本地 hosts 文件中配置如下映射:

127.0.0.1 peer1
127.0.0.1 peer2
127.0.0.1 peer3

更改注册中心的 application.yml 配置文件,增加三个 profile,分别对应三个 eureka-server 的客户端配置。

eureka-server 在集群中作为客户端就需要抓取注册表,并配置 eureka-server 的地址。

spring:
  application:
    name: sunny-register

---
spring:
  profiles: peer1
server:
  port: 8001

eureka:
  instance:
    hostname: peer1
  client:
    # 是否向注册中心注册自己
    register-with-eureka: false
    # 是否抓取注册表
    fetch-registry: true
    service-url:
      defaultZone: http://peer1:8001/eureka,http://peer2:8002/eureka,http://peer3:8003/eureka


---
spring:
  profiles: peer2
server:
  port: 8002

eureka:
  instance:
    hostname: peer2
  client:
    # 是否向注册中心注册自己
    register-with-eureka: false
    # 是否抓取注册表
    fetch-registry: true
    service-url:
      defaultZone: http://peer1:8001/eureka,http://peer2:8002/eureka,http://peer3:8003/eureka

---
spring:
  profiles: peer3
server:
  port: 8003

eureka:
  instance:
    hostname: peer3
  client:
    # 是否向注册中心注册自己
    register-with-eureka: false
    # 是否抓取注册表
    fetch-registry: true
    service-url:
      defaultZone: http://peer1:8001/eureka,http://peer2:8002/eureka,http://peer3:8003/eureka

2、启动集群

分别启动三个注册中心,环境变量 spring.profiles.active 激活对应的集群配置。

启动之后访问 http://peer1:8001/ 进入 peer1 这个注册中心,就可以看到另外两个分片 peer2、peer3,说明集群中有3个节点了。

3、启动客户端

首先客户端配置增加集群地址:

eureka:
  client:
    serviceUrl:
      defaultZone: http://peer1:8001/eureka,http://peer2:8002/eureka,http://peer3:8003/eureka

启动几个客户端实例,过一会 之后,会发现三个 eureka-server 上都注册上去了:

到此 eureka-server 集群就搭建起来了,可以看到注册中心的实例会互相同步,每隔注册注册都可以接收注册、续约、下线等请求,它们是对等的。

Eureka Server 集群架构

一般来说,分布式系统的数据在多个副本之间的复制方式,可分为主从复制和对等复制

1、主从复制

主从复制就是 Master-Slave 模式,即一个主副本,其它副本都为从副本。所有对数据的写操作都提交到主副本,然后再由主副本同步到从副本。

对于主从复制模式来说,写操作的压力都在主副本上,它是整个系统的瓶颈,而从副本则可以帮助主副本分担读请求。

2、对等复制

对等复制就是 Peer to Peer 的模式,副本之间不分主从,任何副本都可以接收写操作,每个副本之间相互进行数据更新同步。

Peer to Peer 模式每个副本之间都可以接收写请求,不存在写操作压力瓶颈。但是由于每个副本都可以进行写操作,各个副本之间的数据同步及冲突处理是一个棘手的问题。

3、Eureka Server 集群架构

Eureka Server 采用的就是 Peer to Peer 的复制模式,比如一个客户端实例随机向其中一个server注册,然后它就会同步到其它节点中。

Eureka Server 启动时抓取注册表

前面已经分析过了,在 eureka server 启动初始化的时候,即 EurekaBootStrap 初始化类,先初始化了 DiscoveryClient,DiscoveryClient 会向注册中心全量抓取注册表到本地。

初始化的最后调用了 registry.syncUp() 来同步注册表,就是将 DiscoveryClient 缓存的实例注册到 eureka-server 的注册表里去。

需要注意的是 eureka 配置的注册表同步重试次数默认为 5,springcloud 中默认为 0,因此需要添加如下配置来开启注册表同步。

eureka:
  server:
    registry-sync-retries: 5

DiscoveryClient 本地的实例注册到注册表中:

集群节点同步

1、注册、续约、下线

前面也分析过了,在客户端注册、续约、下线的时候,都会同步到集群其它节点。可以看到都调用了 replicateToPeers 方法来复制到其它集群。

/////////////////////// 注册 ///////////////////////
public void register(final InstanceInfo info, final boolean isReplication) {
    int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
    // 如果实例中没有周期的配置,就设置为默认的 90 秒
    if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
        leaseDuration = info.getLeaseInfo().getDurationInSecs();
    }
    // 注册实例
    super.register(info, leaseDuration, isReplication);
    // 复制到集群其它 server 节点
    replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
}

/////////////////////// 下线 ///////////////////////
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);
        return true;
    }
    return false;
}

/////////////////////// 续约 ///////////////////////
public boolean renew(final String appName, final String id, final boolean isReplication) {
    // 调用父类(AbstractInstanceRegistry)的 renew 续约
    if (super.renew(appName, id, isReplication)) {
        // 续约完成后同步到集群其它节点
        replicateToPeers(Action.Heartbeat, appName, id, null, null, isReplication);
        return true;
    }
    return false;
}

2、同步到其它节点

来看看 replicateToPeers 方法:

  • 首先判断 isReplication 参数,如果是集群复制操作,最近一分钟复制次数 numberOfReplicationsLastMin + 1。isReplication 是在请求头中指定的,请求头为 PeerEurekaNode.HEADER_REPLICATION(x-netflix-discovery-replication)
  • 接着遍历集群列表,复制实例操作到集群节点中。前面也分析过了,PeerEurekaNode 就代表了一个 eureka-server,PeerEurekaNodes 就代表了 eureka-server 集群。
  • 复制实例操作到集群的方法 replicateInstanceActionsToPeers 就是根据不同的操作类型调用集群 PeerEurekaNode 对应的方法完成操作复制。
private void replicateToPeers(Action action, String appName, String id,
                              InstanceInfo info /* optional */,
                              InstanceStatus newStatus /* optional */, boolean isReplication) {
    Stopwatch tracer = action.getTimer().start();
    try {
        if (isReplication) {
            // 如果是来自其它server节点的注册请求,则最近一分钟集群同步次数+1
            numberOfReplicationsLastMin.increment();
        }
        // If it is a replication already, do not replicate again as this will create a poison replication
        if (peerEurekaNodes == Collections.EMPTY_LIST || isReplication) {
            return;
        }

        // 如果是来自客户端的注册请求,就同步到集群中其它server节点
        for (final PeerEurekaNode node : peerEurekaNodes.getPeerEurekaNodes()) {
			// 同步到集群其它节点
            replicateInstanceActionsToPeers(action, appName, id, info, newStatus, node);
        }
    } finally {
        tracer.stop();
    }
}

private void replicateInstanceActionsToPeers(
		Action action, String appName, String id, InstanceInfo info, InstanceStatus newStatus, PeerEurekaNode node {
    try {
        InstanceInfo infoFromRegistry;
        switch (action) {
            case Cancel:
                // 下线
                node.cancel(appName, id);
                break;
            case Heartbeat:
                // 续约
                node.heartbeat(appName, id, infoFromRegistry, overriddenStatus, false);
                break;
            case Register:
                // 注册
                node.register(info);
                break;
            case StatusUpdate:
            	// 更新状态
                node.statusUpdate(appName, id, newStatus, infoFromRegistry);
                break;
            case DeleteStatusOverride:
            	// 删除覆盖状态
                node.deleteStatusOverride(appName, id, infoFromRegistry);
                break;
        }
    } catch (Throwable t) {
        logger.error("Cannot replicate information to {} for action {}", node.getServiceUrl(), action.name(), t);
    }
}

3、isReplication

PeerEurekaNode 与 eureka-server 通信的组件是 JerseyReplicationClient,这个类重写了 addExtraHeaders 方法,并添加了请求头 PeerEurekaNode.HEADER_REPLICATION,设置为 true。

这样其它 eureka-server 收到这个复制操作后,就知道是来自集群节点的同步操作,就不会再同步给其它节点了,从而避免死循环。

@Override
protected void addExtraHeaders(Builder webResource) {
    webResource.header(PeerEurekaNode.HEADER_REPLICATION, "true");
}

集群同步机制

Eureka Server 集群间同步机制还是比较复杂的,试想如果每次客户端的请求一过来,比如注册、心跳,然后 eureka-server 就立马同步给集群中其它 server 节点,那 eureka-server 这种 Peer to Peer 的模式实际上就无法分担客户端的写操作压力,相当于每个 eureka-server 接收到的请求量都是一样的。那 eureka server 为了避免这种情况,底层采用了三层队列,加批量任务的方式来进行集群间的同步。简单来说就是先将客户端操作放入队列中,然后从队列中取出一批操作,然后将这一批操作发送给其它 Server 节点,Server节点接收到之后再将这批操作解析到本地。下面就来详细看看是如何实现的。

集群节点 PeerEurekaNode

之前分析 eureka-server 启动初始化的时候,EurekaBootStrap 初始化了代表集群的 PeerEurekaNodes,它里面又根据配置的注册中心地址构造了 PeerEurekaNode,集群间同步核心的组件就是这个 PeerEurekaNode 了。下面以客户端注册为例来看下是如何同步的。

1、注册同步

replicateInstanceActionsToPeers 中调用了 PeerEurekaNode 的 register 方法来同步注册操作到集群。

node.register 方法:

  • 可以看到先计算了过期时间,为当前时间 + 租约间隔时间(默认90秒)
  • 然后调用了 batchingDispatcher 批量任务分发器来处理任务,提交了一个 InstanceReplicationTask 的实例,其 execute 方法中调用了 replicationClient 来向这个 server 注册同步。
public void register(final InstanceInfo info) throws Exception {
    // 过期时间:当前时间 + 租约时间(默认90秒)
    long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
    batchingDispatcher.process(
            taskId("register", info),
            new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
                public EurekaHttpResponse<Void> execute() {
                	// 同步到Server
                    return replicationClient.register(info);
                }
            },
            expiryTime
    );
}

再看下 getLeaseRenewalOf 这个方法,这里应该是有bug的,这个方法返回的是毫秒数,可以看到它的卫语句的else部分是乘以 1000 了的,而 if 部分则没有,返回的是 90,不过这里 info.getLeaseInfo() 应该都不会为 null。

private static int getLeaseRenewalOf(InstanceInfo info) {
    // bug : Lease.DEFAULT_DURATION_IN_SECS * 1000
    return (info.getLeaseInfo() == null ? Lease.DEFAULT_DURATION_IN_SECS : info.getLeaseInfo().getRenewalIntervalInSecs()) * 1000;
}

2、PeerEurekaNode 的构造

batchingDispatcher 是在 PeerEurekaNode 的构造方法中初始化的,来看下它的构造方法:

  • registry:本地注册表
  • targetHost:eureka-server host
  • replicationClient:基于 jersey 的集群复制客户端通信组件,它在请求头中设置了 PeerEurekaNode.HEADER_REPLICATION 为 true
  • serviceUrl:eureka-server 地址
  • maxProcessingDelayMs:最大处理延迟毫秒数,默认为30000毫秒,即30秒,在下线的时候有用到
  • batcherName:批处理器名称
  • taskProcessor:复制任务处理器,它封装了 targetHost 和 replicationClient,主要就是 ReplicationTaskProcessor 在处理批量任务的提交
  • batchingDispatcher:批量任务分发器,它会将任务打成一个批次提交到 eureka-server,避免多次请求eureka-server,注册时就是先用这个分发器提交的任务
  • nonBatchingDispatcher:非批量任务分发器,就是一个任务一个任务的提交
public PeerEurekaNode(PeerAwareInstanceRegistry registry, String targetHost, String serviceUrl, HttpReplicationClient replicationClient, EurekaServerConfig config) {
    this(registry, targetHost, serviceUrl, replicationClient, config, BATCH_SIZE, MAX_BATCHING_DELAY_MS, RETRY_SLEEP_TIME_MS, SERVER_UNAVAILABLE_SLEEP_TIME_MS);
}

PeerEurekaNode(PeerAwareInstanceRegistry registry, String targetHost, String serviceUrl,
                                 HttpReplicationClient replicationClient, EurekaServerConfig config,
                                 int batchSize, long maxBatchingDelayMs,
                                 long retrySleepTimeMs, long serverUnavailableSleepTimeMs) {
    this.registry = registry;
    // 集群节点 host
    this.targetHost = targetHost;
    this.replicationClient = replicationClient;

    // 集群节点地址
    this.serviceUrl = serviceUrl;
    this.config = config;
    // 最大延迟时间 默认30秒
    this.maxProcessingDelayMs = config.getMaxTimeForReplication();

    // 批处理器名称
    String batcherName = getBatcherName();

    // 复制任务处理器
    ReplicationTaskProcessor taskProcessor = new ReplicationTaskProcessor(targetHost, replicationClient);
    // 批量任务分发器
    this.batchingDispatcher = TaskDispatchers.createBatchingTaskDispatcher(
            batcherName,
            // 复制池里最大容量,默认 10000
            config.getMaxElementsInPeerReplicationPool(),
            batchSize, // 250
            // 同步使用的最大线程数 默认 20
            config.getMaxThreadsForPeerReplication(),
            maxBatchingDelayMs, // 500
            serverUnavailableSleepTimeMs, // 1000
            retrySleepTimeMs, // 100
            taskProcessor
    );
    // 单个任务分发器
    this.nonBatchingDispatcher = TaskDispatchers.createNonBatchingTaskDispatcher(
            targetHost,
            config.getMaxElementsInStatusReplicationPool(),
            config.getMaxThreadsForStatusReplication(),
            maxBatchingDelayMs,
            serverUnavailableSleepTimeMs,
            retrySleepTimeMs,
            taskProcessor
    );
}

批量分发器 TaskDispatcher

创建 batchingDispatcher 时调用了 TaskDispatchers.createBatchingTaskDispatcher 方法来创建。

首先看下 createBatchingTaskDispatcher 的参数及默认值,后面分析代码的时候会用到这些参数:

  • id:批量分发器的名称
  • maxBufferSize:缓存池最大数量,默认 10000
  • workloadSize:工作负载数量,即一个批次最多多少任务,默认 250
  • workerCount:工作者数量,这个是线程池线程工作线程的数量,默认20
  • maxBatchingDelay:批量任务最大延迟毫秒数,默认为 500 毫秒
  • congestionRetryDelayMs:阻塞重试延迟毫秒数,默认为 1000 毫秒
  • networkFailureRetryMs:网络失败重试延迟毫秒数,默认为 100 毫秒
  • taskProcessor:任务处理器,即 ReplicationTaskProcessor

再看下这个方法:

  • 首先创建了一个接收者执行器 AcceptorExecutor,主要的参数是缓存、时间相关的
  • 再创建了一个任务处理器 TaskExecutors,主要的参数是工作线程数、任务处理器以及接收者执行器,可以猜测这应该就是最终执行批量任务提交的执行器
  • 最后创建了任务分发器 TaskDispatcher,从它的 process 方法可以看出,分发器提交的任务实际上又提交给了 AcceptorExecutor

从这里可以知道,前面注册时 batchingDispatcher.process() 提交的任务其实就是分发到 acceptorExecutor 这个接收者执行器了。创建的这个分发器 TaskDispatcher 主要有接收者执行器 AcceptorExecutor 和 任务处理器 TaskExecutors 这两个组件,核心的分发功能就在这两个组件中。

public static <ID, T> TaskDispatcher<ID, T> createBatchingTaskDispatcher(String id, int maxBufferSize, int workloadSize,
                                                                         int workerCount, long maxBatchingDelay, long congestionRetryDelayMs,
                                                                         long networkFailureRetryMs, TaskProcessor<T> taskProcessor) {
    // 接收者执行器 AcceptorExecutor
    final AcceptorExecutor<ID, T> acceptorExecutor = new AcceptorExecutor<>(
            id, maxBufferSize, workloadSize, maxBatchingDelay, congestionRetryDelayMs, networkFailureRetryMs
    );

    // 任务处理器 TaskExecutors, workerCount = 20
    final TaskExecutors<ID, T> taskExecutor = TaskExecutors.batchExecutors(id, workerCount, taskProcessor, acceptorExecutor);

    return new TaskDispatcher<ID, T>() {
        @Override
        public void process(ID id, T task, long expiryTime) {
            // 任务由 acceptorExecutor 处理
            acceptorExecutor.process(id, task, expiryTime);
        }

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

接收者执行器 AcceptorExecutor

先看下创建 AcceptorExecutor 的构造方法:

  • 根据 congestionRetryDelayMs、networkFailureRetryMs 创建了一个时间调整器 TrafficShaper,应该主要就是用来调整补偿时间的
  • 然后创建了一个后台线程 acceptorThread,它运行的任务是 AcceptorRunner,主要就是将任务转成批量任务的
  • 最后就是注册了一些监控统计之类的
AcceptorExecutor(String id, int maxBufferSize, int maxBatchingSize, long maxBatchingDelay, long congestionRetryDelayMs, long networkFailureRetryMs) {
    // 批处理器名称
    this.id = id;
    // 最大缓冲数:10000
    this.maxBufferSize = maxBufferSize;
    // 每批最大数量:250
    this.maxBatchingSize = maxBatchingSize;
    // 最大延迟时间:500 ms
    this.maxBatchingDelay = maxBatchingDelay;
    // 时间调整器
    // congestionRetryDelayMs 阻塞重试延迟时间,1000ms
    // networkFailureRetryMs 网络异常重试时间,100ms
    this.trafficShaper = new TrafficShaper(congestionRetryDelayMs, networkFailureRetryMs);

    // 接收者后台处理线程
    ThreadGroup threadGroup = new ThreadGroup("eurekaTaskExecutors");
    this.acceptorThread = new Thread(threadGroup, new AcceptorRunner(), "TaskAcceptor-" + id);
    this.acceptorThread.setDaemon(true);
    this.acceptorThread.start();

    // 监控统计相关
    final double[] percentiles = {50.0, 95.0, 99.0, 99.5};
    final StatsConfig statsConfig = new StatsConfig.Builder()
            .withSampleSize(1000)
            .withPercentiles(percentiles)
            .withPublishStdDev(true)
            .build();
    final MonitorConfig config = MonitorConfig.builder(METRIC_REPLICATION_PREFIX + "batchSize").build();
    this.batchSizeMetric = new StatsTimer(config, statsConfig);
    try {
        Monitors.registerObject(id, this);
    } catch (Throwable e) {
        logger.warn("Cannot register servo monitor for this object", e);
    }
}

然后看看 AcceptorExecutor 的属性,它定义了几个队列以及容器来处理批量任务,我们先知道有这些东西,后面再来看看都怎么使用的。

然后可以看到 AcceptorExecutor 大量使用了并发包下的一些类,以及队列的特性,这里我们需要了解下这些类的特性:

  • LinkedBlockingQueue:基于链表的单端阻塞队列,就是队尾入队,队首出队
  • Deque:双端队列,就是队首、队尾都可以入队、出队
  • Semaphore:信号量,需要通过 acquire(或 tryAcquire) 获取到许可证之后才可以进入临界区,通过 release 释放许可证。只要能拿到许可证,Semaphore 是可以允许多个线程进入临界区的。另外注意它这里设置的许可证数量是0,说明要先调用了 release 放入一个许可证,才有可能调用 acquire 获取到许可证。
// 接收任务的队列
private final BlockingQueue<TaskHolder<ID, T>> acceptorQueue = new LinkedBlockingQueue<>();
// 重试任务的队列
private final BlockingDeque<TaskHolder<ID, T>> reprocessQueue = new LinkedBlockingDeque<>();
// 后台接收者线程
private final Thread acceptorThread;
// 待处理任务容器
private final Map<ID, TaskHolder<ID, T>> pendingTasks = new HashMap<>();
// 处理中的队列
private final Deque<ID> processingOrder = new LinkedList<>();

// 单项队列请求的信号量
private final Semaphore singleItemWorkRequests = new Semaphore(0);
// 单项任务队列
private final BlockingQueue<TaskHolder<ID, T>> singleItemWorkQueue = new LinkedBlockingQueue<>();

// 批量队列请求的信号量
private final Semaphore batchWorkRequests = new Semaphore(0);
// 批量任务队列
private final BlockingQueue<List<TaskHolder<ID, T>>> batchWorkQueue = new LinkedBlockingQueue<>();
// 时间调整器
private final TrafficShaper trafficShaper;

TaskDispatcher 调用 acceptorExecutor.process 将任务转给 AcceptorExecutor,可以看到就是将任务添加到接收者队列 acceptorQueue 的队尾了。

void process(ID id, T task, long expiryTime) {
    acceptorQueue.add(new TaskHolder<ID, T>(id, task, expiryTime));
    acceptedTasks++;
}

接收者任务 AcceptorRunner

任务添加到 acceptorQueue 了,那任务在哪处理的呢?这就是在 AcceptorRunner 这个任务里去处理的了,这个任务比较复杂,我先把整个代码放出来,再来分析。

class AcceptorRunner implements Runnable {
    @Override
    public void run() {
        long scheduleTime = 0;
        while (!isShutdown.get()) {
            try {
                // 排出输入队列的任务:将 reprocessQueue、acceptorQueue 队列的任务转移到 pendingTasks
                drainInputQueues();

                // 待处理的数量
                int totalItems = processingOrder.size();

                long now = System.currentTimeMillis();
                if (scheduleTime < now) {
                    // 时间补偿,正常情况下 transmissionDelay() 返回 0
                    scheduleTime = now + trafficShaper.transmissionDelay();
                }
                if (scheduleTime <= now) {
                    // 分配批量工作任务:将 pendingTasks 的任务分一批到(最多250个) batchWorkQueue 队列中
                    assignBatchWork();
                    // 分配单项工作任务:pendingTasks 如果还有剩余任务,将没有过期的转移到 singleItemWorkQueue 队列中
                    assignSingleItemWork();
                }

                // If no worker is requesting data or there is a delay injected by the traffic shaper,
                // sleep for some time to avoid tight loop.
                if (totalItems == processingOrder.size()) {
                    Thread.sleep(10);
                }
            } catch (InterruptedException ex) {
                // Ignore
            } catch (Throwable e) {
                // Safe-guard, so we never exit this loop in an uncontrolled way.
                logger.warn("Discovery AcceptorThread error", e);
            }
        }
    }

    private boolean isFull() {
        // 待处理的任务 >= 10000,也就是说 pendingTasks 最多放 10000 个任务
        return pendingTasks.size() >= maxBufferSize;
    }

    private void drainInputQueues() throws InterruptedException {
        do {
            // 排出 reprocessQueue,将 reprocessQueue 队列的任务转移到 pendingTasks
            drainReprocessQueue();
            // 排出 acceptorQueue,将 acceptorQueue 队列的任务转移到 pendingTasks
            drainAcceptorQueue();

            if (isShutdown.get()) {
                break;
            }
            // If all queues are empty, block for a while on the acceptor queue
            if (reprocessQueue.isEmpty() && acceptorQueue.isEmpty() && pendingTasks.isEmpty()) {
                // 等待任务放入 acceptorQueue,等待 10 毫秒
                TaskHolder<ID, T> taskHolder = acceptorQueue.poll(10, TimeUnit.MILLISECONDS);
                if (taskHolder != null) {
                    // 放入之后 acceptorQueue、pendingTasks 就不为空了
                    appendTaskHolder(taskHolder);
                }
            }
            // pendingTasks 为空、acceptorQueue 不为空、reprocessQueue不为空时,就会一直循环
            // 如果所有任务都处理完了,reprocessQueue、acceptorQueue、pendingTasks 都是空的,
            // 这时就会循环等待任务进入 acceptorQueue,每次等待 10 毫秒
        } while (!reprocessQueue.isEmpty() || !acceptorQueue.isEmpty() || pendingTasks.isEmpty());
    }

    private void drainAcceptorQueue() {
        while (!acceptorQueue.isEmpty()) {
            // 将 acceptorQueue 的任务转移到 pendingTasks
            appendTaskHolder(acceptorQueue.poll());
        }
    }

    private void drainReprocessQueue() {
        long now = System.currentTimeMillis();
        while (!reprocessQueue.isEmpty() && !isFull()) {
            // 从 reprocessQueue 队尾取出任务
            TaskHolder<ID, T> taskHolder = reprocessQueue.pollLast();
            ID id = taskHolder.getId();
            if (taskHolder.getExpiryTime() <= now) {
                // 任务过期
                expiredTasks++;
            } else if (pendingTasks.containsKey(id)) {
                // pendingTasks 已存在
                overriddenTasks++;
            } else {
                // 将 reprocessQueue 队列的任务放到 pendingTasks
                pendingTasks.put(id, taskHolder);
                // 添加到 processingOrder 队列的头部,reprocessQueue 是失败重试的队列,所以优先级高一些
                processingOrder.addFirst(id);
            }
        }
        if (isFull()) {
            queueOverflows += reprocessQueue.size();
            // pendingTasks 满了,就清空 reprocessQueue
            reprocessQueue.clear();
        }
    }

    private void appendTaskHolder(TaskHolder<ID, T> taskHolder) {
        if (isFull()) {
            // pendingTasks 满了就移除一个元素
            pendingTasks.remove(processingOrder.poll());
            queueOverflows++;
        }
        // 将 acceptorQueue 里的任务放到 pendingTasks
        TaskHolder<ID, T> previousTask = pendingTasks.put(taskHolder.getId(), taskHolder);
        if (previousTask == null) {
            // 原本不存在,将任务ID添加到 processingOrder 队列的最后
            processingOrder.add(taskHolder.getId());
        } else {
            // 已经存在了,就是覆盖
            overriddenTasks++;
        }
    }

    void assignSingleItemWork() {
        if (!processingOrder.isEmpty()) {
            if (singleItemWorkRequests.tryAcquire(1)) {
                long now = System.currentTimeMillis();
                while (!processingOrder.isEmpty()) {
                    ID id = processingOrder.poll();
                    TaskHolder<ID, T> holder = pendingTasks.remove(id);
                    if (holder.getExpiryTime() > now) {
                        // 将 pendingTasks 的任务移到 singleItemWorkQueue
                        singleItemWorkQueue.add(holder);
                        return;
                    }
                    expiredTasks++;
                }
                singleItemWorkRequests.release();
            }
        }
    }

    void assignBatchWork() {
        // 有足够的任务做一个批处理
        if (hasEnoughTasksForNextBatch()) {
            if (batchWorkRequests.tryAcquire(1)) {
                long now = System.currentTimeMillis();
                // 一批任务最多 250 个
                int len = Math.min(maxBatchingSize, processingOrder.size());
                List<TaskHolder<ID, T>> holders = new ArrayList<>(len);
                // 将 pendingTasks 中的任务移动一批到 holders 中
                // 也就是说,如果队列中有500个任务,这一批任务最多也是250个
                while (holders.size() < len && !processingOrder.isEmpty()) {
                    ID id = processingOrder.poll();
                    TaskHolder<ID, T> holder = pendingTasks.remove(id);
                    if (holder.getExpiryTime() > now) {
                        holders.add(holder);
                    } else {
                        expiredTasks++;
                    }
                }
                if (holders.isEmpty()) {
                    batchWorkRequests.release();
                } else {
                    batchSizeMetric.record(holders.size(), TimeUnit.MILLISECONDS);
                    // 添加到批量队列中
                    batchWorkQueue.add(holders);
                }
            }
        }
    }

    // 是否有足够的任务做一个批处理
    private boolean hasEnoughTasksForNextBatch() {
        if (processingOrder.isEmpty()) {
            return false;
        }
        if (pendingTasks.size() >= maxBufferSize) {
            return true;
        }

        // 从 processingOrder 队首取一个任务ID,然后从 pendingTasks 读取这个任务。注意 peek() 只是取出元素,并不会移除队首的元素
        TaskHolder<ID, T> nextHolder = pendingTasks.get(processingOrder.peek());
        // 判断任务提交到现在的时间差是否超过最大批任务延迟时间(500毫秒)
        long delay = System.currentTimeMillis() - nextHolder.getSubmitTimestamp();
        return delay >= maxBatchingDelay;
    }
}

先看它的 run 方法:

1、队列中的任务转移到待处理容器中

drainInputQueues 将输入队列(reprocessQueue、acceptorQueue)的任务转移到 pendingTasks 这个待处理容器中。

先是 drainReprocessQueue 将重处理队列 reprocessQueue 中的任务转移到 pendingTasks:

  • 如果 pendingTasks 已满(超过10000),就直接清空了 reprocessQueue。任务丢弃会不会有影响呢?
  • 否则,如果 reprocessQueue 非空,就从 reprocessQueue 队尾一个个取出来:
    • 如果过期了就丢掉这个任务,说明已经超过续约周期了(90秒)。比如实例注册,如果多次同步失败后,然后就直接丢弃,那不是其它 server 永远无法知道注册的这个实例?后面再分析这个问题。
    • 如果 pendingTasks 已经存在了,也丢弃这个重试任务
    • 否则就添加到 pendingTasks 中,并且往 processingOrder 的头部添加了任务ID
    • 注意它这里是从 reprocessQueue 队尾一个个取出,放入 processingOrder 头部,最终任务在 processingOrder 中的顺序跟 reprocessQueue 是一样的

然后是 drainAcceptorQueue 将接收者队列 acceptorQueue 中的任务转移到 pendingTasks:

  • 只要 acceptorQueue 非空,就从队首取出任务
  • 如果 pendingTasks 已满,则从 processingOrder 队首取出第一个任务的ID,并从 pendingTasks 中移除这个任务
  • 否则就将任务添加到 pendingTasks,如果之前不存在相同ID的任务,就将任务ID添加到 processingOrder 队尾
  • 注意它这里是从 acceptorQueue 队首取出任务,放到 processingOrder 队尾,最终任务在 processingOrder 中的顺序跟 acceptorQueue 是一样的

从这段任务转移以及后面的使用来看,processingOrder 将决定任务的处理顺序,最前面的将最先处理,也说明了 reprocessQueue 的优先级比 acceptorQueue 更高。而 pendingTasks 是一个 key-value 的队列,便于快速通过ID读取任务。

private void drainAcceptorQueue() {
    while (!acceptorQueue.isEmpty()) {
        // 将 acceptorQueue 的任务转移到 pendingTasks
        appendTaskHolder(acceptorQueue.poll());
    }
}

private void drainReprocessQueue() {
    long now = System.currentTimeMillis();
    while (!reprocessQueue.isEmpty() && !isFull()) {
        // 从 reprocessQueue 队尾取出任务
        TaskHolder<ID, T> taskHolder = reprocessQueue.pollLast();
        ID id = taskHolder.getId();
        if (taskHolder.getExpiryTime() <= now) {
            // 任务过期
            expiredTasks++;
        } else if (pendingTasks.containsKey(id)) {
            // pendingTasks 已存在
            overriddenTasks++;
        } else {
            // 将 reprocessQueue 队列的任务放到 pendingTasks
            pendingTasks.put(id, taskHolder);
            // 添加到 processingOrder 队列的头部,reprocessQueue 是失败重试的队列,所以优先级高一些
            processingOrder.addFirst(id);
        }
    }
    if (isFull()) {
        queueOverflows += reprocessQueue.size();
        // pendingTasks 满了,就清空 reprocessQueue
        reprocessQueue.clear();
    }
}

private void appendTaskHolder(TaskHolder<ID, T> taskHolder) {
    if (isFull()) {
        // pendingTasks 满了就移除一个元素
        pendingTasks.remove(processingOrder.poll());
        queueOverflows++;
    }
    // 将 acceptorQueue 里的任务放到 pendingTasks
    TaskHolder<ID, T> previousTask = pendingTasks.put(taskHolder.getId(), taskHolder);
    if (previousTask == null) {
        // 原本不存在,将任务ID添加到 processingOrder 队列的最后
        processingOrder.add(taskHolder.getId());
    } else {
        // 已经存在了,就是覆盖
        overriddenTasks++;
    }
}

2、接下来通过 trafficShaper 获取了一个补偿时间,它主要是在发生阻塞或网络异常导致任务提交失败后,在任务调度周期内做一个时间补偿,这块等分析到提交任务失败的时候再回来看看。

long now = System.currentTimeMillis();
if (scheduleTime < now) {
    // 时间补偿,正常情况下 transmissionDelay() 返回 0
    scheduleTime = now + trafficShaper.transmissionDelay();
}

3、任务打包

接着看 assignBatchWork ,它就是将任务打包成一个批次:

  • 首先调用 hasEnoughTasksForNextBatch 判断是否有足够的任务来打成一个批次,注意它判断了最新提交的任务的时间是否超过了延迟时间 maxBatchingDelay(500ms),也就是说批次任务每隔500毫秒运行一次
  • 能够打包后,要获取 batchWorkRequests 信号量的一个许可证,因为许可证默认数量是 0,那一定是先有地方调用了 batchWorkRequests.release() 放入许可证,否则这里就不会打包了。
  • 然后可以看出,一个批次的任务数量最多是250个
  • 它从 processingOrder 的队首取出这个批次的任务ID,并从 pendingTasks 中取出任务,如果是过期的任务就直接丢弃了。
  • 然后如果这个批次并没有任务,他才调用 batchWorkRequests.release() 释放了许可证,否则就把这个批次任务添加到批量工作队列 batchWorkQueue 中,注意并没有释放许可证。
void assignBatchWork() {
    // 有足够的任务做一个批处理
    if (hasEnoughTasksForNextBatch()) {
        // 获取许可证
        if (batchWorkRequests.tryAcquire(1)) {
            long now = System.currentTimeMillis();
            // 一批任务最多 250 个
            int len = Math.min(maxBatchingSize, processingOrder.size());
            List<TaskHolder<ID, T>> holders = new ArrayList<>(len);
            // 将 pendingTasks 中的任务移动一批到 holders 中
            // 也就是说,如果队列中有500个任务,这一批任务最多也是250个
            while (holders.size() < len && !processingOrder.isEmpty()) {
                ID id = processingOrder.poll();
                TaskHolder<ID, T> holder = pendingTasks.remove(id);
                if (holder.getExpiryTime() > now) {
                    holders.add(holder);
                } else {
                    expiredTasks++;
                }
            }
            if (holders.isEmpty()) {
                batchWorkRequests.release();
            } else {
                batchSizeMetric.record(holders.size(), TimeUnit.MILLISECONDS);
                // 添加到批量队列中
                batchWorkQueue.add(holders);
            }
        }
    }
}

// 是否有足够的任务做一个批处理
private boolean hasEnoughTasksForNextBatch() {
    if (processingOrder.isEmpty()) {
        return false;
    }
    if (pendingTasks.size() >= maxBufferSize) {
        return true;
    }

    // 从 processingOrder 队首取一个任务ID,然后从 pendingTasks 读取这个任务。注意 peek() 只是取出元素,并不会移除队首的元素
    TaskHolder<ID, T> nextHolder = pendingTasks.get(processingOrder.peek());
    // 判断任务提交到现在的时间差是否超过最大批任务延迟时间(500毫秒)
    long delay = System.currentTimeMillis() - nextHolder.getSubmitTimestamp();
    return delay >= maxBatchingDelay;
}

接着看分配单项任务的方法 assignSingleItemWork

  • 如果 processingOrder 非空且获取到了 singleItemWorkRequests 信号量的许可证,就将 processingOrder 队列剩余的任务都取出来,放入单项工作队列 singleItemWorkQueue
  • 也就是前面已经打了一批任务(250个)之后,processingOrder 中还有任务,就全部取出来放到 singleItemWorkQueue 队列中
void assignSingleItemWork() {
    if (!processingOrder.isEmpty()) {
        if (singleItemWorkRequests.tryAcquire(1)) {
            long now = System.currentTimeMillis();
            while (!processingOrder.isEmpty()) {
                ID id = processingOrder.poll();
                TaskHolder<ID, T> holder = pendingTasks.remove(id);
                if (holder.getExpiryTime() > now) {
                    // 将 pendingTasks 的任务移到 singleItemWorkQueue
                    singleItemWorkQueue.add(holder);
                    return;
                }
                expiredTasks++;
            }
            singleItemWorkRequests.release();
        }
    }
}

任务处理器 TaskExecutors

batchWorkQueue 中的批量任务以及 singleItemWorkQueue 中的单项任务都已经准备好了,那是在哪里发送到集群节点的呢,那就是任务执行器 TaskExecutors 了。

1、创建 TaskExecutors

从创建 TaskExecutors 的方法中可以看出:

  • 批量处理任务的类是 BatchWorkerRunnable,它主要就是处理批量任务队列 batchWorkQueue 中的任务
  • 处理单项任务的类是 SingleTaskWorkerRunnable,它主要就是处理单项任务队列 singleItemWorkQueue 中的任务
  • TaskExecutors 创建了一个线程池,batchExecutors 默认有20个工作线程(不太理解他为什么不用JDK现成的线程池。。),singleItemExecutors 默认只有一个工作线程。
static <ID, T> TaskExecutors<ID, T> singleItemExecutors(final String name, int workerCount, final TaskProcessor<T> processor, final AcceptorExecutor<ID, T> acceptorExecutor) {
    final AtomicBoolean isShutdown = new AtomicBoolean();
    final TaskExecutorMetrics metrics = new TaskExecutorMetrics(name);
    registeredMonitors.put(name, metrics);
    // workerCount = 1
    return new TaskExecutors<>(idx -> new SingleTaskWorkerRunnable<>("TaskNonBatchingWorker-" + name + '-' + idx, isShutdown, metrics, processor, acceptorExecutor), workerCount, isShutdown);
}

////////////////////////////////////////////////

static <ID, T> TaskExecutors<ID, T> batchExecutors(final String name, int workerCount, final TaskProcessor<T> processor, final AcceptorExecutor<ID, T> acceptorExecutor) {
    final AtomicBoolean isShutdown = new AtomicBoolean();
    final TaskExecutorMetrics metrics = new TaskExecutorMetrics(name);
    registeredMonitors.put(name, metrics);
    // BatchWorkerRunnable 批量任务处理
    return new TaskExecutors<>(idx -> new BatchWorkerRunnable<>("TaskBatchingWorker-" + name + '-' + idx, isShutdown, metrics, processor, acceptorExecutor), workerCount, isShutdown);
}

////////////////////////////////////////////////

private final List<Thread> workerThreads;

TaskExecutors(WorkerRunnableFactory<ID, T> workerRunnableFactory, int workerCount, AtomicBoolean isShutdown) {
    this.isShutdown = isShutdown;
    // 工作线程集合
    this.workerThreads = new ArrayList<>();

    // 创建20个线程,相当于是搞了一个线程池
    ThreadGroup threadGroup = new ThreadGroup("eurekaTaskExecutors");
    for (int i = 0; i < workerCount; i++) {
        WorkerRunnable<ID, T> runnable = workerRunnableFactory.create(i);
        Thread workerThread = new Thread(threadGroup, runnable, runnable.getWorkerName());
        workerThreads.add(workerThread);
        workerThread.setDaemon(true);
        workerThread.start();
    }
}

2、BatchWorkerRunnable

看批量处理的任务:

  • 首先 getWork 获取批量任务,它调用 taskDispatcher.requestWorkItems(),实际就是返回了 taskDispatcher 的 batchWorkQueue,并且调用 batchWorkRequests.release() 往信号量放入一个许可证,这样前面 AcceptorRunner 就可以得到许可证然后去打包批量任务了
  • 如果 batchWorkQueue 中没有批量任务,可以看到是一直在 while 循环等待的,直到拿到一个批量任务。它这个 BatchWorkerRunnable任务和前面的 AcceptorRunner 任务,感觉通过信号量的方式就形成了一个等待通知的机制,BatchWorkerRunnable 放入一个许可证,让 AcceptorRunner 拿到这个许可证去打个批次的任务过来。
  • 拿到这个批次任务后,就调用 processor(ReplicationTaskProcessor)来处理任务。
  • 如果任务处理结果是 Congestion(阻塞)、TransientError(传输失败)就要重处理,调用了 taskDispatcher.reprocess 将这个批次的任务提交到重处理队列 reprocessQueue 中。
static class BatchWorkerRunnable<ID, T> extends WorkerRunnable<ID, T> {

    BatchWorkerRunnable(String workerName, AtomicBoolean isShutdown, TaskExecutorMetrics metrics, TaskProcessor<T> processor, AcceptorExecutor<ID, T> acceptorExecutor) {
        super(workerName, isShutdown, metrics, processor, acceptorExecutor);
    }

    @Override
    public void run() {
        try {
            while (!isShutdown.get()) {
                // 获取一个批量任务
                List<TaskHolder<ID, T>> holders = getWork();
                metrics.registerExpiryTimes(holders);
                // TaskHolder 提取 ReplicationTask
                List<T> tasks = getTasksOf(holders);
                // processor => 任务复制处理器 ReplicationTaskProcessor
                ProcessingResult result = processor.process(tasks);
                switch (result) {
                    case Success:
                        break;
                    case Congestion:
                    case TransientError:
                        // 阻塞或网络失败就重新处理这批任务
                        taskDispatcher.reprocess(holders, result);
                        break;
                    case PermanentError:
                        logger.warn("Discarding {} tasks of {} due to permanent error", holders.size(), workerName);
                }
                metrics.registerTaskResult(result, tasks.size());
            }
        } catch (InterruptedException e) {
            // Ignore
        } catch (Throwable e) {
            // Safe-guard, so we never exit this loop in an uncontrolled way.
            logger.warn("Discovery WorkerThread error", e);
        }
    }

    private List<TaskHolder<ID, T>> getWork() throws InterruptedException {
        // 获取批量队列 batchWorkQueue
        BlockingQueue<List<TaskHolder<ID, T>>> workQueue = taskDispatcher.requestWorkItems();
        List<TaskHolder<ID, T>> result;
        do {
            result = workQueue.poll(1, TimeUnit.SECONDS);
            // 循环等待,直到取到一个批量任务
        } while (!isShutdown.get() && result == null);
        return (result == null) ? new ArrayList<>() : result;
    }
}
BlockingQueue<TaskHolder<ID, T>> requestWorkItem() {
    singleItemWorkRequests.release();
    return singleItemWorkQueue;
}

BlockingQueue<List<TaskHolder<ID, T>>> requestWorkItems() {
    batchWorkRequests.release();
    return batchWorkQueue;
}

3、任务重处理

可以看到处理失败后,就是将这批任务添加到重处理队列 reprocessQueue 中去,然后向时间调整期注册失败,这就和前面 AcceptorRunner 处理 reprocessQueue 对应起来了。

void reprocess(List<TaskHolder<ID, T>> holders, ProcessingResult processingResult) {
    // 添加到重处理队列 reprocessQueue
    reprocessQueue.addAll(holders);
    replayedTasks += holders.size();
    // 时间调整器注册失败
    trafficShaper.registerFailure(processingResult);
}

4、TrafficShaper

还记得前面 AcceptorRunner 中又这样一段代码,可以看到是通过 trafficShaper 计算了一个延迟时间,这里就来看看是如何计算的。

long now = System.currentTimeMillis();
if (scheduleTime < now) {
    // 时间补偿,正常情况下 transmissionDelay() 返回 0
    scheduleTime = now + trafficShaper.transmissionDelay();
}
if (scheduleTime <= now) {
    // 分配批量工作任务:将 pendingTasks 的任务分一批到(最多250个) batchWorkQueue 队列中
    assignBatchWork();
    // 分配单项工作任务:pendingTasks 如果还有剩余任务,将没有过期的转移到 singleItemWorkQueue 队列中
    assignSingleItemWork();
}

时间调整器 TrafficShaper

  • registerFailure 就是设置了失败的最后时间
  • 然后看 transmissionDelay,以阻塞为例,如果上一次阻塞失败到现在 500 毫秒,那么 transmissionDelay 返回 500,那么 transmissionDelay 就大于 now 了,就不会打包任务了。
  • 总结下来就是如果上一次阻塞导致批量任务提交失败,就延迟1000毫秒后执行。如果上一次网络导致批量任务提交失败,就延迟100毫秒执行。
TrafficShaper(long congestionRetryDelayMs, long networkFailureRetryMs) {
    // 1000
    this.congestionRetryDelayMs = Math.min(MAX_DELAY, congestionRetryDelayMs);
    // 100
    this.networkFailureRetryMs = Math.min(MAX_DELAY, networkFailureRetryMs);
}

void registerFailure(ProcessingResult processingResult) {
    if (processingResult == ProcessingResult.Congestion) {
        // 最后一次阻塞导致提交批处理失败的时间
        lastCongestionError = System.currentTimeMillis();
    } else if (processingResult == ProcessingResult.TransientError) {
        // 最后一次网络原因导致提交批处理失败的时间
        lastNetworkFailure = System.currentTimeMillis();
    }
}

// 计算传输延迟的时间
long transmissionDelay() {
    if (lastCongestionError == -1 && lastNetworkFailure == -1) {
        return 0;
    }

    long now = System.currentTimeMillis();
    if (lastCongestionError != -1) {
        // 阻塞延迟时间
        long congestionDelay = now - lastCongestionError;
        if (congestionDelay >= 0 && congestionDelay < congestionRetryDelayMs) {
            return congestionRetryDelayMs - congestionDelay;
        }
        lastCongestionError = -1;
    }

    if (lastNetworkFailure != -1) {
        // 网络延迟时间
        long failureDelay = now - lastNetworkFailure;
        if (failureDelay >= 0 && failureDelay < networkFailureRetryMs) {
            return networkFailureRetryMs - failureDelay;
        }
        lastNetworkFailure = -1;
    }
    return 0;
}

5、SingleTaskWorkerRunnable

单项任务处理跟批量任务处理的流程是类似的,只不过是一个个的发送同步操作,处理失败同样也会放入重处理队列中。

一个批量任务250个对于大部分场景来说其实不会触发单项任务的处理,如果微服务集群中有很多的实例,eureka 通过不断的轮询也能尽量使用批量处理,我觉得单项任务处理更像是对批量任务处理的一种补充。

复制任务处理器 ReplicationTaskProcessor

批量任务最终是提交到 ReplicationTaskProcessor 去处理的,可以看到,就是调用了 replicationClient 提交了批量任务,提交的接口是 POST peerreplication/batch,那我们就可以从这个入口去看 eureka-server 如何接收批量任务的。

public ProcessingResult process(List<ReplicationTask> tasks) {
    // 任务封装到 ReplicationList
    ReplicationList list = createReplicationListOf(tasks);
    try {
        // 提交批量任务:POST peerreplication/batch/
        EurekaHttpResponse<ReplicationListResponse> response = replicationClient.submitBatchUpdates(list);
        int statusCode = response.getStatusCode();
        if (!isSuccess(statusCode)) {
            if (statusCode == 503) {
                return ProcessingResult.Congestion;
            } else {
                return ProcessingResult.PermanentError;
            }
        } else {
            // 处理批量任务结果
            handleBatchResponse(tasks, response.getEntity().getResponseList());
        }
    } catch (Throwable e) {
        if (maybeReadTimeOut(e)) {
            return ProcessingResult.Congestion;
        } else if (isNetworkConnectException(e)) {
            return ProcessingResult.TransientError;
        } else {
            return ProcessingResult.PermanentError;
        }
    }
    return ProcessingResult.Success;
}

接收复制同步请求

很容易找到批量任务提交的接口在 PeerReplicationResourcebatchReplication 方法中。

可以看到,其实遍历批量任务,然后根据不同的操作类型,调用 XxxResource 接口进行对应的操作。比如注册,就是调用 applicationResource.addInstance 完成实例的注册。

@Path("/{version}/peerreplication")
@Produces({"application/xml", "application/json"})
public class PeerReplicationResource {
    private static final Logger logger = LoggerFactory.getLogger(PeerReplicationResource.class);

    private static final String REPLICATION = "true";

    private final EurekaServerConfig serverConfig;
    private final PeerAwareInstanceRegistry registry;

    @Inject
    PeerReplicationResource(EurekaServerContext server) {
        this.serverConfig = server.getServerConfig();
        this.registry = server.getRegistry();
    }

    public PeerReplicationResource() {
        this(EurekaServerContextHolder.getInstance().getServerContext());
    }

    @Path("batch")
    @POST
    public Response batchReplication(ReplicationList replicationList) {
        try {
            ReplicationListResponse batchResponse = new ReplicationListResponse();
            for (ReplicationInstance instanceInfo : replicationList.getReplicationList()) {
                try {
                    // dispatch 分发任务
                    batchResponse.addResponse(dispatch(instanceInfo));
                } catch (Exception e) {
                    batchResponse.addResponse(new ReplicationInstanceResponse(Status.INTERNAL_SERVER_ERROR.getStatusCode(), null));
                }
            }
            return Response.ok(batchResponse).build();
        } catch (Throwable e) {
            return Response.status(Status.INTERNAL_SERVER_ERROR).build();
        }
    }

    private ReplicationInstanceResponse dispatch(ReplicationInstance instanceInfo) {
        ApplicationResource applicationResource = createApplicationResource(instanceInfo);
        InstanceResource resource = createInstanceResource(instanceInfo, applicationResource);

        String lastDirtyTimestamp = toString(instanceInfo.getLastDirtyTimestamp());
        String overriddenStatus = toString(instanceInfo.getOverriddenStatus());
        String instanceStatus = toString(instanceInfo.getStatus());

        Builder singleResponseBuilder = new Builder();
        // 根据不同的类型分别处理
        switch (instanceInfo.getAction()) {
            case Register: // 注册
                singleResponseBuilder = handleRegister(instanceInfo, applicationResource);
                break;
            case Heartbeat: // 续约
                singleResponseBuilder = handleHeartbeat(serverConfig, resource, lastDirtyTimestamp, overriddenStatus, instanceStatus);
                break;
            case Cancel: // 下线
                singleResponseBuilder = handleCancel(resource);
                break;
            case StatusUpdate: // 状态变更
                singleResponseBuilder = handleStatusUpdate(instanceInfo, resource);
                break;
            case DeleteStatusOverride:
                singleResponseBuilder = handleDeleteStatusOverride(instanceInfo, resource);
                break;
        }
        return singleResponseBuilder.build();
    }

    private static Builder handleRegister(ReplicationInstance instanceInfo, ApplicationResource applicationResource) {
        // addInstance
        applicationResource.addInstance(instanceInfo.getInstanceInfo(), REPLICATION);
        return new Builder().setStatusCode(Status.OK.getStatusCode());
    }

    private static Builder handleCancel(InstanceResource resource) {
        // cancelLease
        Response response = resource.cancelLease(REPLICATION);
        return new Builder().setStatusCode(response.getStatus());
    }

    private static Builder handleHeartbeat(EurekaServerConfig config, InstanceResource resource, String lastDirtyTimestamp, String overriddenStatus, String instanceStatus) {
        Response response = resource.renewLease(REPLICATION, overriddenStatus, instanceStatus, lastDirtyTimestamp);
        int responseStatus = response.getStatus();
        Builder responseBuilder = new Builder().setStatusCode(responseStatus);

        if ("false".equals(config.getExperimental("bugfix.934"))) {
            if (responseStatus == Status.OK.getStatusCode() && response.getEntity() != null) {
                responseBuilder.setResponseEntity((InstanceInfo) response.getEntity());
            }
        } else {
            if ((responseStatus == Status.OK.getStatusCode() || responseStatus == Status.CONFLICT.getStatusCode())
                    && response.getEntity() != null) {
                responseBuilder.setResponseEntity((InstanceInfo) response.getEntity());
            }
        }
        return responseBuilder;
    }

    private static Builder handleStatusUpdate(ReplicationInstance instanceInfo, InstanceResource resource) {
        Response response = resource.statusUpdate(instanceInfo.getStatus(), REPLICATION, toString(instanceInfo.getLastDirtyTimestamp()));
        return new Builder().setStatusCode(response.getStatus());
    }

    private static Builder handleDeleteStatusOverride(ReplicationInstance instanceInfo, InstanceResource resource) {
        Response response = resource.deleteStatusUpdate(REPLICATION, instanceInfo.getStatus(),
                instanceInfo.getLastDirtyTimestamp().toString());
        return new Builder().setStatusCode(response.getStatus());
    }

    private static <T> String toString(T value) {
        if (value == null) {
            return null;
        }
        return value.toString();
    }
}

集群数据同步冲突问题

Peer to Peer 模式重点要解决的一个问题是数据复制冲突的问题,因为 peer 节点间的相互复制并不能保证所有操作都成功。eureka 主要通过 lastDirtyTimestamp 标识和心跳来进行数据的最终修复,下面就来看下 eureka 如何处理数据冲突问题的。

1、先看续约的这个方法

  • 在续约 renewLease 里,如果 lastDirtyTimestamp 不为空且允许时间戳不一致时进行同步(默认开启),就调用了 validateDirtyTimestamp 方法校验 lastDirtyTimestamp。
  • 接着看 validateDirtyTimestamp,如果 lastDirtyTimestamp 与本地实例的 lastDirtyTimestamp 一致,说明数据是一致的,就续约成功,返回 OK(200)
  • 如果 lastDirtyTimestamp 大于 本地实例的 lastDirtyTimestamp,说明复制的实例最新更新的,出现数据冲突,返回 NOT_FOUND(404)
  • 如果 lastDirtyTimestamp 小于 本地实例的 lastDirtyTimestamp ,说明复制的实例是旧的,出现数据冲突,返回 CONFLICT(409),并且返回了本地的实例。
@PUT
public Response renewLease(
        @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication,
        @QueryParam("overriddenstatus") String overriddenStatus,
        @QueryParam("status") String status,
        @QueryParam("lastDirtyTimestamp") String lastDirtyTimestamp) {
    boolean isFromReplicaNode = "true".equals(isReplication);
    // 调用注册表的 renew 进行服务续约
    boolean isSuccess = registry.renew(app.getName(), id, isFromReplicaNode);
    if (!isSuccess) {
        logger.warn("Not Found (Renew): {} - {}", app.getName(), id);
        return Response.status(Status.NOT_FOUND).build();
    }
    
    Response response;
    // 如果是复制操作,就校验 lastDirtyTimestamp
    if (lastDirtyTimestamp != null && serverConfig.shouldSyncWhenTimestampDiffers()) {
        response = this.validateDirtyTimestamp(Long.valueOf(lastDirtyTimestamp), isFromReplicaNode);
        // Store the overridden status since the validation found out the node that replicates wins
        if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()
                && (overriddenStatus != null)
                && !(InstanceStatus.UNKNOWN.name().equals(overriddenStatus))
                && isFromReplicaNode) {
            registry.storeOverriddenStatusIfRequired(app.getAppName(), id, InstanceStatus.valueOf(overriddenStatus));
        }
    } else {
        response = Response.ok().build();
    }
    return response;
}

private Response validateDirtyTimestamp(Long lastDirtyTimestamp, boolean isReplication) {
    InstanceInfo appInfo = registry.getInstanceByAppAndId(app.getName(), id, false);
    if (appInfo != null) {
        // 如果复制传过来的实例中 lastDirtyTimestamp 不等于本地实例的 lastDirtyTimestamp
        if ((lastDirtyTimestamp != null) && (!lastDirtyTimestamp.equals(appInfo.getLastDirtyTimestamp()))) {
            Object[] args = {id, appInfo.getLastDirtyTimestamp(), lastDirtyTimestamp, isReplication};

            if (lastDirtyTimestamp > appInfo.getLastDirtyTimestamp()) {
                // 如果复制实例的 lastDirtyTimestamp > 本地实例的 lastDirtyTimestamp,表示数据出现冲突,返回 404,要求应用实例重新进行 register 操作
                return Response.status(Status.NOT_FOUND).build();
            } else if (appInfo.getLastDirtyTimestamp() > lastDirtyTimestamp) {
                // In the case of replication, send the current instance info in the registry for the
                // replicating node to sync itself with this one.
                if (isReplication) {
                    // 如果本地实例的 lastDirtyTimestamp > 复制实例的 lastDirtyTimestamp,就返回 CONFLICT(409),说明数据冲突,要求其同步自己最新的数据
                    // 注意这里将本地实例 appInfo 放入 Response entity 中了
                    return Response.status(Status.CONFLICT).entity(appInfo).build();
                } else {
                    return Response.ok().build();
                }
            }
        }
    }
    return Response.ok().build();
}

2、接着看 PeerReplicationResource 处理心跳的方法

  • 首先就是调用了续约的方法 renewLease 进行续约
  • 如果返回的状态是 OK 或者 CONFLICT,就在 resposeEntity 中返回本地实例
private static Builder handleHeartbeat(EurekaServerConfig config, InstanceResource resource, String lastDirtyTimestamp, String overriddenStatus, String instanceStatus) {
    // 调用 renewLease 续约
    Response response = resource.renewLease(REPLICATION, overriddenStatus, instanceStatus, lastDirtyTimestamp);
    int responseStatus = response.getStatus();
    Builder responseBuilder = new Builder().setStatusCode(responseStatus);

    if ("false".equals(config.getExperimental("bugfix.934"))) {
        if (responseStatus == Status.OK.getStatusCode() && response.getEntity() != null) {
            responseBuilder.setResponseEntity((InstanceInfo) response.getEntity());
        }
    } else {
        if ((responseStatus == Status.OK.getStatusCode() || responseStatus == Status.CONFLICT.getStatusCode())
                && response.getEntity() != null) {
            // 续约成功或 CONFLICT 冲突时,将本地实例 appInfo 返回到客户端
            responseBuilder.setResponseEntity((InstanceInfo) response.getEntity());
        }
    }
    return responseBuilder;
}

3、PeerEurekaNode 发送心跳

ReplicationTaskProcessor 收到批量任务返回结果后,会处理响应结果,对于心跳任务,可以找到,失败后就会回调 handleFailure 方法。

  • 如果返回状态是 404(NOT_FOUND),就会重新注册,也是提交到队列中。通过重新注册来实现数据同步
  • 如果是其它状态(409 CONFLICT)并且开启了时间戳不一致就同步的配置,就将服务端返回的实例注册到本地,实现数据的同步
public void heartbeat(final String appName, final String id, final InstanceInfo info, 
			final InstanceStatus overriddenStatus, boolean primeConnection) throws Throwable {
    ReplicationTask replicationTask = new InstanceReplicationTask(targetHost, Action.Heartbeat, info, overriddenStatus, false) {
        @Override
        public EurekaHttpResponse<InstanceInfo> execute() throws Throwable {
        	// 向集群Server发送心跳
            return replicationClient.sendHeartBeat(appName, id, info, overriddenStatus);
        }

        @Override
        public void handleFailure(int statusCode, Object responseEntity) throws Throwable {
            super.handleFailure(statusCode, responseEntity);
            if (statusCode == 404) {
                logger.warn("{}: missing entry.", getTaskName());
                if (info != null) {
                    // 复制返回 404 时,重新注册
                    register(info);
                }
            } else if (config.shouldSyncWhenTimestampDiffers()) {
                // 409(CONFLICT),将服务端返回的实例同步到本地
                InstanceInfo peerInstanceInfo = (InstanceInfo) responseEntity;
                if (peerInstanceInfo != null) {
                    syncInstancesIfTimestampDiffers(appName, id, info, peerInstanceInfo);
                }
            }
        }
    };
    long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
    batchingDispatcher.process(taskId("heartbeat", info), replicationTask, expiryTime);
}

private void syncInstancesIfTimestampDiffers(String appName, String id, InstanceInfo info, InstanceInfo infoFromPeer) {
    try {
        if (infoFromPeer != null) {
            // 将服务端的实例注册到本地,实现数据同步
            registry.register(infoFromPeer, true);
        }
    } catch (Throwable e) {
        logger.warn("Exception when trying to set information from peer :", e);
    }
}

至此,我们就可以总结出,eureka server 通过对比 lastDirtyTimestamp 和心跳操作来实现集群数据的复制和最终同步

前面提到的实例过期就丢弃任务这样看来就没问题,它也不保证peer节点间相互复制的所有操作都成功,eureka 采用的是最终一致性,它是通过心跳的方式实现集群数据的最终修复和同步,只是集群间可能会同步延迟

一张图总结集群同步

下面总结下 eureka-server 集群节点间的同步:

  • 首先 eureka-server 集群采用的是 Peer To Peer 的模式,即对等复制,各个 server 不分主从,每个 server 都可以接收写请求,然后互相之间进行数据更新同步。
  • 数据同步采用了多层任务队列+批量处理的机制
    • eureka-server 接收到客户端请求(注册、下线、续约)后都会调用集群 PeerEurekaNode 进行操作的同步
    • PeerEurekaNode 将操作封装成 InstanceReplicationTask 实例复制任务,并用批量分发器 batchingDispatcher(TaskDispatcher)来分发处理
    • batchingDispatcher 内部则将任务交给接收者执行器 AcceptorExecutor 处理,任务首先进入到 AcceptorExecutor 内的接收者队列 acceptorQueue 中
    • AcceptorExecutor 有个后台工作线程(AcceptorRunner)不断轮询,将接收者队列 acceptorQueue 和 重处理队列 reprocessQueue 中的任务转移到处理中队列中(processingOrder + pendingTasks)
    • 接着将处理中队列中的任务打包,一次最多 250 个任务,然后放到批量工作队列 batchWorkQueue。如果处理中队列中还有任务,就将任务放到单项任务队列 singleItemWorkQueue
    • 任务都打包好了,任务执行器 TaskExecutors 内分别有批量任务处理器(BatchWorkerRunnable)和单项任务处理器(SingleTaskWorkerRunnable)来处理 batchWorkQueue 和 singleItemWorkQueue 中的任务
    • 处理器会利用任务复制处理器(ReplicationTaskProcessor)来提交任务,批量任务会提交给 server 节点的批量接口(peerreplication/batch/),单项任务则提交到对应的操作接口
    • 任务提交如果阻塞或者网络失败就会被放入重处理队列 reprocessQueue,然后再次被 AcceptorRunner 轮询处理,不过过期(超过90秒)的任务会被丢弃掉
  • 其它 eureka-server 同步:
    • 其它 eureka-server 接收到批量复制请求后,会轮询批量任务列表,根据不同的操作类型(Register、Heartbeat、Cancel 等)分别调用 Resource 的接口进行处理
    • 如果是续约操作,会判断复制实例的 lastDirtyTimestamp 与本地实例的 lastDirtyTimestamp,如果是一致的,就任务数据一致
    • 如果复制实例的 lastDirtyTimestamp > 本地实例的 lastDirtyTimestamp,则复制实例的数据是最新的,返回 404(NOT_FOUND) 要求客户端重新发送一个注册操作过来
    • 如果复制实例的 lastDirtyTimestamp < 本地实例的 lastDirtyTimestamp,则本地实例的数据是最新的,返回 409(CONFLICT)和本地实例,客户端用返回来的实例覆盖本地的实例

下面再用一张图总结集群同步: