文章目录
背景:
在nacos2.x中,有许多任务要在后台调度执行,比如当服务发布者注册实例时,需要把实例同步到服务的调用者,此时,如果nacos设置的AP协议,考虑到性能问题,会包装成一个延时执行的任务异步执行,那在这种情况下就会有新的问题需要考虑:
- 这个任务发布时,上个任务还没执行完,这种任务堆积到底如何处理?
- 一部分任务需要延时执行,一部分任务需要立即执行,代码设计应该如何解耦?
- 当异步在真正要去执行的时候,执行慢,如何提高执行速度?
…
针对于上面的这些问题,nacos下沉了通用的技术底层,为nacos的业务功能提供统一的标准。
大家来做开发的时候,有没有考虑过使用分层设计的思想,去下沉通过的基础件,把经常变化的业务放在顶层,如果没有想过,那这篇可能会带给你很多思考!!
1.任务引擎
1.1. 引擎类继承图
从上面的类继承图可以看到,有两种任务执行引擎,共同继承AbstractNacosTaskExecuteEngine,AbstractNacosTaskExecuteEngine提供的特性为可以增加和删除任务处理器
- NacosExecuteTaskExecuteEngine,表示任务立即执行,里面会维护任务执行器,会把任务key相同的放到一个阻塞队列里面来执行,
- 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;
}
}
主要干了两件事:
- 定义默认的任务处理器用来兜底,并且定义了任务处理器的集合。
- 用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);
}
}
}
}
主要干了三件事:
- 接收外部传进来的任务,并且以传进来的任务标识符为键,task任务为值,存到任务map之中。
- 会起一个线程池,轮询map里面待执行的任务,并且通过标识符获取对应的任务执行器,获取完之后执行对应的任务。
- 当放入任务的时候,任务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();
}
}
功能如下:
- 当把任务添加进来的时候,会根据标识符获取对应的NacosTaskProcessor,如果存在,则执行NacosTaskProcessor的processor方法,否则放入TaskExecuteWorker执行。
- 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);
}
}
}
}
}
主要功能:
- 在添加任务时,会把任务放到阻塞队列BlockingQueue。
- 同时会启动一个线程,去消费队列里面的任务。
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. 延迟执行引擎和立即执行引擎的区别
- 延迟执行引擎:表示我这个任务不急着执行,可能会有后面的任务来,就会触发任务合并。
- 普通执行引擎:任务在执行的时候,如果不指定processor时,会放到TaskExecuteWorker中执行。
2. 任务引擎怎么用
假设我是一个调用者,我想要去使用任务引擎,可以参照下面的流程去
- 创建一个延时任务引擎
- 创建一个指定标识的任务处理器
- 创建一个默认的处理器
- 添加指定标识的任务。
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源码中是如何用的,那会你返回来再去看这块,你会有更深的理解!!