工业网关任务编排:从 DAG 到 Workflow 的工程实战

工业网关上的任务通常不是孤立执行的:设备数据要先采集,再清洗、聚合、写入本地库或上传平台;异常事件要触发诊断任务;部分指令还要等待设备确认。任务一多,如果没有清晰编排,就容易出现依赖混乱、重复执行、失败后无人处理、上线后无法追溯等问题。

本文从工程实战角度梳理工业网关任务编排的选型、DAG 设计、Airflow / Argo Workflows / Temporal 的适用边界,以及轻量自建方案应该补齐的超时、重试、幂等和观测能力。

一、任务编排要解决什么问题

问题 无编排时的表现 编排系统应提供的能力
依赖管理 脚本之间靠启动顺序或注释约定 显式 DAG,声明上游和下游
触发调度 cron 分散、重复配置 时间触发、事件触发、手动触发
失败处理 任务失败后不知道从哪一步重跑 重试、断点、失败策略、幂等约束
并发控制 多任务同时抢 CPU、内存、设备连接 并发上限、资源隔离、队列
可追溯性 只有零散日志 任务状态、输入输出引用、耗时、错误
版本管理 逻辑改动后无法对齐历史执行 DAG / Workflow 版本、变更记录

编排不是把所有逻辑塞进框架,而是把"什么时候执行、依赖谁、失败怎么办、执行到哪一步"变成可声明、可观测、可恢复的系统能力。

二、主流方案与适用边界

方案 技术定位 适合场景 边缘注意事项
Airflow Python DAG 批处理调度平台 数据抽取、清洗、汇总、日报、周期 ETL 调度器、数据库和 Web 服务有资源开销,不适合单纯事件低延迟场景
Argo Workflows Kubernetes 原生 Workflow 引擎 已运行 K3s / Kubernetes,任务需要容器隔离 需要管理镜像、存储、Pod 资源和集群运维能力
Temporal 持久化工作流引擎 长流程、需要断点恢复、人工确认、复杂重试 需要 Server / History 服务和 Worker,部署复杂度高于轻量网关
Prefect Python 原生 Flow 编排 Python 团队希望动态 DAG、部署较轻 仍需评估状态存储、UI 和执行器的资源占用
自建调度器 进程内或服务内 DAG 执行器 任务数量少、逻辑稳定、资源极有限 必须补齐超时、重试、幂等、状态持久化和观测

选型建议先看运行环境:没有 Kubernetes 时,不建议为了几个周期任务强行部署 Argo;任务需要跨进程、跨容器和长周期恢复时,也不建议只用 asyncio.gather() 包装函数。工业边缘常见的务实组合是:实时事件走消息队列或规则引擎,周期批处理走 DAG 调度,长流程走 Temporal 这类持久化工作流。

三、DAG 设计的工程规则

1. 明确节点边界

一个 DAG 节点应该代表一个可重试、可观测的业务单元,而不是任意函数拆分。

推荐按以下边界拆分:

  • 采集 / 拉取:从设备、PLC、Modbus 采集或从消息队列消费;
  • 清洗 / 转换:单位换算、异常值处理、去重、聚合;
  • 存储:写入本地时序库、对象存储或关系库;
  • 上传:同步到云端平台或第三方系统;
  • 通知 / 诊断:告警、生成诊断报告、触发补偿任务。

2. 节点必须幂等

调度系统通常提供"至少一次"执行语义。网络超时、进程重启、Worker 失联都可能导致任务重复执行,因此节点要按业务键处理重复数据。

常用做法:

  • 数据库使用唯一键约束,重复写入执行 UPSERT
  • 文件写入使用版本号、批次 ID 或内容哈希;
  • 上传前先查询远端状态,或使用带去重 ID 的接口;
  • 设备指令设置请求 ID,避免确认包重复触发;
  • 聚合任务按时间窗口和站点 ID 重建结果,而不是盲目累加。

3. 传引用,不传大数据

Airflow XCom、Temporal Event History、Argo Artifact 都不适合无限放大。工业数据量高时,任务之间应传递批次号、文件路径、对象存储 Key 或数据库查询条件,而不是完整数据集。

4. 显式声明并发和资源

边缘设备 CPU、内存、磁盘 IO 和设备连接数都有限。DAG 设计时至少明确:

  • 同一任务的最大并发运行数;
  • 同一设备的互斥锁;
  • 任务超时时间;
  • 重试次数和退避间隔;
  • 本地磁盘写入上限;
  • 失败后是否允许跳过、阻塞或触发降级。

四、Airflow:周期批处理 DAG

下面是一个设备数据 ETL 示例。Airflow 更适合分钟级以上的批处理;如果业务要求毫秒级响应,应使用消息队列或规则引擎,而不是把 Airflow 当实时系统。

