配置驱动的通用 ETL 执行引擎:告别"每个需求写一个 Spark 作业"(改配置不发版)

上篇定了调度与血缘的握手协议,本篇是全系列工程含金量最高的一篇------解决"任务怎么定义、怎么跑"的问题)。


一、痛点场景:半年 60 个 Spark 作业,没人敢动

平台跑起来半年后,Spark 作业数量翻了三倍。每接一个数据需求的标准动作是:写一个 Main 类 → 打一个 fat jar → 上传到跳板机 → 建 DolphinScheduler 任务 → 联调 → 发版。最忙的一周发了四次版,有一次只改了一行 SQL 的 where 条件------为了这一行 SQL,走了完整的编码、打包、审批、发布流程。三个问题随之爆发。其一,任务碎片化:60 个作业就是 60 个 fat jar,依赖冲突修一次要全量回归;其二,Yarn 资源申请放大:每个作业独立 spark-submit,每个 Application 冷启动都要申请 Driver + Executor 资源,大量小任务把队列吃满,真实计算时间不到运行时间的三分之一;其三,知识沉淀为零:SQL 散落在 60 个类的字符串常量里,新人想知道"客户表每天怎么加工的",只能逐个翻代码。

二、主流方案为什么不够:三个看似合理的逃逸路线

路线一:DolphinScheduler 的 SQL/Shell 任务节点直接拼 spark-submit。把 SQL 放在调度系统的任务参数里,看似解决了"改 SQL 要发版",但 SQL 散落在调度系统的任务定义里,没有版本管理、没有参数校验、没有统一的水位和血缘上报(第 6 篇的握手协议就接不上了),等于把碎片化从代码库搬到了调度平台。路线二:上 Kyuubi / Livy 这类 Spark 服务化网关。它们解决的是"SQL 提交的服务化",但企业级采集任务的套路------增量水位、断点重跑、全量初始化、血缘上报------每个作业还是各写各的,碎片化的根子没动。路线三:自研一个"任务 DSL"。方向对了,但很多团队死在把 DSL 做成了编程语言:支持 if/else/循环,最后没人会用、没人敢改。结论:要产品化的不是提交方式,而是"任务的共性"。采集类任务 90% 的流程是相同的(取水位 → 拼增量 SQL → 读写湖表 → 上报血缘),不同的只是那 10% 的配置。把这 90% 沉淀成一个引擎,让 10% 的差异全部落在配置表里。

三、我的设计与取舍:父-子两级任务模型 + 二维正交路由

核心设计一:业务 SQL 全存配置表,代码里没有一行业务 SQL。

sql 复制代码
-- ETL v2 主任务配置表(脱敏)
CREATE TABLE t_etl_v2_main (
    main_code     VARCHAR(64)  COMMENT '主任务编码,如 main_customer_daily',
    engine_type   VARCHAR(32)  COMMENT '执行引擎:SPARK / TRINO',
    source_db     VARCHAR(64)  COMMENT '源端数据库',
    schedule_cron VARCHAR(32)  COMMENT '调度周期',
    task_status   TINYINT      COMMENT '0-停用 1-启用'
);

-- 子任务配置表:一个主任务挂 N 个子任务
CREATE TABLE t_etl_v2_sub (
    sub_code     VARCHAR(64)  COMMENT '子任务编码,如 sub_ods_opportunity',
    main_code    VARCHAR(64)  COMMENT '所属主任务',
    source_table VARCHAR(128) COMMENT '源表,如 ods_sc_crm.opportunity',
    target_table VARCHAR(128) COMMENT '目标表,如 iceberg.ods.ods_opportunity',
    incr_field   VARCHAR(64)  COMMENT '增量水位字段,空则为全量',
    custom_sql   TEXT         COMMENT 'SQL 自定义逻辑:完整 SQL 或对源表查询的注入条件',
    processor    VARCHAR(64)  COMMENT 'Processor 扩展点,空则走 General 兜底'
);

核心设计二:父-子两级任务模型,一次调度 = 一个父任务 = N 个子任务 = 一个 Spark Application。调度系统只编排父任务;父任务启动后从配置表拉取子任务清单,在一个 Application 内串行/受控并发跑完所有子任务。60 个作业从 60 次 spark-submit 收敛为 6 个父任务,Yarn 冷启动开销直接降一个数量级,且同一主任务下的子任务天然共享一条血缘上报链路。核心设计三:引擎维度 × 任务维度的二维正交路由。引擎维度解决"用什么跑"(Spark 吃大批量、Trino 做联邦),任务维度解决"怎么跑"(全量/增量/SQL 自定义)。两个维度正交组合,新增引擎或新增任务类型都不动对方。引擎侧复用第 5 篇的 IEngineSession.identifier(),父任务上报的运行坐标对上层完全透明。为什么不选 Flink CDC / DataX 全家桶?Flink CDC 强在实时,我的场景以 T+1 批量为主,为 10% 的实时需求引入一整套流式运维不值;DataX 插件化思想很好,但它是"单表搬运"模型,SQL 自定义逻辑(多表join后入湖)和湖格式特性(Iceberg 分区、回刷)要绕路实现。取舍的本质:配置驱动引擎的复杂度守恒------省下的编码工作量,会转化为配置模型的设计成本。前期把配置模型设计对,后面全是收益。

