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 的答案是把调度做进平台本身,并且坚持三个原则:
DataY 的调度系统由两层组成:
- 时间触发层:Quartz 负责 Cron 到点触发;
- 调度编排层:DataY 自研,负责实例、依赖、分发、状态、重试、超时、停止。
Quartz 负责「什么时候跑」,DataY 负责「跑什么、能不能跑、在哪跑、跑完怎么记」。
07 一套代码,三种角色
DataY 通过一个配置项 development.mode 声明当前进程的角色:
| 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 当前的选择不同:
调度层单活,执行层分片。
把「分片」拆开看,会更清楚:
| 调度分片 | 不按 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 分片?
因为分片会引入一组新的问题:
对几十到几百个任务的中小团队来说,单活调度的容量足够,复杂度却低得多。
先把「不重复」做扎实,再谈「分片」,是更稳妥的顺序。
如果规模继续增长,怎么演进?
当任务量真的到了数千级、或者多租户需要强隔离时,可以演进成「调度分片」:
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 算法做拓扑排序:
拓扑排序保证:父任务一定在子任务之前执行。
当前 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 任务的重试有一个明确语义:整体重跑所有子任务。
代码里不查询子任务的历史状态,避免重试过程中依赖数据库状态,导致「跑到一半以为完成了」。
停止任务时,调度器会:
停止不是「不管了」,而是一条完整的状态回收链路。
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
工作方式:
这样,任务运行时和调度进程隔离,任务跑完即销毁。
对小团队来说,这比维护一个 YARN 集群轻得多。
15 和市面上的调度系统比,差在哪、省在哪?
| 定位 | 分布式工作流调度平台 | 代码定义工作流 | 轻量任务调度 | 数据平台内置调度 |
| 部署组件 | 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,调度这件事才真正回到了它本来的样子:
让任务按时、按依赖、可靠地跑完。
网硕互联帮助中心




评论前必须登录!
注册