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 表)
批处理全景:历史脉络 → 挑战 → 解法(串讲)¶
一句话主线:业务要 从原始数据定期算出报表/宽表 → 数据太大单机放不下 → 用 分布计算 + 可重跑 换可靠;后来用 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 分区计算)→ Shuffle → Reduce(聚合);输入 只读;输出 新目录;失败 重跑该 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_dwd → build_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 等)对比表 | 每个系统安装教程 |
读完本节,你应该能口述¶
- 批处理在算什么(派生数据),和 OLTP 改余额有啥不同。
- 历史链:单机不够 → MR → Spark → SQL 数仓 + 编排。
- 有界、不可变分区输出、可重跑 各解决什么故障场景。
- 你们栈里 Airflow / dbt / ClickHouse 各站哪一环。
下潜顺序:本节 → Airflow → dbt → Ch.11 流处理(对比延迟)。
QA 疑问解答¶
对话里的疑问与纠错 都落在这里。正文写结构化知识;QA 保留 ❌ 我的理解 + ✅ 纠正。
条目模板:
### Qn:标题(日期 可选)
**关联**:[正文锚点](#...)
❌ **我的理解(错 / 不完整)**
- …
✅ **纠正**
- …
(暂无条目 — 有疑问随学随记。)
待沉淀¶
- 日批计费完整 DAG 拓扑图(链到 Airflow 文档)
- 回填规范与成本估算
- 流批同一指标双实现时的对账(链 Ch.11)