从源码一把聊透nacos2.x的任务执行引擎,满满干货分享!

背景:

在nacos2.x中,有许多任务要在后台调度执行,比如当服务发布者注册实例时,需要把实例同步到服务的调用者,此时,如果nacos设置的AP协议,考虑到性能问题,会包装成一个延时执行的任务异步执行,那在这种情况下就会有新的问题需要考虑:

  1. 这个任务发布时,上个任务还没执行完,这种任务堆积到底如何处理?
  2. 一部分任务需要延时执行,一部分任务需要立即执行,代码设计应该如何解耦?
  3. 当异步在真正要去执行的时候,执行慢,如何提高执行速度?

针对于上面的这些问题,nacos下沉了通用的技术底层,为nacos的业务功能提供统一的标准。

大家来做开发的时候,有没有考虑过使用分层设计的思想,去下沉通过的基础件,把经常变化的业务放在顶层,如果没有想过,那这篇可能会带给你很多思考!!

1.任务引擎

1.1. 引擎类继承图

从上面的类继承图可以看到,有两种任务执行引擎,共同继承AbstractNacosTaskExecuteEngine,AbstractNacosTaskExecuteEngine提供的特性为可以增加和删除任务处理器

  1. NacosExecuteTaskExecuteEngine,表示任务立即执行,里面会维护任务执行器,会把任务key相同的放到一个阻塞队列里面来执行,
  2. NacosDelayTaskExecuteEngine:表示任务延迟执行,里面会去任务Map,key为任务标识符,value为待执行的任务。

看到此处,你可能有点懵,别急,我们后面会全部聊透。

1.2. NacosTaskExecuteEngine

NacosTaskExecuteEngine的类如下,主要定义了增加移除任务以及任务处理器。

public interface NacosTaskExecuteEngine<T extends NacosTask> extends Closeable {
    
    /**
     * Get Task size in execute engine.
     * 获取任务大小
     * @return size of task
     */
    int size();
    
    /**
     * Whether the execute engine is empty.
     * 判断任务引擎是否没有任务执行
     * @return true if the execute engine has no task to do, otherwise false
     */
    boolean isEmpty();
    
    /**
     * Add task processor {@link NacosTaskProcessor} for execute engine.
     * 往任务引擎中添加处理类
     * @param key           key of task
     * @param taskProcessor task processor
     */
    void addProcessor(Object key, NacosTaskProcessor taskProcessor);
    
    /**
     * Remove task processor {@link NacosTaskProcessor} form execute engine for key.
     * 任务引擎移除处理类
     * @param key key of task
     */
    void removeProcessor(Object key);
    
    /**
     * Try to get {@link NacosTaskProcessor} by key, if non-exist, will return default processor.
     * 从任务引擎中找到合适的处理类,没有找到的话,将使用默认的处理类
     * @param key key of task
     * @return task processor for task key or default processor if task processor for task key non-exist
     */
    NacosTaskProcessor getProcessor(Object key);
    
    /**
     * Get all processor key.
     * 获取所有的处理类key
     * @return collection of processors
     */
    Collection<Object> getAllProcessorKey();
    
    /**
     * Set default task processor. If do not find task processor by task key, use this default processor to process
     * task.
     * 设置默认的处理类
     * @param defaultTaskProcessor default task processor
     */
    void setDefaultTaskProcessor(NacosTaskProcessor defaultTaskProcessor);
    
    /**
     * Add task into execute pool.
     * 往引擎中添加任务
     * @param key  key of task
     * @param task task
     */
    void addTask(Object key, T task);
    
    /**
     * Remove task.
     * 从引擎中删除任务
     * @param key key of task
     * @return nacos task
     */
    T removeTask(Object key);
    
    /**
     * Get all task keys.
     * 获取所有的任务Key
     * @return collection of task keys.
     */
    Collection<Object> getAllTaskKeys();
}

在这个类里面主要定义了任务处理器,任务的相关操作,再去看一下抽象基类的实现:

1.3. AbstractNacosTaskExecuteEngine

