云计算百科
云计算领域专业知识百科平台

DolphinScheduler、Airflow、XXL-JOB 都很强,为什么我们还要自研一个调度引擎?

DolphinScheduler、Airflow、XXL-JOB 都很强,为什么我们还要自研一个调度引擎?

——DataY 分布式调度的设计与实现

凌晨两点,订单同步任务没有按时跑。

值班同学先看调度器,再看注册中心,再看数据库,最后发现是某个 ETL 任务把调度进程的内存拖垮了。

任务和调度挤在一个 JVM 里,一个任务失控,整个调度停摆。

这不是某个产品的 bug,而是很多调度系统都会遇到的架构问题:

调度和执行,到底该不该放在一起?


01 为了几十个任务,先维护十个进程?

先看一个很多团队都似曾相识的场面。

业务部门提了 30 个数据任务:每天凌晨同步订单、每小时更新库存、每 10 分钟跑一次指标、失败要重试、上游成功下游才能跑。

于是开始选型:

  • 要可视化 DAG,上 DolphinScheduler;
  • 要 Python 生态,上 Airflow;
  • 要轻量定时,上 XXL-JOB;
  • 要容器隔离,再配 Kubernetes CronJob;
  • 要协调,再加 ZooKeeper;
  • 要状态,再加数据库和 Redis。

最后,任务还没跑稳,平台先有了十几个进程:

API Server
Master × 3
Worker × 3
ZooKeeper × 3
Database × 2
Redis
Alert Server
监控 / 日志 / 网关……

这不是夸张,而是很多「标准分布式调度」的默认形态。

问题是:这些组件不是为 30 个任务准备的,而是为 3000 个任务、几十个团队、跨集群容灾准备的。

对中小团队来说,真正需要的往往不是「最强调度」,而是:

第一天能单机跑,第二年需要扩容时,不用换一套系统。


02 DolphinScheduler:能力完整,但默认很重

Apache DolphinScheduler 是国内使用最广的开源分布式调度之一。

它的定位很清晰:

可视化、分布式、多租户的工作流任务调度平台。

核心能力包括:

  • 可视化 DAG 编排,拖拽节点、连线、配置依赖;
  • 丰富的任务类型:Shell、SQL、Spark、Flink、HTTP、DataX、子流程等;
  • Cron 定时、依赖调度、失败重试、补数、告警;
  • Master / Worker 分离,支持水平扩展;
  • 多租户、资源中心、Worker 分组、环境管理;
  • 注册中心 + 数据库,支持高可用部署。

如果你的组织有多个数据团队、上千个任务、需要资源组和强隔离,DolphinScheduler 是非常成熟的选择。

但它的「完整」,也意味着「重」:

  • API、Master、Worker、Alert 多个服务;
  • 通常还要注册中心(如 ZooKeeper)和关系型数据库;
  • 多 Master、多 Worker 的部署、升级、排障,需要专门的运维经验;
  • 任务类型多,配置项也多,小团队容易「用不到但必须维护」;
  • 为了高可用,组件往往要三副本起步。

一句话:

DolphinScheduler 是一套完整的调度平台,不是一个小团队的轻量定时器。


03 Airflow:DAG 表达力很强,运行时也很重

Apache Airflow 是另一个绕不开的名字。

它的核心优势是「用代码定义工作流」:

with DAG(
"order_pipeline",
schedule="0 2 * * *",
start_date=datetime(2026, 1, 1),
catchup=False,
) as dag:
extract = PythonOperator(task_id="extract", python_callable=extract_orders)
transform = PythonOperator(task_id="transform", python_callable=transform_orders)
load = PythonOperator(task_id="load", python_callable=load_orders)

extract >> transform >> load

Airflow 的特点:

  • Python 原生,DAG 就是代码,表达能力强;
  • Operator / Provider 生态丰富,几乎什么系统都能接;
  • Web UI 成熟,日志、重试、补数、依赖管理都有;
  • 支持 Celery、Kubernetes 等 Executor;
  • 社区大,资料多。