python 复制代码
from __future__ import annotations

from datetime import datetime, timedelta, timezone
from typing import Any

from airflow.decorators import dag, task


@dag(
    dag_id="device_etl",
    schedule="*/5 * * * *",
    start_date=datetime(2024, 1, 1, tzinfo=timezone.utc),
    catchup=False,
    max_active_runs=1,
    default_args={
        "retries": 2,
        "retry_delay": timedelta(minutes=1),
    },
    tags=["edge", "device", "etl"],
)
def device_etl() -> None:
    @task
    def extract_devices() -> str:
        """Return a batch reference instead of all records."""
        batch_id = "batch_20260917_001"
        return batch_id

    @task
    def transform(batch_id: str) -> str:
        """Read by batch reference and write transformed data to storage."""
        transformed_ref = f"{batch_id}:transformed"
        return transformed_ref

    @task
    def load(transformed_ref: str) -> dict[str, Any]:
        """Persist records idempotently by business key."""
        return {
            "status": "ok",
            "ref": transformed_ref,
            "rows": 0,
        }

    batch_id = extract_devices()
    transformed_ref = transform(batch_id)
    load(transformed_ref)


dag = device_etl()

关键配置说明:

  • schedule 是 Airflow 2.4+ 推荐写法,旧项目中的 schedule_interval 应逐步迁移;
  • catchup=False 避免服务长时间停机后补跑大量历史任务;
  • max_active_runs=1 限制同一 DAG 的并发实例,防止上一批未完成时重复执行;
  • retries 只解决瞬时故障,业务失败仍应通过告警和人工任务处理;
  • XCom 只传批次引用,大数据走对象存储或数据库。

五、Argo Workflows:容器化步骤编排

如果工业网关已经运行 K3s / Kubernetes,Argo 适合把每个步骤放进独立容器,实现镜像级隔离和资源限制。

yaml 复制代码
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  generateName: device-etl-
spec:
  entrypoint: pipeline
  activeDeadlineSeconds: 1800
  ttlStrategy:
    secondsAfterCompletion: 86400
  templates:
    - name: pipeline
      retryStrategy:
        limit: "2"
        retryPolicy: "OnError"
      dag:
        tasks:
          - name: extract
            template: extract
          - name: transform
            template: transform
            dependencies: [extract]
          - name: load
            template: load
            dependencies: [transform]
    - name: extract
      container:
        image: registry.local/edge-etl:1.0.0
        command: [python, extract.py]
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi
    - name: transform
      container:
        image: registry.local/edge-etl:1.0.0
        command: [python, transform.py]
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: "1"
            memory: 512Mi
    - name: load
      container:
        image: registry.local/edge-etl:1.0.0
        command: [python, load.py]
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi

工程要点:

  • 镜像使用固定版本,避免 latest 导致重跑行为不一致;
  • 每一步都设置 resources,防止任务抢占网关主业务;
  • activeDeadlineSeconds 限制整体执行时长;
  • ttlStrategy 自动清理历史 Workflow,避免集群元数据膨胀;
  • 中间结果放在 PVC、S3 / MinIO 或数据库,用批次 ID 关联;
  • 需要访问设备网络的任务要配置 NetworkPolicy 和最小权限。

六、Temporal:长流程与可恢复执行

Temporal 的价值在于持久化执行状态:流程在进程重启、网络中断、活动失败后可以从事件历史恢复。它适合设备升级审批、批量指令下发、跨系统对账、人工确认等长流程。

python 复制代码
from __future__ import annotations

from datetime import timedelta
from typing import Any

from temporalio import activity, workflow
from temporalio.common import RetryPolicy


@activity.defn
async def fetch_device(device_id: str) -> dict[str, Any]:
    return await device_api.fetch(device_id)


@activity.defn
async def update_database(data: dict[str, Any]) -> None:
    await database.upsert_by_device_id(data["device_id"], data)


@workflow.defn
class DeviceWorkflow:
    @workflow.run
    async def run(self, device_id: str) -> dict[str, Any]:
        data = await workflow.execute_activity(
            fetch_device,
            device_id,
            start_to_close_timeout=timedelta(seconds=30),
            retry_policy=RetryPolicy(maximum_attempts=3),
        )

        await workflow.execute_activity(
            update_database,
            data,
            start_to_close_timeout=timedelta(seconds=10),
            retry_policy=RetryPolicy(maximum_attempts=3),
        )

        return {"device_id": device_id, "status": "updated"}