AbstractNacosTaskExecuteEngine 定义了NacosTaskProcessor在引擎内以Map的形式存在,并且可以通过key来获取processor,同时也会定义一个defaultTaskProcessor,表示当通过key获取NacosTaskProcessor为空时,默认使用defaultTaskProcessor。

public abstract class AbstractNacosTaskExecuteEngine<T extends NacosTask> implements NacosTaskExecuteEngine<T> {

    /**
     *  log日志
     */
    private final Logger log;

    /**
     * key为任务id value为任务处理器
     */
    private final ConcurrentHashMap<Object, NacosTaskProcessor> taskProcessors = new ConcurrentHashMap<>();

    /**
     * 默认任务处理器
     */
    private NacosTaskProcessor defaultTaskProcessor;
    
    public AbstractNacosTaskExecuteEngine(Logger logger) {
        this.log = null != logger ? logger : LoggerFactory.getLogger(AbstractNacosTaskExecuteEngine.class.getName());
    }
    
    @Override
    public void addProcessor(Object key, NacosTaskProcessor taskProcessor) {
        taskProcessors.putIfAbsent(key, taskProcessor);
    }
    
    @Override
    public void removeProcessor(Object key) {
        taskProcessors.remove(key);
    }
    
    @Override
    public NacosTaskProcessor getProcessor(Object key) {
        return taskProcessors.containsKey(key) ? taskProcessors.get(key) : defaultTaskProcessor;
    }
    
    @Override
    public Collection<Object> getAllProcessorKey() {
        return taskProcessors.keySet();
    }
    
    @Override
    public void setDefaultTaskProcessor(NacosTaskProcessor defaultTaskProcessor) {
        this.defaultTaskProcessor = defaultTaskProcessor;
    }
    
    protected Logger getEngineLog() {
        return log;
    }
}

主要干了两件事:

  1. 定义默认的任务处理器用来兜底,并且定义了任务处理器的集合。
  2. 用getProcessor(Object key)获取处理器的时候,如果没有任务key对应的任务处理器,会拿到默认的任务处理器。

1.4. NacosDelayTaskExecuteEngine

这个类的主要作用的延时任务执行引擎

public class NacosDelayTaskExecuteEngine extends AbstractNacosTaskExecuteEngine<AbstractDelayTask> {

    //定时任务线程池:负责处理tasks所关联的任务
    private final ScheduledExecutorService processingExecutor;
    
    protected final ConcurrentHashMap<Object, AbstractDelayTask> tasks;
    
    protected final ReentrantLock lock = new ReentrantLock();
    
    public NacosDelayTaskExecuteEngine(String name) {
        this(name, null);
    }
    
    public NacosDelayTaskExecuteEngine(String name, Logger logger) {
        this(name, 32, logger, 100L);
    }
    
    public NacosDelayTaskExecuteEngine(String name, Logger logger, long processInterval) {
        this(name, 32, logger, processInterval);
    }
    
    public NacosDelayTaskExecuteEngine(String name, int initCapacity, Logger logger) {
        this(name, initCapacity, logger, 100L);
    }
    
    public NacosDelayTaskExecuteEngine(String name, int initCapacity, Logger logger, long processInterval) {
        super(logger);
        tasks = new ConcurrentHashMap<>(initCapacity);
        processingExecutor = ExecutorFactory.newSingleScheduledExecutorService(new NameThreadFactory(name));
        processingExecutor
                .scheduleWithFixedDelay(new ProcessRunnable(), processInterval, processInterval, TimeUnit.MILLISECONDS);
    }
    
    @Override
    public AbstractDelayTask removeTask(Object key) {
        lock.lock();
        try {
            AbstractDelayTask task = tasks.get(key);
            if (null != task && task.shouldProcess()) {
                return tasks.remove(key);
            } else {
                return null;
            }
        } finally {
            lock.unlock();
        }
    }
    
    
    @Override
    public void shutdown() throws NacosException {
        tasks.clear();
        processingExecutor.shutdown();
    }
    