但它的运行时同样不轻:

  • Web Server、Scheduler、Executor、Metadata Database;
  • 分布式执行还要 Celery Broker(Redis/RabbitMQ)或 Kubernetes;
  • Scheduler 要不断解析 DAG 文件,DAG 多了以后本身就有压力;
  • Python 依赖管理是长期痛点,不同任务的环境容易冲突;
  • 对「数据任务」的语义支持,需要靠 Operator 和插件拼;
  • 高可用 Scheduler、数据库、日志存储,都是额外的运维工作。

一句话:

Airflow 是一个优秀的工作流编排框架,但它不是为「轻量数据平台」设计的。


04 XXL-JOB、Quartz、K8s CronJob:轻,但不够

除了上面两个「平台级」调度,还有很多更轻的选择。

XXL-JOB

XXL-JOB 是国内非常流行的轻量分布式任务调度平台:

  • 调度中心(Admin) + 执行器(Executor);
  • Cron 定时、失败重试、告警;
  • 路由策略丰富:轮询、随机、一致性 Hash、故障转移、忙碌转移;
  • 执行器注册、任务分片、GLUE 模式;
  • 部署简单,Java 生态友好。

它很适合「Java 应用里的定时任务」,但它的核心抽象是「任务」,不是「数据工作流」:

  • DAG 能力弱,复杂依赖要靠子任务或业务代码拼;
  • 没有数据任务特有的 Schema、CDC 位点、增量状态;
  • 调度和执行虽然分离,但任务编排能力有限;
  • 不适合表达「多任务、多依赖、可回填」的数据管道。

Quartz

Quartz 是 Java 世界里最经典的定时任务库:

  • Cron 表达式;
  • RAMJobStore / JDBCJobStore;
  • 集群模式通过数据库锁避免重复触发;
  • 生态成熟,Spring Boot 直接集成。

但 Quartz 只是一个「时间触发内核」:

  • 没有 Web UI;
  • 没有 DAG;
  • 没有任务依赖;
  • 没有实例状态机;
  • 没有执行隔离;
  • 没有任务级别的重试、超时、停止。

它是很好的「零件」,但不是「调度平台」。

Kubernetes CronJob

Kubernetes CronJob 的优势是容器隔离:

  • 到点起一个 Pod,跑完销毁;
  • 资源限制清晰;
  • 和 K8s 生态天然集成。

但它的调度语义很薄:

  • 没有 DAG;
  • 没有跨任务依赖;
  • 没有实例状态机;
  • 没有统一的日志、重试、告警、补数;
  • 出问题只能看 Pod 状态和日志。

它适合「简单定时任务」,不适合「数据管道调度」。


05 这些调度系统,共同的问题是什么?

把市面上的调度系统放在一起看:

产品核心抽象强项主要成本
DolphinScheduler 工作流 + 任务 DAG、多租户、任务类型丰富 组件多,部署运维重
Airflow Python DAG 表达力强、生态丰富 运行时重,依赖管理复杂
XXL-JOB 任务 轻量、路由策略丰富 DAG 与数据语义弱
Quartz 定时触发器 成熟、嵌入简单 没有平台能力
K8s CronJob 容器任务 隔离好、云原生 调度语义薄

它们的能力都很强,但复杂度来源也很一致。

问题一:分布式是默认值,不是可选项

很多调度系统从第一天就假设你会部署多 Master、多 Worker、注册中心、数据库。

但小团队第一天可能只有一台机器。

为了「以后可能扩容」,先承担了「现在就复杂」。

问题二:调度和执行容易耦合

任务如果直接跑在调度进程里:

  • 一个 ETL 任务内存泄漏,调度服务跟着挂;
  • 任务依赖的 JDBC 驱动、Python 包、第三方 jar,会污染调度进程;
  • 任务日志、临时文件、线程池,和调度共享资源。

但完全拆开,又需要消息队列、注册中心、服务发现,复杂度立刻上升。

问题三:状态和日志跨节点后很难查

调度器知道「任务失败了」,但不知道:

  • 是哪个节点跑的?
  • 失败在哪一步?
  • 日志在哪台机器上?
  • 重试后状态有没有覆盖?
  • 上一次成功实例是什么时候?

