批处理仍然是将大量历史和操作数据移入数据湖的主要方式。虽然流式架构因其低延迟能力而受到很多关注,但批处理工作流提供了初始化数据集、执行夜间同步以及从不发送变更事件的源系统摄取数据所需的吞吐量和可靠性。一个批处理摄取工作流包括从源系统提取离散数据块,通过网络传输它,并按预定时间间隔将其持久化到存储层(通常是 Bronze 或 Raw 层)。摄取策略批处理流水线的设计很大程度上受源系统特点和数据实时性的业务要求影响。我们通常将这些工作流分为两种主要模式:全量快照和增量加载。全量快照在全量快照方法中,流水线在每个执行周期中从源表提取整个数据集,并覆盖数据湖中的目标位置。这种方法确保数据湖中的数据在提取时点与源数据完全一致,无需复杂的逻辑即可有效处理硬删除(从源中移除的记录)。此策略适用于数据量较小(例如,少于 1000 万行)的维度表或参考数据集,在这些情况下,跟踪变更的运维开销超过了简单重新加载数据的成本。然而,随着数据量增长,此方法会变得计算成本高昂且网络资源消耗大。$$ 成本_{快照} \propto O(N) $$其中 $N$ 是源数据集的总大小。随着 $N$ 的增长,摄取时间线性增加,最终会超出可用的批处理窗口。增量加载为处理大型事实表或事务日志,工程师会采用增量加载。此方法只提取自上次成功执行以来创建或修改过的记录。这需要源系统中有一个可靠的单调跟踪列,通常称为“高水位标记”或游标。常见选项包括自增主键或 updated_at 时间戳。增量提取的逻辑可以表示为:$$ R_{增量} = { r \in R_{源} \mid r.水位标记 > t_{上次成功运行} } $$实现这一点需要状态管理。编排引擎(如 Airflow 或 Dagster)必须持久存储上次成功提取的时间戳 ($t_{上次成功运行}$),并将其作为参数传递给后续作业。使用高水位标记策略的增量批处理作业工作流逻辑。digraph G { rankdir=LR; node [shape=box, style=filled, fontname="Helvetica", fontsize=10]; subgraph cluster_0 { label = "编排"; style=dashed; color="#adb5bd"; Scheduler [label="作业调度器", fillcolor="#e9ecef", color="#ced4da"]; StateStore [label="状态存储\n(上次水位标记)", fillcolor="#fff5f5", color="#ffc9c9"]; } subgraph cluster_1 { label = "执行"; style=dashed; color="#adb5bd"; Extractor [label="提取过程", fillcolor="#e7f5ff", color="#74c0fc"]; Writer [label="Parquet 写入器", fillcolor="#e7f5ff", color="#74c0fc"]; } Source [label="源数据库", shape=cylinder, fillcolor="#f8f9fa", color="#adb5bd"]; DataLake [label="数据湖\n(Bronze 层)", shape=folder, fillcolor="#b2f2bb", color="#40c057"]; Scheduler -> StateStore [label="获取上次运行"]; StateStore -> Extractor [label="传递水位标记"]; Source -> Extractor [label="SELECT * WHERE\nts > 水位标记"]; Extractor -> Writer [label="内存流"]; Writer -> DataLake [label="写入文件"]; Writer -> Scheduler [label="成功时"]; Scheduler -> StateStore [label="更新水位标记"]; }摄取期间的分区批处理摄取中一个常见错误是写入数据时没有预定义的目录结构。如果一个流水线将数千个文件倒入单个 S3 前缀或 Azure Blob 容器,下游查询引擎必须扫描整个文件列表才能找到相关数据。为优化后续检索,批处理作业应使用 Hive 风格分区写入数据。这涉及将文件路径构造为包含列名和值,通常基于摄取日期或事件日期。例如,与其写入到: s3://my-lake/sales/batch_job_123.parquet流水线应写入到: s3://my-lake/sales/ingest_date=2023-10-27/part-001.parquet这种结构允许查询引擎执行“分区裁剪”,忽略不符合查询谓词的文件夹。在设计批处理工作流时,写入器必须配置为根据数据内容或执行日期动态确定分区路径。管理加载频率和文件大小数据新鲜度(批处理运行频率)与存储效率之间存在直接权衡。过于频繁地运行批处理作业会导致“小文件问题”,即打开和关闭许多小文件的开销会降低查询性能。运行频率过低则会导致数据过时。对于标准批处理工作流,目标是生成足够大的文件,以便列式读取器高效处理(理想情况下在 128MB 到 1GB 之间),且频率足够高以满足服务级别协议 (SLA)。下图说明了全量快照和增量加载之间的效率差异,随着数据集随时间增长。虽然快照实现起来更简单,但其资源消耗使其不适用于主要的事务表。随着数据量增加,全量快照与增量加载策略所需处理时间的对比。{"layout": {"title": "处理时间 vs. 数据量", "xaxis": {"title": "自开始以来天数 (数据增长)"}, "yaxis": {"title": "处理时间 (分钟)"}, "showlegend": true, "plot_bgcolor": "#f8f9fa", "paper_bgcolor": "#f8f9fa"}, "data": [{"x": [1, 10, 20, 30, 40, 50, 60], "y": [5, 50, 100, 150, 200, 250, 300], "type": "scatter", "mode": "lines+markers", "name": "全量快照", "line": {"color": "#fa5252", "width": 3}}, {"x": [1, 10, 20, 30, 40, 50, 60], "y": [5, 6, 5, 7, 6, 8, 7], "type": "scatter", "mode": "lines+markers", "name": "增量加载", "line": {"color": "#228be6", "width": 3}}]}一致性和隔离当数据摄取到 Bronze 层时,需要定义隔离级别。在现代数据湖架构中,使用对象存储时,文件写入是原子的。一个文件要么完全可见,要么完全不可见。然而,批处理作业通常包含多个文件(多分段上传)。如果一个作业中途失败,可能会得到一个部分写入的分区。为减轻此问题,标准工程模式包括:暂存目录: 将数据写入临时前缀(例如,_temporary/),并在完成时执行纯粹基于元数据的移动(重命名)操作到最终位置。请注意,在 S3 上,重命名操作不是原子的,而是模拟复制-删除,因此这可能很慢。清单文件: 使用开放表格式,如 Iceberg 或 Delta Lake,这些格式首先写入数据文件,然后原子地提交一个指向有效文件的元数据文件(清单)。这确保消费者永远不会看到部分数据。写入-审计-发布 (WAP): 一种模式,其中数据被写入隐藏分区或分支,进行质量审计(例如,检查空主键或行数异常),然后发布到主表。处理迟到数据在增量批处理工作流中,仅依赖 created_at 时间戳可能导致数据丢失,如果在批处理窗口关闭后,记录以过去的 timestamp 插入到源数据库中。例如,如果批处理作业在 02:00 运行,覆盖到 01:59 的数据,而一条记录在 02:01 插入,其事务时间戳为 01:30(由于系统延迟),那么,下一次运行时,基于事务时间戳的标准水位标记过滤器将漏掉这条记录。为解决此问题,批处理流水线应尽可能使用 system_inserted_at 时间戳,或者应用一个重叠窗口(例如,回溯 2-3 小时),并在后续处理层中使用去重逻辑来处理重新摄取的记录。