任务调度系统

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. 结尾

  作者很懒,没有什么想说的... ... 

 


 

posted @ 2025-07-24 16:59  一愚亮  阅读(29)  评论(0)    收藏  举报