没有统一的实例状态和日志回收,分布式调度就会变成「分布式排障」。

问题四:数据任务的语义,通用调度器不一定懂

数据任务不只是「到点执行一个脚本」,它还需要:

  • 上游成功后才跑;
  • 上游最近一次实例的时间窗口;
  • 失败重试、超时中断;
  • 增量位点、断点续传;
  • DAG 内部父子依赖;
  • 任务实例可追踪、可停止。

这些语义,通用调度器要么没有,要么要靠插件和外部系统拼。


06 DataY 的答案:不是再做一个调度平台

我们做 DataY 调度时,没有想再写一个 DolphinScheduler。

而是先问了一个问题:

如果大多数团队只需要「按时、按依赖、可靠地跑任务」,为什么一定要先有一套分布式集群?

DataY 的答案是把调度做进平台本身,并且坚持三个原则:

  • 单机是默认值:standalone 模式不需要 Redis,调度和执行同进程;
  • 集群是可选项:需要扩容时,改一个配置就能拆成 master / worker;
  • 同一套代码:单机和集群不是两个版本,不是两套分支。
  • DataY 的调度系统由两层组成:

    • 时间触发层:Quartz 负责 Cron 到点触发;
    • 调度编排层:DataY 自研,负责实例、依赖、分发、状态、重试、超时、停止。

    Quartz 负责「什么时候跑」,DataY 负责「跑什么、能不能跑、在哪跑、跑完怎么记」。


    07 一套代码,三种角色

    DataY 通过一个配置项 development.mode 声明当前进程的角色:

    模式调度能力执行能力依赖 Redis适用场景
    standalone 有 有 否 单机、开发、边缘节点
    master 有 无 是 只做调度,可多副本
    worker 无 有 是 只做执行,水平扩展
    master,worker 有 有 是 小集群,调度与执行同机

    几个关键点:

    • standalone 是默认值,单机不需要 Redis;
    • 角色用逗号分隔,也支持分号和空格,大小写不敏感;
    • 角色是运行时行为,不是两套代码;
    • 不需要维护 application-master.yml、application-worker.yml 这种角色 profile;
    • 只有纯 standalone 走进程内队列,一旦带上 master / worker,就切换到 Redis 事件通道。

    启动方式也很直接:

    # 单机(默认,无需 Redis)
    java -jar datay-web.jar

    # 只做调度
    java -jar datay-web.jar –development.mode=master

    # 只做执行
    java -jar datay-web.jar –development.mode=worker

    # 调度 + 执行同机
    java -jar datay-web.jar –development.mode=master,worker

    对小团队来说,这意味着:

    第一天用一个 JAR 跑起来,等任务真的多了,再改配置扩容。

    而不是第一天就按「大厂架构」部署。


    08 时间触发、Leader 选举与任务分片

    8.1 Quartz 只做时间触发

    DataY 没有自己造一个 Cron 引擎,而是复用了 Quartz:

    • Cron 表达式精确到秒;
    • 任务上线时注册触发器,下线时移除;
    • 修改 Cron 时重新调度;
    • 立即执行时触发一次。

    但 Quartz 只负责「到点触发」,不负责分布式协调。

    真正的调度语义在 Quartz 之上:

    Quartz 到点
    │
    ▼
    创建任务实例(APPENDING)
    │
    ▼
    检查依赖是否满足
    │
    ├── 满足 ──▶ 投递执行事件 ──▶ Worker 执行
    │
    └── 不满足 ──▶ 实例置为 WAITING

    8.2 多 Master 怎么避免重复触发?

    先说结论:

    DataY 的多 Master 是「主备式多 Master」,不是「每个 Master 各跑一部分」的多活调度。

    同一时刻,只有一个 Leader 真正负责调度,其他 Master 待命。避免重复触发,靠的是前三层约束;第四层用于事后判定。

    如果希望待命节点不浪费,可以用 master,worker 部署:非 Leader 不参与调度,但仍然作为 Worker 消费任务队列。纯 master 节点才是热备。

    第一层:抢同一把锁。

    DataY 用 Redis 分布式锁做 Leader 选举:

    • 锁名默认 datafusion-leader;
    • 未当选的节点循环尝试 tryLock(最多等待 1 秒);
    • 当选后回调 onElected(),加载已上线任务、注册 Cron 触发器;
    • 只有 Leader 从数据库加载已上线任务并重建触发器;
    • Leader 挂掉后,其他节点接管,重新初始化。

    第二层:只有 Leader 注册 Cron 触发器。

    这里有一个很重要的设计:

    Quartz 的 JobStore 是进程内的,不是共享数据库。

    所以不是每个 Master 都注册一遍 Cron,而是只有 Leader 注册。这样不会出现多个 Master 同时持有同一个 Cron 触发器,也不需要把 Quartz 的触发器状态同步到共享存储。

    第三层:只有 Leader 消费调度事件。

    任务上线、下线、状态回报,最终都由 Leader 侧的事件处理器消费:

    • addJobQueue:Leader 注册触发器;
    • deleteJobQueue:Leader 删除触发器;
    • endJobQueue:Leader 接收 Worker 上报的实例状态并落库。

    未当选的 Master 不启动这些处理器,自然不会重复执行调度动作。

    第四层:实例落库,状态机兜底。

    每次触发都会生成一个任务实例,实例有明确的状态和唯一编码。即使发生极端情况,最终也可以根据实例状态判断「这次到底跑没跑」,而不是只看触发器有没有响。

    Leader 切换时,新 Leader 从数据库重新加载任务定义,重新建立触发器。

    任务定义是持久的,触发器是临时的。

    这里必须说清楚一个边界:

    主备选举能解决「正常情况下的重复调度」,但不是数学意义上的 Exactly-Once。

    如果 Redis 不可用、网络发生分区、或者旧 Leader 在锁失效后仍然继续运行,理论上仍可能出现重复触发。DataY 的兜底是实例状态与业务幂等:任务本身要能接受「至少一次」的执行语义。

    这不是 DataY 独有的问题,而是所有基于租约的分布式调度都要面对的边界。


    8.3 多 Master 的任务分片:调度不分片,执行分片

    很多人听到「多 Master」,会默认认为任务会被拆到多个 Master 上并行调度。

    DataY 当前的选择不同:

    调度层单活,执行层分片。

    把「分片」拆开看,会更清楚:

    分片层次DataY 当前做法说明
    调度分片 不按 Master 分片 只有 Leader 调度,其他 Master 待命
    执行分片 Redis 队列 + 多 Worker 竞争消费 一个任务实例只会被一个 Worker 取走
    DAG 子任务分片 当前不跨 Worker 整个 DAG 在一个 Worker 上按拓扑序执行
    任务内部并发 组件级 parallelism ETL 内部按组件并发,不是跨节点分片
    执行分片:任务怎么落到不同 Worker?

    调度器生成实例后,把执行事件投递到 startJobQueue。

    多个 Worker 同时消费这个队列:

    ┌─────────────────┐
    │ startJobQueue │
    └────────┬────────┘
    │
    ┌─────────────────┼─────────────────┐
    ▼ ▼ ▼
    ┌─────────┐ ┌─────────┐ ┌─────────┐
    │ Worker1 │ │ Worker2 │ │ Worker3 │
    └────┬────┘ └────┬────┘ └────┬────┘
    │ │ │
    ▼ ▼ ▼
    任务 A 任务 B 任务 C

    队列天然具备「一条消息只被一个消费者取走」的语义,所以:

    • 任务 A 不会被 Worker1 和 Worker2 同时执行;
    • Worker 多了,整体吞吐自然上升;
    • 不需要在调度器里维护一张「任务 → Worker」的路由表;
    • Worker 正常失败会按重试策略处理;如果 Worker 进程直接挂掉,实例可能停留在 RUNNING,需要人工重跑或后续补偿机制。

    这就是 DataY 的「执行分片」:不是按 Hash 把任务预分配到某个 Worker,而是让 Worker 竞争消费。

    它适合数据任务的实际情况:任务大小不一、执行时间不一、节点规格也可能不同。竞争消费比静态分片更灵活,也不会出现「某个分片空闲、另一个分片排队」的问题。

    DAG 子任务:当前不跨 Worker 分片

    需要特别说明:DataY 当前的 DAG 任务,是由一个 Worker 取走整个 DAG,然后按拓扑序串行推进子任务。

    也就是说:

    • DAG 不会把子任务拆到多个 Worker 上并行执行;
    • 一个 DAG 内的父子依赖,仍由这个 Worker 顺序保证;
    • 代码里留了「按层并行」的优化点,但当前版本没有做跨节点 DAG 分片。

    这是一个明确的边界,不是「已经支持但没写」。

    为什么调度层不按 Master 分片?

    因为分片会引入一组新的问题:

  • 触发器一致性:每个分片都要保证自己的 Cron 不重复;
  • 故障转移:一个分片挂了,它负责的任务要迁移到其他分片;
  • 依赖跨分片:A 任务在分片 1,B 任务在分片 2,依赖判断要跨节点;
  • 热点与再平衡:任务大小不一,按 Hash 分片很容易出现热点;
  • 运维复杂度:分片数、路由规则、迁移策略,都要额外维护。
  • 对几十到几百个任务的中小团队来说,单活调度的容量足够,复杂度却低得多。

    先把「不重复」做扎实,再谈「分片」,是更稳妥的顺序。

    如果规模继续增长,怎么演进?

    当任务量真的到了数千级、或者多租户需要强隔离时,可以演进成「调度分片」:

    jobId / tenantId
    │
    ▼
    hash % N
    │
    ├──▶ Scheduler Shard 0(Leader 锁 0)──▶ startJobQueue-0
    ├──▶ Scheduler Shard 1(Leader 锁 1)──▶ startJobQueue-1
    └──▶ Scheduler Shard 2(Leader 锁 2)──▶ startJobQueue-2

    大致思路是:

    • 按 jobId 或 tenantId 做一致性 Hash,把任务分配到 N 个调度分片;
    • 每个分片有自己的 Leader 锁,例如 datafusion-leader-{shard};
    • 每个分片只加载自己负责的任务,只注册自己的触发器;
    • Worker 可以消费共享队列,也可以按分片订阅队列;
    • 扩缩容时做分片迁移,并用版本号 / fencing token 防止旧 Leader 写入。

    需要强调:这是 DataY 在更大规模下的演进方向,不是当前版本已经实现的能力。

    当前版本的多 Master,解决的是「高可用」;任务分片,解决的是「执行吞吐」。两者不是一回事。

    多 Master 负责「调度不中断」,多 Worker 负责「任务跑得快」。


    09 单机与集群,只差一个事件服务

    调度器和执行器之间,需要传递四类事件:

    队列方向作用
    startJobQueue Master → Worker 待执行任务
    endJobQueue Worker → Master 任务状态变更
    addJobQueue 调度侧 → Leader 任务上线,注册触发器
    deleteJobQueue 调度侧 → Leader 任务下线,删除触发器

    DataY 把事件服务抽象成 IEventService,有两个实现:

    实现使用场景底层
    StandaloneEventService 单机 进程内 LinkedBlockingQueue,容量 10000
    RedisEventService 集群 Redisson RBoundedBlockingQueue,容量 10000

    同一套调度逻辑,单机走内存队列,集群走 Redis 队列。

    上层的实例状态机、依赖检查、重试逻辑完全不用改。

    这就是「一套代码,三种角色」的底气:

    不是为单机和集群写两套调度,而是把「事件通道」抽出来,换一个实现。

    另外,集群模式还有一条硬约束:ETL 增量状态必须放共享存储(MinIO)。

    • 单机用 local,状态写本地文件;
    • 集群用 minio,Worker 执行时写 MinIO,Master 通过共享配置读取同一份状态;
    • 如果集群仍用 local,状态只落在各 Worker 本地磁盘,Master 读不到,也无法跨节点恢复增量。

    调度状态在数据库,增量状态在共享存储,两者不能混为一谈。


    10 实例状态机:调度的账本

    调度系统最重要的是「可追踪」。

    DataY 为每个任务实例维护一套明确的状态:

    ┌─────────────┐
    │ APPENDING │ 已创建,等待投递
    └──────┬──────┘
    │
    ┌────────────┴────────────┐
    │ 依赖满足 │ 依赖未满足
    ▼ ▼
    ┌──────────┐ ┌──────────┐
    │ RUNNING │◀─────────────│ WAITING │
    └────┬─────┘ 依赖满足 └──────────┘
    │
    ┌───────┼──────────┬──────────────┐
    ▼ ▼ ▼ ▼
    SUCCESSFUL FAILED TIMEOUT INTERRUPTED

    状态含义
    APPENDING 实例已创建,等待调度投递
    RUNNING Worker 正在执行
    WAITING 依赖未满足,等待恢复
    SUCCESSFUL 执行成功
    FAILED 执行失败
    TIMEOUT 执行超时
    INTERRUPTED 手动停止或被取消

    实例上还会记录:

    • 执行节点(IP:端口);
    • 开始时间、结束时间;
    • 执行消息(失败时包含错误日志摘要);
    • 重试次数、超时时间、优先级;
    • 父实例编码(DAG 子任务)。

    这些信息看起来不起眼,但它们是排障的起点。

    没有它们,调度系统就只是一个「定时器」。


    11 DAG 与依赖:拓扑排序 + 等待恢复

    11.1 DAG 工作流

    一个 DAG 任务由多个子任务和它们之间的依赖组成。

    DataY 用 Guava 的 MutableGraph 建图,再用 Kahn 算法做拓扑排序:

  • 把所有子任务实例加入图;
  • 根据 JobDepend 添加有向边;
  • 计算每个节点的入度;
  • 入度为 0 的节点先执行;
  • 每执行完一个节点,后继节点入度减一,减到 0 就进入队列。
  • 拓扑排序保证:父任务一定在子任务之前执行。

    当前 DAG 子任务按拓扑序串行推进,代码里留了「按层并行」的优化点。对中小团队的任务规模来说,串行已经够用,也更容易保证状态一致。

    DAG 失败时,当前子任务之后的节点会被标记为 INTERRUPTED,不会留下「一半成功、一半悬空」的实例。

    11.2 跨任务依赖

    除了 DAG 内部的父子关系,DataY 还支持任务之间的依赖(JobDepend):

    • 子任务触发时,先查它依赖的所有父任务;
    • 取每个父任务的最近一次实例;
    • 如果父实例不存在,或者父实例的时间与当前实例的间隔超过配置的 lastInterval,判定依赖未满足;
    • 依赖满足才执行,否则实例进入 WAITING。

    等待中的实例由 WaitingJobQuartzTask 每分钟扫描一次:

    WAITING 实例 ──▶ 检查依赖 ──▶ 满足 ──▶ 投递执行
    │
    └── 不满足 ──▶ 继续等待

    同时,如果同一个任务产生了新的等待实例,旧的等待实例会被置为 TIMEOUT,避免无限堆积。

    这套机制不复杂,但覆盖了数据任务最常见的依赖语义:

    上游今天跑成功了,我才跑;上游没跑,我等。


    12 Worker:并发、优先级与背压

    Worker 收到执行事件后,不是直接 new Thread,而是提交到一个有界线程池:

    参数默认值说明
    工作线程数 100 可调整,2C4G 环境建议按实际负载下调
    待执行队列 1000 队列满后不再从事件队列取任务
    优先级 1/2/3 低 / 中 / 高,同优先级按提交顺序
    拒绝策略 CallerRunsPolicy 队列和线程都满时,由提交线程执行

    几个工程细节:

    • 优先级队列:PriorityBlockingQueue + PrioritizedFutureTask,高优先级任务先执行;
    • 背压:执行队列满了,Worker 暂停从 Redis / 内存事件队列取任务,避免任务在进程内无限堆积;
    • 执行节点记录:任务开始前写入 IP:端口,出问题知道是哪台机器跑的;
    • 任务停止:先标记停止,再调用任务的 cancel(),最后 future.cancel(true) 中断线程;
    • 租户上下文:Worker 线程没有请求上下文,执行前按任务所属租户设置 TenantContext,避免被租户过滤器拦截。

    这些细节决定了一个调度系统能不能在真实环境里稳定运行。


    13 重试、超时与停止:别让任务「挂死」

    DataY 把重试和超时抽成了一个统一的执行支撑层 TaskExecutionSupport:

    • 重试:retryCount 表示首次执行之外的最大重试次数,最多 10 次;
    • 超时:timeoutSeconds 表示单次尝试的超时时间,最多 7 天;
    • 重试间隔:默认 10 秒;
    • 只重试失败和超时:成功不重试,手动停止(INTERRUPTED)也不重试;
    • 每次尝试创建全新的任务执行器:避免上一次执行的残留状态影响下一次;
    • 超时看门狗:独立线程计时,超时后取消任务并中断执行线程。

    DAG 任务的重试有一个明确语义:整体重跑所有子任务。

    代码里不查询子任务的历史状态,避免重试过程中依赖数据库状态,导致「跑到一半以为完成了」。

    停止任务时,调度器会:

  • 标记 stopped;
  • 调用任务自身的 cancel()(例如 ETL 任务会停止所有组件、关闭队列);
  • 取消 Future 并中断线程;
  • 把实例状态写成 INTERRUPTED。
  • 停止不是「不管了」,而是一条完整的状态回收链路。


    14 调度与执行隔离:需要时再上 Docker

    任务跑在 Worker 进程里,最大的风险是「任务把 Worker 拖死」:

    • ETL 任务依赖的 JDBC 驱动版本冲突;
    • 某个任务内存泄漏;
    • 某个任务长时间占用线程。

    DataY 提供了一个可选的 Docker 执行模式:

    development:
    etl:
    mode: docker
    docker:
    image: eclipse–temurin:17–jre
    jar-path: /opt/datay/datay–core–1.0.1–SNAPSHOT–jar–with–dependencies.jar
    memory: 2g

    工作方式:

  • Worker 把任务 JSON 写入宿主机日志目录;
  • 执行 docker run 启动容器,挂载日志、驱动、数据目录;
  • 容器内运行 DataY Core 执行任务;
  • 以容器退出码判定成功/失败(0 成功,非 0 失败);
  • 停止任务时 docker rm -f 清理容器。
  • 这样,任务运行时和调度进程隔离,任务跑完即销毁。

    对小团队来说,这比维护一个 YARN 集群轻得多。


    15 和市面上的调度系统比,差在哪、省在哪?

    维度DolphinSchedulerAirflowXXL-JOBDataY 调度
    定位 分布式工作流调度平台 代码定义工作流 轻量任务调度 数据平台内置调度
    部署组件 API/Master/Worker/Alert + ZK + DB Web/Scheduler/Executor/DB Admin + Executor + DB 一个 JAR;集群模式加 Redis
    最小资源 多节点起步 多组件起步 较轻 单机 2 核 4G
    DAG 强 强 弱 强
    跨任务依赖 强 强 中 强
    单机模式 需要完整部署 需要完整部署 需要 Admin/Executor 默认 standalone,无需 Redis
    执行隔离 Worker / 资源组 Executor 执行器 进程内 / Docker 可选
    多租户 完整 中 弱 基础租户隔离
    适用规模 大型、多团队 中大型 中小型 中小团队、几十到几百任务

    DataY 的取舍很明确:

    • 不做 DolphinScheduler 那种大型调度平台,但保留了 DAG、依赖、重试、超时、实例状态;
    • 不做 Airflow 那种通用工作流框架,但把数据任务的语义做进调度;
    • 不做 XXL-JOB 那种纯任务调度,但支持 DAG 和数据管道;
    • 不要求你先有一套集群,但需要扩容时可以拆成 master/worker。

    一句话:

    DataY 不是要替代谁,而是把「中小团队最常用的那部分调度能力」收进一个 JAR。


    16 它的边界:哪些场景不适合?

    把边界说清楚,比把话说满更重要。

    DataY 调度不适合:

    • 上万任务、多团队强隔离:需要资源组、队列、配额、审计,应该用更重的调度平台;
    • 跨机房容灾:DataY 支持多 Master 选主,但不提供跨地域的容灾编排;
    • 复杂的事件驱动编排:例如「任意一个上游成功就触发」「动态分支」「循环子流程」,DataY 目前只覆盖常见的 Cron + DAG + 父子依赖;
    • 需要严格 Exactly-Once 调度语义:调度触发本身可以做到不重复,但任务执行的幂等仍需业务自己保证;
    • Worker 进程级故障的自动接管:任务正常失败会重试,但 Worker 直接宕机后实例可能停留在 RUNNING,需要人工重跑或后续补偿;
    • 超大规模并发执行:Worker 线程池和执行队列都有上限,需要按机器规格和任务类型调优。

    DataY 调度真正擅长的是:

    • 每天 / 每小时 / 每分钟的数据同步与加工任务;
    • 多任务 DAG 编排、父子依赖;
    • 失败重试、超时中断、手动停止;
    • 单机起步,后续扩成 master/worker;
    • 和 DataY 的数据集成、DuckDB、DuckLake 打通。

    17 DataY 是什么

    DataY 是一套开源、轻量的一体化数据平台:

    一个 JAR、内嵌 DuckDB,2 核 4G 就能跑通「数据集成 → 数据建模 → 数据开发 → 任务调度 → 数据应用」的完整链路。

    如果用一句话概括它的定位:

    DataY = DataX + Nifi + DolphinScheduler

    但这个「等于」不是把三个开源项目打包在一起,而是用同一套运行时,重新实现数据同步、数据流编排和任务调度这三类能力。

    它由两块组成:

    模块定位说明
    DataY Core 数据集成引擎 轻量、可嵌入、可扩展、流批一体;JSON 定义任务、自动建表、CDC 实时同步;可脱离 Web 独立运行或嵌入 Java 应用
    DataY Web 可视化数据平台 拖拽式任务设计、任务调度、维度建模、指标管理、智能问数

    和调度相关的能力:

    • ⏰ Cron 调度:Quartz 内核,精确到秒,支持动态上线/下线;
    • 🔗 DAG 编排:父子任务拓扑排序,失败中断,状态可追踪;
    • 🧭 依赖调度:父任务成功后触发子任务,未满足进入 WAITING,定时恢复;
    • 🧩 角色分离:standalone / master / worker,同一套代码;
    • 🛡️ 多 Master 选主:Redis 分布式锁,Leader 唯一调度;执行任务由 Worker 队列竞争分片;
    • 🔁 重试与超时:失败重试、超时中断、手动停止;
    • 🐳 Docker 执行:任务与调度进程隔离,跑完即销毁;
    • 📊 实例监控:状态、执行节点、起止时间、执行消息全程记录;
    • 📦 一体化:调度、集成、建模、问数共用一套元数据与权限。

    快速开始:

    git clone https://github.com/yuyuejia/datay
    cd datay
    mvn clean package -DskipTests
    java -jar datay-web/target/*.jar –DATAY_AI_API_KEY=sk-xxxxxxxxx
    # 浏览器打开 http://localhost:8080,默认账号 admin/admin

    产品首页:http://datay.yuyuejia.com.cn/ 在线体验:http://datay-demo.yuyuejia.com.cn/ 项目地址:https://github.com/yuyuejia/datay


    18 写在最后

    调度系统的价值,不在于它用了多少分布式组件,而在于:

    • 任务到点能不能可靠触发;
    • 依赖不满足时能不能正确等待;
    • 失败后能不能重试、超时后能不能停下来;
    • 出问题时能不能一眼看到是哪个实例、哪个节点、什么原因。

    DataY 的调度设计,本质上是在做一次取舍:

    把「必须分布式」变成「可以分布式」; 把「默认复杂」变成「默认简单」; 把「多套组件」变成「一套代码、三种角色」。

    当单机不需要 Redis,集群只需要加一个 Redis,调度这件事才真正回到了它本来的样子:

    让任务按时、按依赖、可靠地跑完。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » DolphinScheduler、Airflow、XXL-JOB 都很强,为什么我们还要自研一个调度引擎?
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!