    @Override
    public void addTask(Object key, AbstractDelayTask newTask) {
        lock.lock();
        try {
            //根据key获取对应的任务
            AbstractDelayTask existTask = tasks.get(key);
            //如果对应的task有任务,调用新任务的merge方法,并且把就任务传进去
            //原因:因为是延迟任务执行引擎,因此当你下一个任务来之后,上一个任务不一定能消费完
            if (null != existTask) {
                newTask.merge(existTask);
            }
            tasks.put(key, newTask);
        } finally {
            lock.unlock();
        }
    }
    
    /**
     * process tasks in execute engine.
     */
    protected void processTasks() {
        Collection<Object> keys = getAllTaskKeys();
        //循环获取所有的待执行任务
        for (Object taskKey : keys) {
            AbstractDelayTask task = removeTask(taskKey);
            if (null == task) {
                continue;
            }
            //获取对应的任务执行器
            NacosTaskProcessor processor = getProcessor(taskKey);
            if (null == processor) {
                getEngineLog().error("processor not found for task, so discarded. " + task);
                continue;
            }
            //如果任务失败了,会把任务放回到待执行的任务map中,直到任务执行完
            try {
                // ReAdd task if process failed
                if (!processor.process(task)) {
                    retryFailedTask(taskKey, task);
                }
            } catch (Throwable e) {
                getEngineLog().error("Nacos task execute error ", e);
                retryFailedTask(taskKey, task);
            }
        }
    }
    
    private void retryFailedTask(Object key, AbstractDelayTask task) {
        task.setLastProcessTime(System.currentTimeMillis());
        addTask(key, task);
    }
    
    private class ProcessRunnable implements Runnable {
        
        @Override
        public void run() {
            try {
                processTasks();
            } catch (Throwable e) {
                getEngineLog().error(e.toString(), e);
            }
        }
    }
}

主要干了三件事:

  1. 接收外部传进来的任务,并且以传进来的任务标识符为键,task任务为值,存到任务map之中。
  2. 会起一个线程池,轮询map里面待执行的任务,并且通过标识符获取对应的任务执行器,获取完之后执行对应的任务。
  3. 当放入任务的时候,任务map中对应的任务标识符已存在,会调用新任务的merge方法,对就任务做合并。

接下来我们去看下延时任务合并和任务失败重试具体如何做?冲!!!

1.4.1. 延时任务合并

public void addTask(Object key, AbstractDelayTask newTask) {
    lock.lock();
    try {
        //根据key获取对应的任务
        AbstractDelayTask existTask = tasks.get(key);
        //如果对应的task有任务,调用新任务的merge方法,并且把就任务传进去
        //原因:因为是延迟任务执行引擎,因此当你下一个任务来之后,上一个任务不一定能消费完
        if (null != existTask) {
            newTask.merge(existTask);
        }
        tasks.put(key, newTask);
    } finally {
        lock.unlock();
    }
}

在上面代码会发现,当添加任务时,会判断当前上一个任务是否执行完,如果还没有执行完,会调用事件的merge方法,这个正是一个很大的亮点,他把合并的细节交给事件本身去处理,调度引擎只实现任务的编排,实现了很好的解耦。这也就解决了开头提出的第一个问题:当任务发布时,上个任务还没执行完,这种任务堆积到底如何处理?

1.4.2. 任务失败重试

大家可以先去思考一个问题:

由于在延时任务中使用了Map,就会导致一个问题,当任务发布完之后,由于任务执行的异步的,那任务出错了怎么办?假如直接报异常,肯定不合适,因为有些任务处理时可能会有偶发异常,比如调用网络,怎么也得重试下吧,那问题来了,重试几次呢?在nacos里面的做法是我去一直重试,知道成功为止。

