Netflix Eureka - 注册表同步使用的三层队列批处理机制

320 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。


Netflix Eureka 目录汇总

  1. eureka server启动以及初始化

  2. eureka client启动以及初始化

  3. 服务注册

    3.1 可重入读写锁-读锁

  4. 服务发现

    4.1. 全量抓取注册表 4.2. 注册表多级缓存机制 4.3. 注册表多级缓存过期机制(主动、定时、被动) 4.4. 增量抓取注册表

     4.4.1. 一致性Hash对比机制
     4.4.2. 可重入读写锁-写锁
    
  5. 服务续约

  6. 服务下线

  7. 服务故障自动感知及服务实例自动摘除

  8. 自我保护机制

  9. Eureka Server集群注册表同步及高可用

  10. Eureka Server集群之间注册表同步使用的三层队列机制


Netflix Eureka 时间间隔简要

服务注册(延迟40秒执行注册逻辑)

服务发现-

读写缓存-定时过期(180秒)

只读缓存-被动过期(30秒(整30秒))

定时抓取增量注册表(30秒)

定时删除超过3分钟的服务实例变更记录(30秒)

服务续约(30秒)

服务下线

服务故障自动感知及服务实例自动摘除(感知:90秒(BUG:90 * 2秒);摘除:60秒)

自我保护机制

每分钟期望的续约次数 & 每分钟期望的续约次数阈值(15分钟)

MeasuredRate renewsLastMin(60秒)

Eureka Server集群信息更新(10分钟)

注册表同步使用的三层队列机制


注册表同步的3层队列任务批处理机制

组件简要 - 实例

batchingDispatcher:

  1. 构造AcceptorExecutor实例。
  2. 构造TaskExecutors实例,通过TaskExecutors.batchExecutors()方法。包含AcceptorExecutorReplicationTaskProcessor实例。
  3. 构造TaskDispatcher实例,通过TaskDispatchers.createBatchingTaskDispatcher()方法。

batchingDispatcher.process():

  1. 封装实例变化信息,通过acceptorExecutor.process放入acceptorQueue(LinkedBlockingQueue)

AcceptorRunner:

  1. 构造AcceptorExecutor实例时,启动AcceptorRunner任务。
  2. acceptorQueue队列数据全部放入processingOrder队列中(processingOrder只是存放taskId集合的链表,pendingTasks才是真正存放acceptorQueue队列里的任务)

acceptorQueue:接收服务实例状态变更任务的队列

TaskExecutors:构造器内通过WorkerRunnableFactory工厂获取BatchWorkerRunnable,并启动BatchWorkerRunnable任务的线程。

ReplicationTaskProcessor:

ProcessingResult :处理任务结果。

  1. Success:成功。

  2. Congestion:拥挤错误,任务将会被重试。例如,请求被限流。

  3. TransientError:瞬时错误,任务将会被重试。例如,网络请求超时。

  4. PermanentError:永久错误,任务将会被丢弃。例如,执行时发生程序异常。



组件简要 - 类

任务分发器:分发器在收到任务执行请求后,提交到接收队列,任务实际未执行。

TaskDispatcher:

    批量任务分发器:

    单任务分发器:

任务接收器

AcceptorExecutor:

AcceptorRunner:

任务执行器

TaskExecutors:

WorkerRunnableFactory:

    WorkerRunnable:

        BatchWorkerRunnable:

        SingleTaskWorkerRunnable:

任务处理器

TaskProcessor:

    ReplicationTaskProcessor:

任务持有者

TaskHolder:

三层队列批处理

第一层队列:接收队列(acceptorQueue),避免处理任务的阻塞等待;重处理队列(reprocessQueue),存放任务提交失败的请求。

    /**
     * 存放服务实例状态变更命令对象的TaskHolder结构体
     */
    private final BlockingQueue<TaskHolder<ID, T>> acceptorQueue = new LinkedBlockingQueue<>();
    /**
     * 存放请求提交失败的任务,随着acceptorQueue一起,重新放入处理队列(processingQueue)
     */
    private final BlockingDeque<TaskHolder<ID, T>> reprocessQueue = new LinkedBlockingDeque<>();

第二层队列:处理队列,接收线程( AcceptorRunner )合并任务,将相同任务编号的任务合并,只执行一次。

    /**
     * 转存从acceptorQueue队列移过来的命令对象
     */
    private final Map<ID, TaskHolder<ID, T>> pendingTasks = new HashMap<>();
    /**
     * 顺序存放acceptorQueue队列移过来的命令对象taskId。关联pendingTasks,作为索引使用。
     * 为啥不使用LinkedHashMap???
     */
    private final Deque<ID> processingOrder = new LinkedList<>();

第三个队列:存放批处理任务,等批量处理任务poll处理

    /**
     * 根据数量及时间打包批量任务
     */
    private final BlockingQueue<List<TaskHolder<ID, T>>> batchWorkQueue =
            new LinkedBlockingQueue<>();

数据流

第一层队列:acceptorQueue

// PeerEurekaNode.java
/**
 * Sends the registration information of {@link InstanceInfo} receiving by
 * this node to the peer node represented by this class.
 *
 * @param info the instance information {@link InstanceInfo} of any instance
 *             that is send to this instance.
 * @throws Exception
 */
