3. 架构原理:Dataset Trigger 在调度系统中的位置

讲 Dataset Trigger 之前,咱们得先搞清楚它在整个调度系统里到底站在哪。我见过不少团队,上来就写触发逻辑,结果跟现有调度框架打架,最后不得不重构。说白了,位置没摆对,后面全是坑。

3.1 Dataset Trigger 在调度系统中的层级

一个典型的大数据调度系统,从上到下大概分三层:

  • 应用层:用户看到的 DAG 定义、任务配置、告警规则
  • 调度引擎层:负责任务排队、资源分配、依赖解析、触发执行
  • 数据层:元数据服务、事件总线、状态存储

Dataset Trigger 就横跨在调度引擎层和数据层之间。它不是调度引擎本身,而是调度引擎的“眼睛”和“耳朵”。

核心观点:Dataset Trigger 是一个数据驱动的事件监听器,它监听数据层的变化,然后通知调度引擎去触发相应的任务。

我习惯把它比作一个“哨兵”。哨兵不负责打仗,但负责发现敌人(数据就绪),然后吹号(触发任务)。

3.2 事件驱动架构基础

为什么 Dataset Trigger 要用事件驱动?你想想看,传统调度是轮询——每隔几分钟扫一遍数据库,看看有没有新数据。数据量小还行,数据量大了,轮询就是灾难。

我曾经维护过一个每天跑 10 万+ 任务的集群,轮询间隔设到 30 秒,数据库 CPU 直接飙到 80%。后来改成事件驱动,CPU 降到 15%。

事件驱动架构的核心就四个字:有变才动。

对比项 轮询模式 事件驱动模式
触发方式 定时扫描 数据变化时主动推送
资源消耗 高(空转浪费) 低(按需触发)
实时性 取决于轮询间隔 毫秒级响应
扩展性 数据库压力随任务量线性增长 事件总线可水平扩展

事件驱动架构里,有三个角色你必须记住:

  • 事件生产者:谁产生数据变化?比如 Hive 表写入完成、HDFS 文件落盘、Kafka 分区更新。
  • 事件总线:消息的通道,负责把事件从生产者传给消费者。常见实现有 Kafka、Pulsar、RabbitMQ。
  • 事件消费者:谁处理事件?Dataset Trigger 就是消费者之一,它收到事件后判断是否满足触发条件。

个人经验:事件总线选型时,别只看吞吐量。我踩过一个坑——选了吞吐最高的消息队列,但它的消息顺序保证不够,导致 Dataset Trigger 收到乱序的事件,触发逻辑全乱套。后来换了支持分区有序的 Kafka,才稳住。

3.3 元数据服务与事件总线

这两个东西是 Dataset Trigger 的左右手。元数据服务告诉 Trigger“数据长什么样”,事件总线告诉 Trigger“数据什么时候变了”。

3.3.1 元数据服务

元数据服务存的是数据的“身份信息”。比如:

  • 这个 Dataset 叫什么名字?
  • 它存在哪个路径下?
  • 它的分区结构是怎样的?
  • 它依赖哪些上游 Dataset?
  • 它当前的状态是什么(就绪/未就绪/过期)?

我建议把元数据服务设计成无状态的,方便水平扩展。存储层可以用 MySQL 或者 TiDB,但查询层一定要加缓存,不然高并发下元数据查询会成为瓶颈。

避坑指南:我曾经见过一个团队把元数据直接存在调度系统的业务库里,结果调度系统一升级,元数据也跟着迁移,数据丢了两次。元数据服务应该独立部署,跟调度引擎解耦。

3.3.2 事件总线

事件总线是 Dataset Trigger 的“神经”。它传递的事件消息,一般包含这些字段:

{
  "event_id": "evt_20250321143001",
  "event_type": "DATASET_AVAILABLE",
  "dataset_name": "ods.order_daily",
  "partition": "dt=2025-03-21",
  "timestamp": 1742553001000,
  "producer": "hive_etl_worker_01"
}

事件类型不止一种。我归纳过常见的几种:

事件类型 含义 触发动作
DATASET_AVAILABLE 数据集已就绪 触发下游任务
DATASET_EXPIRED 数据集过期 触发重跑或告警
DATASET_UPDATED 数据集被覆盖 触发下游增量更新
DATASET_DELETED 数据集被删除 暂停下游依赖

事件总线的设计要点,我总结三条:

  1. 至少一次投递:宁可重复,不能丢失。重复的事件可以在消费端做幂等处理。
  2. 分区有序:同一个 Dataset 的事件必须按顺序消费,否则状态会乱。
  3. 消息 TTL:设置合理的过期时间。我习惯设 7 天,太短了历史回溯困难,太长了浪费存储。

3.4 整体架构图

下面这张图,是我画的一个简化版架构。它展示了 Dataset Trigger 在系统中的位置,以及元数据服务和事件总线怎么配合。

Dataset Trigger 架构位置图 应用层:DAG 定义 / 任务配置 / 告警规则 调度引擎层:任务排队 / 资源分配 / 依赖解析 Dataset Trigger(事件哨兵) 元数据服务 事件总线(Kafka / Pulsar) Hive / HDFS / Kafka 等数据源 下游任务 / 告警 / 重跑 说明:Dataset Trigger 位于调度引擎层与数据层之间,监听事件总线,查询元数据服务,驱动调度决策

3.5 数据流与事件流

搞清楚了位置,咱们再看看数据是怎么流的。我习惯把流程拆成两条线:

数据流(实线):数据从 Hive/HDFS 写入,元数据服务记录状态变化,事件总线广播事件,Dataset Trigger 消费事件并判断,最后通知调度引擎触发任务。

控制流(虚线):调度引擎向 Dataset Trigger 注册监听规则,Trigger 向元数据服务查询 Dataset 的依赖关系,元数据服务返回 Dataset 的上下游拓扑。

这两条线是并行的。数据流负责“快”,控制流负责“准”。

一个小技巧:我习惯在 Dataset Trigger 内部维护一个本地状态缓存。每次从事件总线拿到事件后,先查本地缓存,命中就直接判断,没命中再去元数据服务查。这样能大幅降低元数据服务的压力。缓存失效时间我设 30 秒,够用。

3.6 高可用设计

最后聊两句高可用。Dataset Trigger 本身不能挂,挂了整个数据驱动调度就瘫痪了。

我建议至少做三件事:

  • 多副本部署:至少两个实例,用消费者组的方式消费事件总线。一个挂了,另一个自动接管。
  • 状态持久化:Trigger 的消费进度(offset)要定期写入外部存储,重启后能续上。
  • 降级策略:如果事件总线挂了,回退到轮询模式。虽然慢,但至少不丢任务。

我曾经踩过的坑:有一次事件总线集群磁盘写满,消息积压了 3 个小时。Trigger 的 offset 没及时提交,重启后重复消费了 10 万条消息,下游任务全部重复跑了一遍。从那以后,我强制要求所有事件消费逻辑做幂等——同一个事件处理两次,结果必须一样。

嗯,架构原理这部分就这些。记住三个关键词:位置(调度引擎和数据层之间)、机制(事件驱动)、依赖(元数据服务 + 事件总线)。后面咱们会深入讲 Dataset Trigger 的具体实现,到时候这些原理都会用上。