工程边界:

  • Workflow 代码必须确定性,不能直接调用数据库、HTTP、随机数或当前时间;
  • IO、网络、设备访问都放到 Activity;
  • Activity 要幂等,因为 Worker 失联后可能重复执行;
  • start_to_close_timeout 是单次活动执行上限,复杂任务还应评估 schedule_to_close_timeout
  • Workflow 和 Activity 变更需要遵循版本兼容规则,不能随意修改已运行流程的逻辑;
  • 生产部署要规划 Temporal Server、数据库持久化、Worker 资源和历史数据保留策略。

七、轻量自建:只做受控的小型 DAG

当任务数量少、运行环境简单、只需要进程内编排时,可以自建一个很小的 DAG 执行器。但要清楚:下面这类实现没有持久化历史、没有分布式调度、没有 Web UI,只适合作为网关内部任务的受控执行骨架。

python 复制代码
from __future__ import annotations

import asyncio
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import Any


TaskFunc = Callable[[], Awaitable[Any]]


@dataclass
class TaskSpec:
    name: str
    func: TaskFunc
    dependencies: set[str] = field(default_factory=set)
    timeout: float = 30.0
    retries: int = 0


class SimpleDag:
    def __init__(self, max_parallelism: int = 2) -> None:
        self.tasks: dict[str, TaskSpec] = {}
        self.max_parallelism = max_parallelism

    def add(self, task: TaskSpec) -> None:
        if task.name in self.tasks:
            raise ValueError(f"duplicate task: {task.name}")
        self.tasks[task.name] = task

    def validate(self) -> None:
        names = set(self.tasks)
        for task in self.tasks.values():
            unknown = task.dependencies - names
            if unknown:
                raise ValueError(f"{task.name} has unknown dependencies: {unknown}")

        visiting: set[str] = set()
        visited: set[str] = set()

        def visit(name: str) -> None:
            if name in visited:
                return
            if name in visiting:
                raise ValueError(f"cycle detected at {name}")
            visiting.add(name)
            for dependency in self.tasks[name].dependencies:
                visit(dependency)
            visiting.remove(name)
            visited.add(name)

        for name in names:
            visit(name)

    async def run(self) -> dict[str, Any]:
        self.validate()
        results: dict[str, Any] = {}
        completed: set[str] = set()
        running: set[str] = set()

        while len(completed) < len(self.tasks):
            runnable = [
                task
                for task in self.tasks.values()
                if task.name not in completed
                and task.name not in running
                and task.dependencies <= completed
            ][: self.max_parallelism]

            if not runnable and running:
                await asyncio.sleep(0.05)
                continue
            if not runnable:
                raise RuntimeError("workflow has no runnable tasks")

            running.update(task.name for task in runnable)
            outcomes = await asyncio.gather(
                *(self._run_with_retry(task) for task in runnable),
                return_exceptions=True,
            )

            failures = [
                (task.name, error)
                for task, error in zip(runnable, outcomes, strict=True)
                if isinstance(error, BaseException)
            ]
            if failures:
                raise RuntimeError(f"workflow failed: {failures}")

            for task, result in zip(runnable, outcomes, strict=True):
                results[task.name] = result
                completed.add(task.name)
                running.remove(task.name)

        return results

    async def _run_with_retry(self, task: TaskSpec) -> Any:
        last_error: BaseException | None = None
        for attempt in range(task.retries + 1):
            try:
                return await asyncio.wait_for(task.func(), timeout=task.timeout)
            except Exception as error:
                last_error = error
                if attempt < task.retries:
                    await asyncio.sleep(min(2**attempt, 5))
        raise RuntimeError(f"task {task.name} failed") from last_error

这个实现仍然缺少:

  • 状态持久化和进程重启后的恢复;
  • 分布式锁和跨实例任务分配;
  • Web UI、执行历史和审计;
  • 更细的失败策略,例如跳过、降级、人工介入;
  • 子进程或容器隔离;
  • 追踪、指标和结构化事件。

如果这些能力是刚需,应优先选择 Airflow、Argo 或 Temporal,而不是继续扩展自研调度平台。

八、边缘部署与运维实践

1. 隔离批任务与实时链路

周期 ETL、模型推理、报表生成等任务容易产生 CPU 和磁盘峰值,应与设备协议接入、实时告警、指令下发等关键链路隔离。可用方案包括独立进程、cgroup / systemd 资源限制、容器资源限制,或将重任务上传到边缘服务器执行。

2. 控制本地状态增长

任务日志、中间文件、Artifact 和历史记录都要设置保留策略。建议按"热数据本地可查、冷数据压缩归档、过期数据自动删除"分层处理,并监控磁盘使用率。

3. 配置重跑和补偿

编排不是只定义成功路径,还要定义失败路径:

  • 哪些任务可以自动重试;
  • 哪些任务失败后必须人工确认;
  • 哪些数据可以重新生成;
  • 哪些设备指令不能重复下发;
  • 重跑时从哪个批次或时间窗口开始;
  • 是否需要先清理半成品数据。