public void register(final InstanceInfo info) throws Exception {
    long expiryTime = System.currentTimeMillis() + getLeaseRenewalOf(info);
    // 调用批处理分发器,封装注册、下线、心跳、故障等行为到命令对象中
    batchingDispatcher.process(
            taskId("register", info),
            // 封装命令(实例同步任务)
            new InstanceReplicationTask(targetHost, Action.Register, info, null, true) {
                @Override
                public EurekaHttpResponse<Void> execute() {
                    return replicationClient.register(info);
                }
            },
            expiryTime
    );
}
// TaskDispatchers.java
/**
 * 任务分发器
 */
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();
    }
};
// AcceptorExecutor.java
void process(ID id, T task, long expiryTime) {
    // 1. 接收任务放到接收队列(acceptorQueue)
    acceptorQueue.add(new TaskHolder<ID, T>(id, task, expiryTime));
    acceptedTasks++;
}

第一层队列:reprocessQueue

// TaskExecutors.java
// 提交批处理任务
ProcessingResult result = processor.process(tasks);
switch (result) {
    case Success:
        break;
    case Congestion:
    case TransientError:
        // 批处理任务返回 瞬断错误(网络)时,需要放入reprocessQueue队列中,重新发送
        taskDispatcher.reprocess(holders, result);
        break;
    case PermanentError:
        logger.warn("Discarding {} tasks of {} due to permanent error", holders.size(), workerName);
}
// AcceptorExecutor.java
void reprocess(List<TaskHolder<ID, T>> holders, ProcessingResult processingResult) {
    // 4(1). 接收请求失败的任务放入重处理队列(reprocessQueue)
    reprocessQueue.addAll(holders);
    replayedTasks += holders.size();
    trafficShaper.registerFailure(processingResult);
}

第二层队列:processingOrder & pendingTasks

注意:
     batchWorkRequests信号量初始是没有权限的,需要等待TaskExecutors$BatchWorkerRunnable
     任务线程赋予权限,assignBatchWork才会真正的分配【批处理任务】
// AcceptorExecutor.java
/**
 * 1. 合并【第一层队列】,迁移到【第二层队列】
 * 2. 合并【任务】打包为【批处理任务】
 */
class AcceptorRunner implements Runnable {
    @Override
    public void run() {
        long scheduleTime = 0;
        while (!isShutdown.get()) {
            try {
                // 2. 将acceptorQueue与reprocessQueue队列的任务迁移到pendingTasks中
                drainInputQueues();

                int totalItems = processingOrder.size();

                long now = System.currentTimeMillis();
                if (scheduleTime < now) {
                    scheduleTime = now + trafficShaper.transmissionDelay();
                }
                if (scheduleTime <= now) {
                    // 3. 将任务(holder)合并成批量任务(holders),提交到工作队列(batchWorkQueue)
                    assignBatchWork();
                    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);
            }
        }
    }

第三层队列:batchWorkRequests

// TaskExecutors$BatchWorkerRunnable.java
/**
 * 获取批处理任务,并提交任务
 */
@Override
public void run() {
    try {
        while (!isShutdown.get()) {
            // 给予信号量【batchWorkRequests】权限,并获取【批处理任务】
            List<TaskHolder<ID, T>> holders = getWork();
            metrics.registerExpiryTimes(holders);

            List<T> tasks = getTasksOf(holders);
            // 提交批处理任务
            ProcessingResult result = processor.process(tasks);
            switch (result) {
                case Success:
                    break;
                case Congestion:
                case TransientError:
                    // 批处理任务返回 瞬断错误(网络)时,需要放入reprocessQueue队列中,重新发送
                    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);
    }
}
// TaskExecutors$BatchWorkerRunnable.java
/**
 * 给予信号量【batchWorkRequests】权限,并获取【批处理任务】
 *
 * @return
 * @throws InterruptedException
 */
private List<TaskHolder<ID, T>> getWork() throws InterruptedException {
    // 给予信号量【batchWorkRequests】权限
    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;
}
// AcceptorExecutor.java
/**
 * 给予信号量【batchWorkRequests】权限,并返回【批处理任务队列】的引用
 * @return
 */
BlockingQueue<List<TaskHolder<ID, T>>> requestWorkItems() {
    batchWorkRequests.release();
    return batchWorkQueue;
}
// ReplicationTaskProcessor.java
/**
 * 同步任务处理器,处理批处理任务
 *
 * @param tasks
 * @return
 */
@Override
public ProcessingResult process(List<ReplicationTask> tasks) {
    ReplicationList list = createReplicationListOf(tasks);
    try {
        // 提交批处理任务
        EurekaHttpResponse<ReplicationListResponse> response =
                replicationClient.submitBatchUpdates(list);
        int statusCode = response.getStatusCode();
        if (!isSuccess(statusCode)) {
            if (statusCode == 503) {
                logger.warn("Server busy (503) HTTP status code received from the peer {}; " +
                        "rescheduling tasks after delay", peerId);
                return ProcessingResult.Congestion;
            } else {
                // Unexpected error returned from the server. This should ideally never happen.
                logger.error("Batch update failure with HTTP status code {}; discarding {} " +
                        "replication tasks", statusCode, tasks.size());
                return ProcessingResult.PermanentError;
            }
        } else {
            handleBatchResponse(tasks, response.getEntity().getResponseList());
        }
    } catch (Throwable e) {
        if (isNetworkConnectException(e)) {
            logNetworkErrorSample(null, e);
            return ProcessingResult.TransientError;
        } else {
            logger.error("Not re-trying this exception because it does not seem to be a " +
                    "network exception", e);
            return ProcessingResult.PermanentError;
        }
    }
    return ProcessingResult.Success;
}