protected void processTasks() {
    Collection<Object> keys = getAllTaskKeys();
    //循环获取所有的待执行任务
    for (Object taskKey : keys) {
        AbstractDelayTask task = removeTask(taskKey);
        if (null == task) {
            continue;
        }
        //获取对应的任务执行器
        NacosTaskProcessor processor = getProcessor(taskKey);
        if (null == processor) {
            getEngineLog().error("processor not found for task, so discarded. " + task);
            continue;
        }
        //如果任务失败了,会把任务放回到待执行的任务map中,直到任务执行完
        try {
            // ReAdd task if process failed
            if (!processor.process(task)) {
                retryFailedTask(taskKey, task);
            }
        } catch (Throwable e) {
            getEngineLog().error("Nacos task execute error ", e);
            retryFailedTask(taskKey, task);
        }
    }
}

在上面的代码会发现:当任务处理失败之后或者调用processor.process(task)返回false之后,会调用retryFailedTask(taskKey, task)重试任务,重点来了,我们看下retryFailedTask方法。

private void retryFailedTask(Object key, AbstractDelayTask task) {
    //设置任务的上次处理时间
    task.setLastProcessTime(System.currentTimeMillis());
    //再次放入任务执行引擎之中
    addTask(key, task);
}

会重新设置上次任务的处理时间,再放回到执行引擎之中。

此刻,尴尬了,这要一直失败,一直执行,后面任务全阻塞了咋办呀?别慌,那接下来去看下不断失败重试的任务如何处理呢?其实在这块可以在定义的processor.process(NacosTask task)中做处理,比如不想做重试直接返回true,这样就默认执行成功了,如果想再次执行,可以返回false,就可以做定制化区分。

稳住,坚持就是胜利!!继续分析

当延时任务真正要执行的时候,会交给任务立即执行引擎,也就是NacosExecuteTaskExecuteEngine这个类,

那这个类主要干了哪些事呢?继续往下看

1.5. NacosExecuteTaskExecuteEngine

1.5.1 主要功能

public class NacosExecuteTaskExecuteEngine extends AbstractNacosTaskExecuteEngine<AbstractExecuteTask> {

    //具体执行的worker
    private final TaskExecuteWorker[] executeWorkers;
    
    public NacosExecuteTaskExecuteEngine(String name, Logger logger) {
        this(name, logger, ThreadUtils.getSuitableThreadCount(1));
    }

    //初始化worker执行器,当不指定时,默认并行度为1
    public NacosExecuteTaskExecuteEngine(String name, Logger logger, int dispatchWorkerCount) {
        super(logger);
        executeWorkers = new TaskExecuteWorker[dispatchWorkerCount];
        for (int mod = 0; mod < dispatchWorkerCount; ++mod) {
            executeWorkers[mod] = new TaskExecuteWorker(name, mod, dispatchWorkerCount, getEngineLog());
        }
    }
    
    @Override
    public int size() {
        int result = 0;
        for (TaskExecuteWorker each : executeWorkers) {
            result += each.pendingTaskCount();
        }
        return result;
    }
    
    @Override
    public boolean isEmpty() {
        return 0 == size();
    }
    
    @Override
    public void addTask(Object tag, AbstractExecuteTask task) {
        //如果有对应的任务处理器,则拿出来执行
        NacosTaskProcessor processor = getProcessor(tag);
        if (null != processor) {
            processor.process(task);
            return;
        }
        //如果没有对应的执行器,默认使用worker执行
        TaskExecuteWorker worker = getWorker(tag);
        worker.process(task);
    }

    private TaskExecuteWorker getWorker(Object tag) {
        //去hash值和worker数量取模,得到对应的执行器
        int idx = (tag.hashCode() & Integer.MAX_VALUE) % workersCount();
        return executeWorkers[idx];
    }
    
    private int workersCount() {
        return executeWorkers.length;
    }
    
    @Override
    public AbstractExecuteTask removeTask(Object key) {
        throw new UnsupportedOperationException("ExecuteTaskEngine do not support remove task");
    }
    
    @Override
    public Collection<Object> getAllTaskKeys() {
        throw new UnsupportedOperationException("ExecuteTaskEngine do not support get all task keys");
    }
    
    @Override
    public void shutdown() throws NacosException {
        for (TaskExecuteWorker each : executeWorkers) {
            each.shutdown();
        }
    }
    
