任务调度系统
Is Aliang
1. 系统概述
本任务调度系统采用分布式架构,由任务调度中心和多个执行器(Runner)组成,实现了任务的分布式处理和状态管理。系统通过心跳
机制维持调度中心与执行器之间的通信,确保任务的可靠执行和状态同步。
2. 系统组件
2.1 任务调度中心(Scheduler)
负责任务的统一管理、分配和状态监控
维护所有任务的状态信息(队列、运行、完成、失败等)
管理执行器的注册和状态跟踪
2.2 执行器(Runner)
负责具体任务的执行
定期向调度中心发送心跳,报告自身状态
执行分配的任务并反馈执行结果
2.3 任务选择器(Picker)
根据指定规则,选择处于队列中的任务返回给调度中心,调度中心再下发任务到执行器
2.4 任务回调器(callback-er)
执行器执行完任务后,任务会被设置callback等待回调状态,回调器定时查看等待回调任务,并执行后续结果
3. 任务调度流程
3.1 任务创建阶段
用户或系统创建新任务
任务被置入调度中心的任务队列中
任务初始状态标记为"队列状态"(QUEUED)
3.2 执行器状态管理
执行器通过心跳机制向调度中心报告以下状态:
IDLE(空闲):执行器无任务执行,可接收新任务
RUNNING(运行):执行器正在执行任务
FINISHED(完成):任务执行成功完成
FAILED(失败):任务执行失败
3.3 任务分配机制
调度中心定期检查各执行器状态
将任务分配给处于IDLE状态的执行器
每个执行器只会处理一个任务,处理完成才会接受新的任务
3.4 任务执行流程
任务下发:调度中心向IDLE状态的执行器发送RUN_NEW指令及任务信息
任务启动:执行器接收任务,创建子进程执行具体任务。
状态监控:执行器通过心跳持续向调度中心报告任务执行状态,执行过程中,执行器会每过30秒发送心跳,同时执行器会每过1秒检查
任务是否结束,任务结束则发送心跳。
结果收集:任务完成后,执行器将执行结果(标准输出、错误日志等)存储并上报,同时调度中心设置任务为“callback”状态,由任
务回调器获取结果并处理相应逻辑。
3.5 心跳通信机制
执行器以固定间隔(30秒)向调度中心发送心跳包。执行器执行任务时,每过1秒会检查任务是否结束,任务结束会发送一次心跳包。
心跳包包含:
执行器标识(RUNNER_ID)
当前任务标识(CURRENT_TASK_ID)
执行器状态(IDLE 空闲, FINISHED 完成, RUNNING 运行中, FAILED 失败)
其他信息:例如执行器配置,版本等
调度中心根据心跳信息决定下发指令:
GO_ON:维持当前状态
RESET:重置执行器状态
RUN_NEW:分配新任务
4. 异常处理机制
4.1 runner开启任务异常
描述:分配runner任务后,任务状态为DISPATCHED,runner上报心跳状态为IDLE,可能原因runner开启子进程运行任务失败。
解决:scheduler根据当前runnerID和任务状态DISPATCHED,获取该任务后,设置任务失败。
4.2 runner运行任务过程中异常
描述:runner上报心跳为IDEL,分配的任务的状态为RUNNING
解决:scheduler根据当前runnerID和任务状态RUNNING,获取该任务后,设置任务失败。
4.3 手动终止运行中的任务
描述:runner上报心跳为RUNNING,分配的任务的状态为TERMINATING
解决:scheduler向runner发送RESET指令,runner重置后上报心跳为IDEL,scheduler再根据当前runnerID和任务状态TERMINATING,
获取该任务后,设置任务终止。
4.4 任务执行失败
描述:runner上报心跳为FILED
解决:scheduler设置任务失败,向runner发送RESET指令
4.5 runner失联
描述:runner未按规定时间上报心跳
解决:分配任务的runner上报心跳时,scheduler设置当前任务的lastSeenTime为nowTime,表示最后监测时间。
scheduler开启定时轮询程序,每过10秒查看所有运行中的任务的最后监测时间是否已经过去N个心跳时间(自己定义合适时间),超过
指定值,则判定runner宕机,设置任务失败
4.6 scheduler失联
描述:runner无法上报心跳
解决:runner保持GO_ON状态
5. 数据流向
任务创建 → 任务队列 → 调度中心 → 执行器分配 → 任务执行 → 结果收集 → 状态更新
6.实现
6.1 Runner状态枚举
1 public enum RunnerStatus { 2 IDLE, //空闲 3 FINISHED, //完成 4 RUNNING, //运行中 5 FAILED //失败 6 }
6.2 Task状态枚举
1 public enum TaskStatus { 2 CREATED, // 任务初始状态,表示任务已创建但尚未开始执行。 3 QUEUED, // 表示任务已加入队列,等待调度执行。 4 DISPATCHED, // 表示任务已被调度,准备运行。 5 RUNNING, // 表示任务正在执行中。 6 CALLBACK, // 表示任务已完成执行,正在回调通知。 7 FINISHED, // 表示任务已成功完成所有步骤。 8 FAILED, // 表示任务执行过程中发生错误,任务失败。 9 SKIPPED, // 表示任务被跳过,未执行。 10 TERMINATING, // 表示任务正在终止,处于终止过程中。 11 TERMINATED; // 表示任务已完全终止,终止过程已完成。 12 }
6.3 Scheduler命令枚举
1 public enum ScheduleCommand { 2 GO_ON, // 继续执行 3 RESET, // 重置任务 4 RUN_NEW // 运行新任务 5 }
6.4 Runner的设计
6.4.1 Runner类
Runner类记录了当前配置信息,当前执行的任务id,当前任务进程。首次启动时Runner时,向Scheduler上报心跳,获取任务。
1 /** 2 * 执行器 3 * 负责与调度器通信并执行任务 4 * 执行器状态:IDLE | RUNNING | FINISHED | FAILED 5 */ 6 public class Runner { 7 private final String RUNNER_ID;// Runner ID 8 private final List<String> RUNNER_CONFIG;// Runner 配置(用于判断任务是否可以在此执行器上执行) 9 private final int VERSION = 1;// Runner 版本 10 private final String SCHEDULER_URL;// 调度器地址 11 private Integer currentTask = null;// 当前任务ID 12 private Process process = null;// 当前任务进程 13 private FileOutputStream stdout = null;// 当前任务的标准输出和标准错误文件 14 private FileOutputStream stderr = null;// 当前任务的标准输出和标准错误文件 15 private final HttpClient client; 16 private final Gson gson; 17 private final Logger logger; 18 19 /** 20 * 构造函数初始化Runner实例 21 */ 22 public Runner(String runnerId, String schedulerUrl, List<String> runnerConfig) { 23 this.RUNNER_ID = runnerId; 24 this.SCHEDULER_URL = schedulerUrl; 25 this.RUNNER_CONFIG = new ArrayList<>(runnerConfig); 26 this.client = HttpClient.newBuilder() 27 .connectTimeout(Duration.ofSeconds(10)) 28 .build(); 29 this.gson = new Gson(); 30 this.logger = LoggerFactory.getLogger(Runner.class.getName()); 31 } 32 33 /** 34 * 启动Runner主循环 35 */ 36 public void start() { 37 while (true) { 38 CommandTaskPair result = heartbeat(); //发送心跳获取指令 39 String command = result.command; 40 Task task = result.task; 41 42 if ("GO_ON".equals(command)) { 43 // 没有运行任务时等待30秒发送下一个心跳 44 if (process == null) { 45 try { 46 Thread.sleep(30000); 47 } catch (InterruptedException e) { 48 Thread.currentThread().interrupt(); 49 break; 50 } 51 continue; 52 } 53 54 // 运行任务时每秒检查一次任务是否结束, 任务结束后发送下一个心跳 55 // 最多等待30秒发送下个心跳 56 for (int i = 0; i < 30; i++) { 57 try { 58 process.exitValue(); 59 break; 60 } catch (IllegalThreadStateException e) { 61 try { 62 Thread.sleep(1000); 63 } catch (InterruptedException ie) { 64 Thread.currentThread().interrupt(); 65 break; 66 } 67 } 68 } 69 } else if ("RESET".equals(command)) { 70 reset(); 71 } else if ("RUN_NEW".equals(command)) { 72 runNew(task); 73 } 74 } 75 } 76 77 78 /** 79 * 发送心跳到调度器,获取指令 80 * 81 * @return 包含指令和任务的元组 82 */ 83 private CommandTaskPair heartbeat() {...} 84 85 /** 86 * 重置当前任务状态 87 */ 88 public void reset() {...} 89 90 /** 91 * 运行新任务 92 * 93 * @param task 任务对象 94 */ 95 public void runNew(Task task) {...} 96 }
6.4.2 发送心跳
a. 根据子进程状态判断执行器状态:
子进程为null——>执行器IDEL;
子进程退出码0——>执行器FINISHED;
子进程退出码非0 ——>执行器FAILED;
获取退出码报错——>执行器RUNNING
b. 构建心跳数据,发送给Scheduler
c. 获取响应命令及派遣任务,通信失败时,执行GO_ON命令
1 /** 2 * 发送心跳到调度器,获取指令 3 * 4 * @return 包含指令和任务的元组 5 */ 6 private CommandTaskPair heartbeat() { 7 Integer code = null; 8 String status; 9 10 // 判断当前任务状态 11 if (process == null) { 12 status = "IDLE"; 13 } else { 14 try { 15 code = process.exitValue(); // 尝试获取退出码 16 if (code == 0) { 17 status = "FINISHED"; 18 } else { 19 status = "FAILED"; 20 } 21 } catch (IllegalThreadStateException e) { 22 status = "RUNNING"; // 进程仍在运行 23 } 24 } 25 26 // 构造心跳数据 27 HeartbeatData data = new HeartbeatData(); 28 data.runnerId = RUNNER_ID; 29 data.currentTask = currentTask; 30 data.status = status; 31 data.returnCode = code; 32 data.runnerConfig = RUNNER_CONFIG; 33 data.version = VERSION; 34 data.finalArgs = status.equals("RUNNING") ? List.of("此处为执行任务命令") : null; 35 36 logger.info("Sending heartbeat: " + gson.toJson(data)); 37 38 // 发送心跳请求 39 HttpRequest request = HttpRequest.newBuilder() 40 .uri(URI.create(SCHEDULER_URL)) 41 .header("Content-Type", "application/json") 42 .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(data))) 43 .build(); 44 45 try { 46 HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); 47 if (response.statusCode() != 200) { 48 logger.warn("Failed to send heartbeat: " + response.body()); 49 return new CommandTaskPair("GO_ON", null); 50 } 51 52 JsonObject jsonResponse = gson.fromJson(response.body(), JsonObject.class); 53 JsonObject responseData = jsonResponse.getAsJsonObject("data"); 54 logger.info("Heartbeat response: " + responseData.toString()); 55 56 String command = responseData.get("command").getAsString(); 57 Task task = null; 58 if (responseData.has("task") && !responseData.get("task").isJsonNull()) { 59 task = gson.fromJson(responseData.get("task"), Task.class); 60 } 61 62 return new CommandTaskPair(command, task); 63 } catch (Exception e) { 64 logger.warn("Failed to send heartbeat: " + e.getMessage()); 65 return new CommandTaskPair("GO_ON", null); 66 } 67 }
6.4.3 重置当前执行器
1 /** 2 * 重置当前任务状态 3 */ 4 public void reset() { 5 //若有任务,则中止任务 6 if (process != null) { 7 process.destroy(); // ① 请求进程优雅退出 8 try { 9 if (!process.waitFor(10, java.util.concurrent.TimeUnit.SECONDS)) { // ② 等待最多10秒 10 process.destroyForcibly(); // ③ 强制杀死进程 11 process.waitFor(); // ④ 等待强制终止完成 12 } 13 } catch (InterruptedException e) { 14 Thread.currentThread().interrupt(); // ⑤ 恢复中断状态 15 } 16 } 17 18 logger.info("Reset command received. Current task: " + currentTask); 19 // 清空任务状态 20 currentTask = null; 21 process = null; 22 23 if (stdout != null) { 24 try { 25 stdout.close(); 26 } catch (IOException e) { 27 // 忽略关闭异常 28 } 29 stdout = null; 30 } 31 32 if (stderr != null) { 33 try { 34 stderr.close(); 35 } catch (IOException e) { 36 // 忽略关闭异常 37 } 38 stderr = null; 39 } 40 }
6.4.4 运行新任务
/** * 运行新任务 * * @param task 任务对象 */ public void runNew(Task task) { //运行新任务前,若有旧任务,则先中止旧任务,重置执行器状态 if (process != null) { reset(); } currentTask = task.taskId; try { List<String> command = new ArrayList<>(task.getCommand());//获取任务命令 ProcessBuilder processBuilder = new ProcessBuilder(command); //创建子任务进程,执行任务 // 重定向到文件 String stdoutPath = parsePath("此处为执行器执行任务后的输出结果路径" + "/stdout"); String stderrPath = parsePath("此处为执行器执行任务后的输出错误日志路径" + "/stderr"); // 使用 FileOutputStream 前先创建文件 File stdoutFile = new File(stdoutPath); File stderrFile = new File(stderrPath); // 确保父目录存在 stdoutFile.getParentFile().mkdirs(); stderrFile.getParentFile().mkdirs(); processBuilder.redirectOutput(ProcessBuilder.Redirect.to(stdoutFile)); processBuilder.redirectError(ProcessBuilder.Redirect.to(stderrFile)); process = processBuilder.start(); } catch (IOException e) { logger.warn("Failed to start process: " + e.getMessage()); } }
6.5 Scheduler设计
6.5.1 心跳包设计
Scheduler通过心跳包的信息获取执行器的配置以及运行状态。
1 public class HeartBeat { 2 private String runnerId; 3 private Long currentTask; 4 private RunnerStatus status; 5 private Integer returnCode;//任务执行结果码,0表示成功,非0失败,null运行ing 6 private String stdout;//日志输出的路径 7 private String stderr;//错误日志输出的路径 8 private List<String> runnerConfig; 9 private Integer runnerVersion; 10 }
6.5.2 提供心跳接口
1 /** 2 * Runner心跳 3 * 4 * @param beat 心跳信息 5 * @return 控制指令 6 */ 7 @PostMapping("/heartbeat-url") 8 public RestResponse<SchedulerCommand> heartbeat(@RequestBody HeartBeat beat) { 9 return RestResponse.ok(handleHeartBeat(beat)); 10 }
6.5.3 心跳接口逻辑设计
a. 通过心跳包的runner状态,判断runner是否已分配任务,若分配,将分配的任务的属性lastSeenTime设置now,为前文异常处理4.5 runner失联做铺垫。
b. 对Runner状态进行不同逻辑处理
IDEL:
Runner空闲时,Scheduler发送RUN_NEW命令,并通过任务选择器Picker选出新任务返回给Runner。同时需要处理异常情况。
异常情况:4.1 Runner开启任务异常、4.2 Runner运行任务过程中异常、4.3 手动终止运行中的任务
RUNNING:
Runner运行时,Scheduler判断任务状态是否RUNNING,任务状态是RUNNING,正常,调度器发出GO_ON命令;
任务状态异常(例如:终止运行中的任务,任务状态为TERMINATING),Scheduler发出重置命令;
任务状态是DISPATCHED已派遣,正常,Scheduler发出GO_ON命令,任务设置RUNNING状态。
FINISHED
Runner完成时,Scheduler发出RESET命令,同时设置任务状态为CALLBACK回调。
FAILED
Runner失败时,Scheduler发出RESET命令,同时执行任务失败逻辑。
c. 实现
1 public RunnerHeartBeatRsp handleHeartBeat(HeartBeat beat) { 2 SchedulerCommand command = getCommandByHeart(beat); 3 return switch (command) { 4 case GO_ON -> new RunnerHeartBeatRsp(SchedulerCommand.GO_ON, null); 5 case RESET -> new RunnerHeartBeatRsp(SchedulerCommand.RESET, null); 6 case RUN_NEW -> { 7 //运行新任务,从任务选择器获取新任务 8 Task task = picker.pick(); 9 if (task == null) { 10 //没有任务需要执行 11 yield new RunnerHeartBeatRsp(SchedulerCommand.GO_ON, null); 12 } 13 task.setRunner(beat.getRunnerId()); 14 task.setStatus(StepStatus.DISPATCHED); 15 taskServ.save(task); 16 yield new RunnerHeartBeatRsp(SchedulerCommand.RUN_NEW, task); 17 } 18 }; 19 } 20 21 private SchedulerCommand getCommandByHeart(HeartBeat beat) { 22 Task task = null; 23 if (List.of(RunnerStatus.RUNNING, RunnerStatus.FINISHED, RunnerStatus.FAILED).contains(beat.getStatus())) { 24 task = taskServ.findById(beat.getCurrentTask()); 25 task.setLastSeen(LocalDateTime.now()); 26 taskServ.save(task); 27 } 28 return switch (beat.getStatus()) { 29 case IDLE -> this.handleIdle(beat); 30 case RUNNING -> this.handleRunning(task, beat); 31 case FINISHED -> this.handleFinished(task, beat); 32 case FAILED -> this.handleFailed(task, beat); 33 }; 34 } 35 36 protected SchedulerCommand handleIdle(HeartBeat beat) { 37 // 任务刚被分配到runner, runner下一次心跳没有上报RUNNING状态, 而是IDLE状态 38 // runner可能在启动任务时遇到异常, 并被守护系统重启,任务设置失败 39 taskServ.findAllByRunnerAndStatusAndValidTrue(beat.getRunnerId(), TaskStatus.DISPATCHED) 40 .forEach(step -> taskServ.stepFailed(step, null, "runner is failed to start this task")); 41 42 // 任务已被runner运行,runner下次心跳上报IDLE空闲状态,则runner可能处于异常状态,需要重置runner,任务设置失败 43 taskServ.findAllByRunnerAndStatusAndValidTrue(beat.getRunnerId(), TaskStatus.RUNNING) 44 .forEach(step -> taskServ.stepFailed(step, null, "runner reset")); 45 46 // 终止任务 47 taskServ.findAllByRunnerAndStatusAndValidTrue(beat.getRunnerId(), TaskStatus.TERMINATING) 48 .forEach(taskServ::stepTerminated); 49 50 //调度器发出运行新任务命令 51 return SchedulerCommand.RUN_NEW; 52 } 53 54 protected SchedulerCommand handleRunning(Task task, HeartBeat beat) { 55 return switch (task.getStatus()) { 56 //执行器running,Scheduler判断任务状态是否RUNNING,状态异常(例如:终止运行中的任务,任务状态为TERMINATING),Scheduler发出重置命令 57 case CREATED, CALLBACK, FINISHED, FAILED, SKIPPED, TERMINATING, TERMINATED, QUEUED -> 58 SchedulerCommand.RESET; 59 // 执行器running,任务状态是RUNNING,正常,调度器发出继续运行命令 60 case RUNNING -> SchedulerCommand.GO_ON; 61 // 执行器running,任务状态是DISPATCHED已派遣,正常,调度器发出继续运行命令 62 case DISPATCHED -> { 63 //任务已被派遣,且运行器已开始运行,则任务设置RUNNING状态 64 taskServ.taskBegan(task); 65 yield SchedulerCommand.GO_ON; 66 } 67 }; 68 } 69 70 protected SchedulerCommand handleFinished(Task task, HeartBeat beat) { 71 // 执行器显示已完成状态,调度器发出重置命令,任务设置回调状态 72 if (task.getStatus() == TaskStatus.RUNNING || task.getStatus() == TaskStatus.TERMINATING) 73 taskServ.taskCallback(task, beat.getReturnCode()); 74 75 76 } 77 78 protected SchedulerCommand handleFailed(Task task, HeartBeat beat) { 79 taskServ.taskFailed(task, beat.getReturnCode(), "runner failed"); 80 return SchedulerCommand.RESET; 81 }
6.5.4 Runner失联、Scheduler宕机处理
a. Runner失联:开启定时轮询任务程序,每10秒轮询所有运行中的任务,若任务lastSeenTime超过3个心跳未更新,则判断Runner失联,任务设置失败。
b. Scheduler宕机:重启Scheduler后,三个心跳内不启动定时轮询任务程序,避免任务lastSeenTime未来得及更新就被设置失败。
c. 实现
1 private LocalDateTime initTime = LocalDateTime.now(); 2 @Scheduled(fixedDelay = 10000L) 3 public void cleanUpRunningTimeout() { 4 // Scheduler启动后等待3个心跳时间以避免Scheduler重启后立即清除所有任务 5 if (LocalDateTime.now().isBefore(initTime.plus(30000 * 3L, ChronoUnit.MILLIS))) { 6 return; 7 } 8 var allTask = taskServ.findAllTask(List.of(TaskStatus.RUNNING)); 9 for (Task task : allTask) { 10 if (task.getLastSeen().plus(30000 * 3L, ChronoUnit.MILLIS).isBefore(LocalDateTime.now())) { 11 //三个心跳时间内,任务没有响应,执行任务失败逻辑 12 taskServ.setFailed(task, null, String.format("执行器 %s 没有响应", task.getRunner())); 13 } 14 } 15 }
6.6 任务选择器设计
根据任务优先级字段,任务创建时间,Runner配置,Runner版本,进行选择。此处实现略... ... 对就是懒得写了。
6.7 任务回调器设计
a. 开启定时程序轮询所有回调状态的任务,执行回调逻辑。鉴于不同任务回调逻辑不同,可以采用模板设计模式。
b. 实现
1 public abstract class AbstractTaskService { 2 protected abstract void handleTaskCallback(Task task); 3 @Scheduled( 4 initialDelay = 30000L, 5 fixedDelay = 3000L 6 ) 7 protected void handleTask() { 8 Task task = this.getTaskRandOne(); 9 handleTaskCallback(task); 10 } 11 } 12 13 @Service 14 class TaskService extends AbstractTaskService { 15 16 @Override 17 protected void handleTaskCallback(Task task) { 18 //回调逻辑 19 } 20 }
c. 扩展:可以添加任务失败回调逻辑... ...
7. 结尾
作者很懒,没有什么想说的... ...

详细介绍了任务调度系统
浙公网安备 33010602011771号