四、关键设计:Processor 反射加载 + General 兜底

引擎骨架是"通用流程 + 可插拔扩展点":主流程固定(加载配置 → 路由 Processor → 执行 → 上报),差异点抽象成 Processor 接口,按配置反射加载,不配置则走 General 兜底:

java 复制代码
/**
 * 子任务处理器扩展点(脱敏):通用流程中的可插拔差异点
 */
public interface SubTaskProcessor {

    /** 是否可处理该子任务(按 processor 配置或能力声明路由) */
    boolean match(EtlSubTaskConfig config);

    /** 执行:拿到引擎会话与子任务配置,内部自行完成读写 */
    void execute(IEngineSession session, EtlSubTaskConfig config) throws Exception;
}

/**
 * 父任务主流程(脱敏简化):一个 Application 跑完 N 个子任务
 */
public class EtlV2MainRunner {

    public void run(String mainCode) {
        List<EtlSubTaskConfig> subs = configService.loadEnabledSubTasks(mainCode);
        for (EtlSubTaskConfig sub : subs) {
            // 二维路由:任务维度选 Processor(反射加载),引擎维度已由 main 配置决定
            SubTaskProcessor processor = processorRegistry.resolve(sub); // 未配置走 GeneralProcessor
            try {
                processor.execute(engineSession, sub);
                lineageReporter.reportSuccess(sub);   // 第6篇的 Push 血缘
            } catch (Exception e) {
                lineageReporter.reportFail(sub, e);
                if (sub.isFailFast()) throw new RuntimeException(e); // 关键子任务可中断父任务
            }
        }
    }
}

/** General 兜底:覆盖 90% 的标准采集(增量水位拼接逻辑第 8 篇展开) */
public class GeneralProcessor implements SubTaskProcessor {
    public boolean match(EtlSubTaskConfig config) { return true; }
    public void execute(IEngineSession session, EtlSubTaskConfig config) {
        String sql = config.buildExecSql();  // custom_sql + 水位条件 + 权限/方言处理
        session.execute(sql);
    }
}

特殊需求(如某张表要先脱敏再入湖)只需写一个 DesensitizeProcessor 并在子任务配置里填上类名,主流程一行不改------这就是"改任务不改代码、不发版"的落地形态。

五、踩坑与效果

坑一:早期版本把 Processor 做成了 SPI 全量扫描。每个子任务都扫一遍 jar 里的实现类,主任务挂 40 个子任务时启动明显变慢。改为启动时一次性注册到 Map,路由 O(1)。坑二:子任务并发跑出脏数据。曾为提速让同主任务下子任务全并发,结果两个子任务写同一张 Iceberg 目标表的不同分区时触发并发提交冲突。修正:默认串行,仅对"目标表互不相同"的子任务开放受控并发(提交前按 target_table 分组校验)。坑三:配置改错了没有后悔药。有人手滑把增量条件改错,静默跑了三天错数据。补救:配置表变更走统一入口并留版本快照,执行时记录本次生效的配置版本号,出问题可一键定位到"哪次改的配置"。效果:Spark 作业从 60+ 收敛到 6 个父任务,Yarn 资源申请次数降为原来的 1/10;新接一个常规采集需求从"半天编码发版"变成"10 分钟配表 + 单子任务试跑";SQL 全部沉淀在配置表里,OpenMetadata 血缘覆盖率 100% 且免维护;团队新人上手成本从"读懂 60 个 jar"变成"读懂一张配置表"。

六、一句话总结

一句话总结:配置驱动引擎的本质是把任务的 90% 共性沉淀为流程、把 10% 差异性沉淀为配置------父-子任务模型收敛资源与编排,二维正交路由隔离"用什么跑"与"怎么跑",Processor 反射加载 + General 兜底让扩展点永远只在配置里生长;从此,改任务不改代码,不发版。

相关推荐
Neurocoda37 分钟前
基于 NumPy 从零实现 Tiny-AlexNet 在 MNIST 数据集上的验证
人工智能·深度学习
youcans_44 分钟前
【嵌入式软件AI编程】(3)面向 AI 协同开发的 STM32 硬件信息标准化
人工智能·stm32·嵌入式硬件·ai编程
AOI小白新手上路1 小时前
深度学习算力购置方案分析:本地 RTX 4090 服务器 vs 云端租赁
服务器·人工智能·深度学习
QXWZ_IA1 小时前
北斗高精度定位在泛自然资源行业的技术演进与应用方向
大数据·人工智能
坐吃山猪1 小时前
果蝇AI06-Mac部署实操
人工智能·深度学习·机器学习·果蝇ai
微尘hjx1 小时前
【Deep-OC-SORT 02】多轨迹算法Deep-OC-SORT-相对 OC-SORT 的改进点
人工智能·算法·yolo·目标跟踪·视觉检测·ultralytics·deep-oc-sort
梦帮科技1 小时前
意识启发式量子隧穿退火算法用于全局优化 (CITA) 完整技术指南
网络·人工智能·算法·量子计算·模拟退火算法
波儿菜1 小时前
Xcode MCP 助力运行时二进制分析
人工智能·程序员·汇编语言
Vol火山1 小时前
从买工具到建能力:数字化部门如何把 AI 变成可复制的生产力
人工智能