    /**
     * Get workers status.
     * 获取执行器的状态
     * @return workers status string
     */
    public String workersStatus() {
        StringBuilder sb = new StringBuilder();
        for (TaskExecuteWorker worker : executeWorkers) {
            sb.append(worker.status()).append('\n');
        }
        return sb.toString();
    }
}

功能如下:

  1. 当把任务添加进来的时候,会根据标识符获取对应的NacosTaskProcessor,如果存在,则执行NacosTaskProcessor的processor方法,否则放入TaskExecuteWorker执行。
  2. TaskExecuteWorker内部维护一个队列,主要用来缓存投递进来的任务,并且会启动一个线程池去消费。

1.5.2. 任务并行度

这块有个任务并行度的注意点:

在类里面创建TaskExecuteWorker数组的时候,默认的大小为1,在getWorker(tag)的时候,只会获取数组里面的第一个,意味着这块只会拿到同一个TaskExecuteWorker,任务的并行度就是1.

private TaskExecuteWorker getWorker(Object tag) {
    //去hash值和worker数量取模,得到对应的执行器
    int idx = (tag.hashCode() & Integer.MAX_VALUE) % workersCount();
    return executeWorkers[idx];
}

如果想提升任务并行度怎么办呢?可以使用下面这个构造函数:

    public NacosExecuteTaskExecuteEngine(String name, Logger logger, int dispatchWorkerCount) {
        super(logger);
        executeWorkers = new TaskExecuteWorker[dispatchWorkerCount];
        for (int mod = 0; mod < dispatchWorkerCount; ++mod) {
            executeWorkers[mod] = new TaskExecuteWorker(name, mod, dispatchWorkerCount, getEngineLog());
        }
    }

在提升并行度的时候不用担心同一类型任务顺序会乱,因为这块是按照任务key获取的TaskExecuteWorker,说明同一类型的任务时串行的。

那TaskExecuteWorker具体如何执行的呢? 听我细细道来

1.5.3. TaskExecuteWorker

public final class TaskExecuteWorker implements NacosTaskProcessor, Closeable {
    

    //任务阻塞队列
    private final BlockingQueue<Runnable> queue;
    
    //内部worker消费者
    private final InnerWorker realWorker;
    
    
    @Override
    public boolean process(NacosTask task) {
        if (task instanceof AbstractExecuteTask) {
            putTask((Runnable) task);
        }
        return true;
    }
    
    private void putTask(Runnable task) {
        try {
            queue.put(task);
        } catch (InterruptedException ire) {
            log.error(ire.toString(), ire);
        }
    }
    
    /**
     * Inner execute worker.
     */
    private class InnerWorker extends Thread {
        
        InnerWorker(String name) {
            setDaemon(false);
            setName(name);
        }
        
        @Override
        public void run() {
            while (!closed.get()) {
                try {
                    Runnable task = queue.take();
                    long begin = System.currentTimeMillis();
                    task.run();
                    long duration = System.currentTimeMillis() - begin;
                    if (duration > 1000L) {
                        log.warn("task {} takes {}ms", task, duration);
                    }
                } catch (Throwable e) {
                    log.error("[TASK-FAILED] " + e, e);
                }
            }
        }
    }
}

主要功能:

  1. 在添加任务时,会把任务放到阻塞队列BlockingQueue。
  2. 同时会启动一个线程,去消费队列里面的任务。

1.6. PushDelayTaskExecuteEngine

推送任务延迟执行引擎,继承了NacosDelayTaskExecuteEngine,拥有延迟引擎的特性,并且会设置默认的NacosTaskProcessor,这个默认的NacosTaskProcessor为PushDelayTaskProcessor,这个processor的作用会把任务推送给nacos立即任务执行引擎执行,源码如下:

public class PushDelayTaskExecuteEngine extends NacosDelayTaskExecuteEngine {

    private final ClientManager clientManager;
    
    private final ClientServiceIndexesManager indexesManager;
    
    private final ServiceStorage serviceStorage;
    
    private final NamingMetadataManager metadataManager;

    private final PushExecutor pushExecutor;
    
