跳转至

DDIA Ch.10 批处理 — 复习专题

梳理自《Designing Data-Intensive Applications》第 2 版 Ch.10 批处理
定位:历史脉络 → 挑战 → 解法,先框架后下钻;配合 6h 冲刺第 3 段 使用
配对:Part 2 分布式 · Ch.11 流处理

← 返回 DDIA 框架 | 专题索引 | ↑ 总脑图 批路径 · 计费场景

组件深潜Airflow 调度 · dbt 数据建模 · ClickHouse OLAP


6h 第 3 段复习清单(60min)

对应 ddia-framework § 第 3 段(2:15–3:15)。
节奏:50 分钟阅读 + 10 分钟休息;先读 全景串讲,再下钻。

时段 做什么
0–15 min § 全景串讲 + 30 秒版
15–25 min § 架构图双视角 + § MVP 伪代码
25–40 min § 核心概念下钻 + DDIA 书 Ch.10 挑读(见 DDIA 对照
40–50 min § 与本专题映射 + 扫 Airflow / dbt 目录各 5 min
50–60 min 写下产出 + 勾选 checkbox

读法

  • 这章和 Airflow、dbt 最贴,值得精读(相对 Ch.5–9 小结)。
  • 术语表、历史轶事、MapReduce 实现细节 可跳;抓住 有界、可重跑、不可变输出、派生数据

必须搞懂的点

概念 问自己 下文
派生数据 批处理算的是「第二份数据」还是改源库? §0 起点
有界输入 / 再运行 日批分区、Airflow 重跑意味着什么? § 有界输入
MapReduce 思想 用 dbt SQL 时还有「分而治之」吗? § MapReduce 思想
输出不可变、追加 分区 overwrite / insert 和容错啥关系? § 不可变输出
血缘、分层 ODS→DWD→DWS 解决什么历史问题? § 血缘与分层

写下来(必做)

批处理 4 步:有界输入 → ____ → ____ → 可重跑/对账

批处理链路(填真实名字):____ → ____ → ____ 
  (例:Airflow DAG → dbt model → ClickHouse ADS 表)

批处理全景:历史脉络 → 挑战 → 解法(串讲)

用法:先读这一节建立时间线,再 下钻。疑问记入 QA

一句话主线:业务要 从原始数据定期算出报表/宽表 → 数据太大单机放不下 → 用 分布计算 + 可重跑 换可靠;后来用 SQL 数仓 + DAG 编排 换可维护。


0. 起点:派生数据

源数据(业务库、日志、快照)  ──计算──▶  派生数据(汇总表、指标、宽表)
  • 批处理 不替代 源库;是 读一份、写另一份(常是数仓分区表)。
  • Ch.11 流处理 相同目标、不同 延迟与运行方式

第一个挑战:报表/指标要等 T+1,但 nightly SQL 脚本在数据量变大后 跑不完、跑挂难恢复


1. 单机时代:SQL + cron / 脚本

年代/场景 早期数据仓库、部门级报表
挑战 数据进 TB 级;单机内存/磁盘不够;脚本失败要 从头再来
解法 更大机器、拆成多个 SQL 文件 cron 串联
得到 简单、业务方会 SQL
新代价 扩展天花板低;依赖乱; 失败恢复靠人肉

需要 自动把作业拆到多机、失败可局部重试 → MapReduce 时代。


2. MapReduce(~2004):分布 + 不可变 + 重跑

年代/场景 Google 内部 → Hadoop 生态;日志分析、ETL
挑战 ① 数据远大于单机;② 机器会挂;③ 要避免共享可变状态
解法 Map(按 key 分区计算)→ ShuffleReduce(聚合);输入 只读;输出 新目录;失败 重跑该 task
为何当年选它 commodity 机器集群上换 可靠吞吐;程序员不用手写分布式容错
得到 水平扩展;容错模型清晰
新代价 开发繁琐(Java MR); 迭代/交互查询极慢;中间结果落盘多

思想遗产(今天仍有效):分而治之、数据不动算动、输出写新分区、作业可重跑


3. Spark(~2010s):内存 DAG + 迭代

挑战 MR 每步落 HDFS,迭代算法 / 多步 SQL 太慢
解法 内存(或溢写)中保留 DAG 中间结果;同一数据集多步变换
为何选它 数据科学、交互式查询、ETL 链 少写磁盘
新代价 内存贵;仍要集群运维;业务 SQL 仍要叠层

4. 现代数仓路径:SQL + dbt + Airflow(本专题主战场)

挑战 业务方要写 SQL 不是 Java;要有 依赖调度、回填、告警;指标口径要 可测试、可文档
解法 ClickHouse / Snowflake 等 存算;dbt 管 SQL 变换与测试;Airflow 管 DAG、重跑、回填
为何选它 派生逻辑 声明式;分层 ODS→DWD→DWS 可维护;日批 有界分区 天然契合
仍继承 MR/Spark 的 分区并行、不可变分区输出、失败重跑

5. 时间线(复习用)

timeline
    title 批处理演化(简化)
    section 派生
        派生数据 : 源数据算出第二份
    section 扩展
        SQL加cron : 单机瓶颈
        MapReduce : 分布加不可变加重跑
        Spark : 内存DAG迭代
    section 数仓工程
        dbt加Airflow : SQL变换加编排

6. 选型:用挑战反推

你的挑战 更倾向
日/小时 有界 分区、可回填 批处理 + 调度器
秒~分钟级 指标 流处理lambda 双写
只跑一次实验 SQL 单机查询即可,不必上集群 MR
指标要强血缘、测试 dbt 类工具

30 秒版

有界输入(某日分区 / 快照)
  → 读入(并行扫分区)
  → 转换(SQL / dbt / Map-Reduce 思想)
  → 幂等写入目标分区(append / overwrite)
  → 失败可重跑 + 对账验证

架构图(双视角)

应用视角:谁在用、数据怎么流

站在业务与数据工程 一侧:关心 源系统、调度、建模、数仓、报表,不展开集群内部。

flowchart TB
  subgraph sources [数据源]
    OltpDB[(业务库/快照)]
    ObjStore[对象存储/日志文件]
    Connector[connector定时拉取]
  end
  subgraph orchestration [编排层]
    Airflow[Airflow DAG]
  end
  subgraph transform [变换层]
    Dbt[dbt SQL ODS到DWD到DWS]
  end
  subgraph warehouse [数仓]
    CH[(ClickHouse分区表)]
  end
  subgraph consumers [消费方]
    BI[报表/BI]
    API[指标API]
    Reconcile[对账/质量检查]
  end
  OltpDB --> Connector
  ObjStore --> Connector
  Connector --> Airflow
  Airflow --> Dbt
  Dbt --> CH
  CH --> BI
  CH --> API
  CH --> Reconcile
角色 本专题文档
connector 有界 数据拉进 ODS 架构总纲
Airflow 何时跑、依赖、重跑、告警 airflow-data-pipeline
dbt 怎么算、血缘、测试 dbt-learning-path
ClickHouse 存结果、OLAP 查 clickhouse-deep-dive

系统内部视角:一次日批作业怎么跑

站在引擎/框架 一侧:一次批作业 = 读有界输入 → 多 stage 变换 → 写不可变输出 → 失败可重试(MR / Spark / SQL 引擎共性)。

flowchart TB
  subgraph trigger [触发]
    Cron[调度时间到]
    DagEngine[解析DAG依赖图]
  end
  subgraph bounded [有界输入]
    Split[按分区切分InputSplit]
    Read[并行读dt分区]
  end
  subgraph compute [计算阶段]
    Map[Map投影过滤]
    Shuffle[Shuffle按Key重分布]
    Reduce[Reduce聚合]
  end
  subgraph output [输出]
    Write[写新分区或Overwrite]
    Commit[提交可见]
  end
  subgraph fault [容错]
    Retry[失败Task重试]
    Idem[同分区幂等覆盖]
  end
  Cron --> DagEngine --> Split --> Read --> Map --> Shuffle --> Reduce --> Write --> Commit
  Map -.失败.-> Retry --> Map
  Write --> Idem
内部步骤 解决的历史问题
InputSplit 单机读不下 → 切开并行
Shuffle 要按 key 全局聚合 → 跨节点归并
写新分区 跑到一半挂了 → 不留半成品,重跑整分区
幂等覆盖 重试多次 → 结果仍一份

即使用 dbt,ClickHouse 仍会在底层做 分区并行 scan + 分布式聚合(思想同 MapReduce)。


MVP 示例代码(Java):批处理管道

教学用伪代码:有界分区 + DAG 依赖 + Map/Reduce 思想 + 幂等写分区
对应上图 系统内部视角;不是 Airflow/dbt 源码。

示例 1:SimpleBatchJob(单作业:读分区 → 聚合 → 写)

// SimpleBatchJob.java (pseudocode)
// 模拟:处理 dt=2026-05-29 有界分区,按 campaign_id 聚合 click 数

class SimpleBatchJob {
    private final String runId;
    private final String dt;

    SimpleBatchJob(String runId, String dt) {
        this.runId = runId;
        this.dt = dt;
    }

    void run() throws IOException {
        // 1) 有界输入:只读该日分区(完结数据集)
        List<Event> input = PartitionReader.readBounded("ods_events", dt);
        // 2) Map:投影 + 过滤(每行独立)
        List<Kv> mapped = new ArrayList<>();
        for (Event e : input) {
            if (e.valid) mapped.add(new Kv(e.campaignId, 1L));
        }
        // 3) Shuffle + Reduce:按 key 求和(内存版;生产在集群上做)
        Map<String, Long> reduced = new HashMap<>();
        for (Kv kv : mapped) {
            reduced.merge(kv.key, kv.value, Long::sum);
        }
        // 4) 幂等写入:写到「新路径」= dt 分区目录(重跑覆盖同路径)
        OutputWriter.writeIdempotent(
            "ads/dws_campaign_daily/dt=" + dt,
            reduced,
            runId
        );
        // 5) 对账钩子(示意)
        Reconcile.checkRowCount("ods_events", dt, reduced.size());
    }

    static class Event {
        String campaignId;
        boolean valid;
    }
    static class Kv {
        String key;
        Long value;
        Kv(String k, Long v) { key = k; value = v; }
    }
}

示例 2:SimpleBatchDag(多任务依赖 = Airflow 思想)

// SimpleBatchDag.java (pseudocode)
// 模拟:extract -> transform_dwd -> transform_dws -> validate

class SimpleBatchDag {
    private final String dt;
    private final Map<String, Runnable> tasks = new LinkedHashMap<>();

    SimpleBatchDag(String dt) {
        this.dt = dt;
        tasks.put("extract_ods", () -> extractOds(dt));
        tasks.put("build_dwd", () -> buildDwd(dt));
        tasks.put("build_dws", () -> buildDws(dt));
        tasks.put("validate", () -> validate(dt));
    }

    void run() {
        // 拓扑序执行(MVP:手写顺序 = extract -> dwd -> dws -> validate)
        runTask("extract_ods");
        runTask("build_dwd");
        runTask("build_dws");
        runTask("validate");
    }

    private void runTask(String name) {
        int maxRetry = 3;
        for (int i = 0; i < maxRetry; i++) {
            try {
                tasks.get(name).run();
                return;
            } catch (Exception e) {
                if (i == maxRetry - 1) throw e;
                // 失败可重跑:同 dt 分区幂等
            }
        }
    }

    private void extractOds(String dt) { /* 拉源 -> ods/dt */ }
    private void buildDwd(String dt) { /* ods -> dwd,依赖 extract 完成 */ }
    private void buildDws(String dt) { /* dwd -> dws 聚合 */ }
    private void validate(String dt) { /* 行数/金额对账 */ }
}

MVP 与两张架构图的对应

架构图(应用视角) 代码 / 概念
connector → ODS extractOds(dt)
Airflow SimpleBatchDag.run + maxRetry
dbt 分层 build_dwdbuild_dws
ClickHouse / 对账 writeIdempotent + Reconcile
架构图(系统内部) 代码里
InputSplit / Read readBounded("ods_events", dt)
Map mapped 循环
Shuffle + Reduce reduced.merge
Write + Commit writeIdempotent(.../dt=)
Retry / 幂等 maxRetry + 同路径覆盖

Ch.11 流处理 MVP 对比

批 MVP 流 MVP
输入 有界 dt 分区 无界 log
触发输出 全批 commit 一次 Watermark 关窗
容错 重跑覆盖分区 Checkpoint

故意没做:分布式 shuffle、Spark SQL 优化器、Airflow 元数据库、dbt ref() 解析。


核心概念下钻

有界输入与可重跑

概念 含义 工作对应
有界(bounded) 输入在某一时刻 完结(如 dt=2026-05-29 分区) 日批、月批
再运行(re-run) 同样逻辑 再打一次 同一输入 Airflow clear + 重跑;dbt --select 重刷
幂等 重跑 N 次结果与 1 次相同 分区 INSERT OVERWRITE;merge key 去重

历史为何重要:MapReduce 时代机器常挂 → 必须 假设作业会失败,设计 可安全重试 的 stage。


MapReduce 思想仍适用

即使用 dbt + ClickHouse,底层仍是:

大表按分区/分片切开 → 各 worker 执行相同 SQL 片段 → 汇总写回
MR 术语 现代对应
Input split 表分区、shard
Map SELECT ... GROUP BY 每分区
Shuffle 分布式 join / redistribute
Reduce 全局聚合、写 ADS
输出新目录 新分区、新 part,不改源分区

不可变输出与分区

做法 容错意义
输出写到 新路径/新分区 跑一半失败 不留脏数据;重跑整分区
不原地 UPDATE 整表 存储引擎 B+Tree 原地改 对比:批处理偏爱 不可变分区

ClickHouse:INSERT 新 part、或按分区 REPLACE;失败则 drop 该分区重跑(视引擎策略)。


血缘与数仓分层

典型作用 解决的历史问题
ODS 贴源、少改 源混乱,先 统一落地
DWD 清洗、维度一致 「同名字段不同口径」
DWS / ADS 汇总、指标 报表直接扫大表太慢

dbt = 把 SQL 依赖显式化(ref())+ 测试 → 可维护的派生链(见 dbt-learning-path)。


幂等与回填(backfill)

场景 注意
补历史 30 天 分区并行;控制 并发与成本
逻辑变更后重刷 版本化模型;避免 双份指标 上线
上游迟到 批处理常 T+1 等数据齐;迟到用 流或二次修正批

详见 Airflow 回填(catchup / backfill)。


与本专题映射

connector / 源表
       ↓
  Airflow DAG(定时、依赖、重试、告警)
       ↓
  dbt-runner(ODS → DWD → DWS → ADS)
       ↓
  ClickHouse 分区表(OLAP 查询、报表)
       ↓
  对账 / 质量检查(可靠性的批处理延伸)
组件 在批处理链中的角色
Airflow 编排:何时跑、失败怎么办、回填
dbt 变换 + 血缘 + 测试:SQL 即 pipeline
ClickHouse 存储 + 查询:列存、分区、merge

计费日批:把 § 全景 的「有界 + 幂等 + 可重跑」对照你们真实 DAG 名填一遍。


批 vs 流(简表)

完整表见 Ch.11 § 批 vs 流

维度 批处理(本章) 流处理
延迟 分钟~天(T+1 常见) 秒~分钟
输入 有界(分区完结) 无界(持续事件)
错了怎么办 重跑分区 Checkpoint + 状态重置 / 补偿

DDIA Ch.10 书内读法对照

中译本以 章 + 小节标题 为准;不必逐字读完。

建议读 可跳或扫读
批处理输入(有界) 特定公司轶事
MapReduce 与分布式批处理 思想 完整 MR API 细节
输出与容错、幂等
批工作流 vs 服务
现代引擎(Spark 等)对比表 每个系统安装教程

读完本节,你应该能口述

  1. 批处理在算什么(派生数据),和 OLTP 改余额有啥不同。
  2. 历史链:单机不够 → MR → Spark → SQL 数仓 + 编排
  3. 有界、不可变分区输出、可重跑 各解决什么故障场景。
  4. 你们栈里 Airflow / dbt / ClickHouse 各站哪一环。

下潜顺序:本节 → AirflowdbtCh.11 流处理(对比延迟)。


QA 疑问解答

对话里的疑问与纠错 都落在这里。正文写结构化知识;QA 保留 ❌ 我的理解 + ✅ 纠正

条目模板

### Qn:标题(日期 可选)

**关联**:[正文锚点](#...)

❌ **我的理解(错 / 不完整)**
- …

✅ **纠正**
- 

(暂无条目 — 有疑问随学随记。)


待沉淀

  • 日批计费完整 DAG 拓扑图(链到 Airflow 文档)
  • 回填规范与成本估算
  • 流批同一指标双实现时的对账(链 Ch.11