4. 建立任务观测

至少采集:

  • 调度触发时间和任务开始时间;
  • 每个节点的耗时、成功数、失败数、重试数;
  • 当前运行中的任务和排队任务;
  • 输入记录数、输出记录数、丢弃记录数;
  • 本地磁盘、内存、CPU 和网络连接;
  • 关键外部依赖可用性。

告警要区分"任务失败"和"任务连续失败"。偶发失败可以由重试恢复,连续失败、积压、磁盘接近上限和队列阻塞才应立即通知值班人员。

5. 做好版本与回滚

DAG、Workflow、容器镜像和任务脚本要统一版本。发布前至少保留:

  • 上一版镜像或代码包;
  • 数据库结构回滚方案;
  • 调度配置备份;
  • 任务的输入输出样例;
  • 灰度范围和停止条件。

九、常见坑与处理

现象 处理
循环依赖 任务永远 pending,或自建调度器死锁 启动前做拓扑校验和环检测
没有超时 外部接口或设备响应挂起,任务长期占用 为每个节点设置执行超时
没有幂等 重试后重复写入、重复下发指令 用业务键、请求 ID、UPSERT 和窗口重建
大对象进执行历史 XCom / Event History / Artifact 膨胀 传引用,数据放对象存储或数据库
并发失控 CPU、内存、连接数互相挤压 限制 DAG 并发、任务并发和设备锁
无状态持久化 进程重启后进度丢失 保存批次状态,或使用持久化工作流引擎
无观测 只知道"失败",不知道卡在哪一步 记录节点状态、耗时、错误和上下游
版本随意变更 重跑历史任务行为不一致 固定镜像和代码版本,遵循兼容策略

十、测试与验收清单

  • DAG 拓扑无环,依赖名称完整;
  • 每个节点都有超时、重试和失败策略;
  • 任务按业务键重复执行不会产生脏数据;
  • 服务重启后可以恢复或安全重跑;
  • 并发上限符合边缘资源预算;
  • 输入输出只传引用,大数据有落盘方案;
  • 执行历史、错误日志和指标可查询;
  • 设备指令有请求 ID 和确认去重;
  • 回滚版本和发布检查单可用;
  • 压测覆盖高峰数据量、慢网络和磁盘不足。

TL;DR

工业网关任务编排的核心不是选择最复杂的框架,而是让依赖、触发、并发、失败处理和执行状态变得清晰可控。周期批处理可选 Airflow 或 Prefect;Kubernetes 环境中的容器化步骤适合 Argo Workflows;长流程和强恢复需求适合 Temporal;资源极受限时可以自建小型 DAG,但必须补齐幂等、超时、观测和持久化边界。

Zenova EdgeOS 这类工业边缘运行环境中,任务编排可用于组织设备数据 ETL、周期诊断、边缘规则联动和跨系统同步,并把执行状态、失败重试与观测能力纳入统一的网关运行链路。

相关推荐
szxinmai主板定制专家9 小时前
【嵌入式实战】RK3588+GMSL多路相机采集方案|SerDes长距离低延时视频流落地教程
人工智能·单片机·机器人·边缘计算·zynq
星野云联AIoT技术洞察1 天前
RK3588 边缘 AI 盒子适合什么工业视觉场景
计算机视觉·边缘计算·rk3588·语音识别·图像识别·边缘网关·边缘计算盒子
znnnk2 天前
【AI应用】Agent:AI 为什么需要“自主决策”?
ai·prompt·agent·workflow·ai应用·skill·mcp
ishangy2 天前
海思 2T 算力|泵吸式四合一气体 AI 布控球技术方案解析
边缘计算·gb28181·ai视觉·气体检测·布控球·有限空间
杉氧2 天前
拒绝重复造轮子:我写了一个生产级的 Kotlin 协程与 Flow 工具库(CoroutineKit)
android·kotlin·workflow
Rocktech_ruixun2 天前
机器人集群调度方案硬件如何选型?瑞迅科技RK3588/3576/3568分级方案解析
人工智能·嵌入式硬件·机器人·边缘计算
Joy T3 天前
Spring AI 2.0 进阶入门:Workflow、Routing、Task State 与可控 Agent
开发语言·人工智能·workflow·routing·springai·orchestrator·evaluator
荆棘鸟智能4 天前
AI算法持续学习与迭代怎么做?从数据闭环到灰度发布的MLOps工程实践
人工智能·算法·架构·边缘计算
土星云SaturnCloud4 天前
ResNet-50 图像分类算法在边缘微服务器上的部署与性能评测
服务器·人工智能·算法·边缘计算·resnet-50