    private final SwitchDomain switchDomain;
    
    public PushDelayTaskExecuteEngine(ClientManager clientManager, ClientServiceIndexesManager indexesManager,
                                      ServiceStorage serviceStorage, NamingMetadataManager metadataManager,
                                      PushExecutor pushExecutor, SwitchDomain switchDomain) {
        super(PushDelayTaskExecuteEngine.class.getSimpleName(), Loggers.PUSH);
        this.clientManager = clientManager;
        this.indexesManager = indexesManager;
        this.serviceStorage = serviceStorage;
        this.metadataManager = metadataManager;
        this.pushExecutor = pushExecutor;
        this.switchDomain = switchDomain;
        //设置默认的任务处理器
        setDefaultTaskProcessor(new PushDelayTaskProcessor(this));
    }
    
    @Override
    protected void processTasks() {
        if (!switchDomain.isPushEnabled()) {
            return;
        }
        super.processTasks();
    }
    
    private static class PushDelayTaskProcessor implements NacosTaskProcessor {
        
        private final PushDelayTaskExecuteEngine executeEngine;
        
        public PushDelayTaskProcessor(PushDelayTaskExecuteEngine executeEngine) {
            this.executeEngine = executeEngine;
        }
        
        @Override
        public boolean process(NacosTask task) {
            //在处理的时候,会把任务传递给nacos执行引擎
            PushDelayTask pushDelayTask = (PushDelayTask) task;
            Service service = pushDelayTask.getService();
            NamingExecuteTaskDispatcher.getInstance()
                    .dispatchAndExecuteTask(service, new PushExecuteTask(service, executeEngine, pushDelayTask));
            return true;
        }
    }
}

这个引擎类的功能主要是:当一个client向nacos注册时,只要实例写到了注册表就完了,具体的把这些注册的实例同步给订阅者,其实都是通过PushDelayTaskExecuteEngine这个引擎来异步执行的,这个在我们后面去聊源码都会聊到!!

1.7. 延迟执行引擎和立即执行引擎的区别

  1. 延迟执行引擎:表示我这个任务不急着执行,可能会有后面的任务来,就会触发任务合并。
  2. 普通执行引擎:任务在执行的时候,如果不指定processor时,会放到TaskExecuteWorker中执行。

2. 任务引擎怎么用

假设我是一个调用者,我想要去使用任务引擎,可以参照下面的流程去

  1. 创建一个延时任务引擎
  2. 创建一个指定标识的任务处理器
  3. 创建一个默认的处理器
  4. 添加指定标识的任务。
public static void main(String[] args) throws InterruptedException {
    NacosDelayTaskExecuteEngine nacosDelayTaskExecuteEngine = new NacosDelayTaskExecuteEngine(NacosDelayTaskExecuteEngine.class.getName());

    //添加指定标识的处理器
    nacosDelayTaskExecuteEngine.addProcessor("test", new NacosTaskProcessor() {
      @Override
      public boolean process(NacosTask task) {
        System.out.println("specific processor执行");
        return true;
      }
    });

    //添加默认的处理器
    nacosDelayTaskExecuteEngine.setDefaultTaskProcessor(new NacosTaskProcessor() {
      @Override
      public boolean process(NacosTask task) {
        System.out.println("default processor执行");
        return true;
      }
    });

    //添加对应的任务
    nacosDelayTaskExecuteEngine.addTask("test", new AbstractDelayTask() {
      @Override
      public void merge(AbstractDelayTask task) {

      }
    });

    //添加对应的任务
    nacosDelayTaskExecuteEngine.addTask("test1", new AbstractDelayTask() {
      @Override
      public void merge(AbstractDelayTask task) {

      }
    });

    Thread.sleep(3000L);
  }

输出结果

第一个任务有标识对应的任务处理器,因此会打印specific processor执行

第二个任务没有标识对应的处理器,因此会打印default processor执行

大家此时有没有对任务执行引擎有深一点的理解,我们在之后会去聊到它在nacos源码中是如何用的,那会你返回来再去看这块,你会有更深的理解!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值