当一个电商平台的订单数据从每天 500 万条增长到每天 5 亿条,当业务部门要求从”T+1 看报表”变成”实时看大盘”,当数据团队同时维护一套 Hadoop 批处理管线和一套 Kafka + Storm 实时管线并为两条链路的数据不一致焦头烂额时——数据密集型架构(Data-Intensive Architecture)的核心矛盾便暴露无遗。过去十年,行业经历了从 MapReduce 到 Lambda 架构(Lambda Architecture),再到 Kappa 架构(Kappa Architecture),最终走向批流一体(Unified Batch and Streaming)与湖仓一体(Lakehouse)的完整演进路径。本文将沿着这条主线,拆解每一步演进背后的技术动因与工程取舍。
一、数据架构的演进:从批处理到批流一体
1.1 批处理时代:MapReduce 与 Hive
2004 年 Google 发表 MapReduce 论文,Hadoop 生态随后在工业界普及。这一阶段的数据处理模式非常直接:
- 数据落入 HDFS(Hadoop Distributed File System);
- 定时调度 MapReduce / Hive 任务做 ETL(Extract-Transform-Load);
- 结果写入数据仓库(Data Warehouse),供 BI 工具查询。
这套架构的优点是简单、可靠、吞吐量大,但致命弱点也很明显——延迟高。一次完整的批处理管线从数据采集到结果可用,通常需要数小时甚至隔天(T+1)。对于实时风控、实时推荐、实时监控等场景,T+1 完全不可接受。
1.2 实时处理的兴起:Storm 与 Kafka
为了填补延迟空白,Storm、Samza 等流处理(Stream Processing)框架应运而生。它们的核心思想是:数据到达即处理,延迟可降至秒级甚至毫秒级。
然而早期流处理框架面临两个根本问题:
- 语义保证不足:Storm 的默认语义是 at-least-once,即消息可能被重复处理,导致计数偏高等数据质量问题;
- 无法处理历史数据:流处理只能看到”从现在开始”的数据,无法对过去一年的历史数据做全量重算。
于是,一个折中方案诞生了——Lambda 架构。
1.3 演进时间线
下面用一个简化的时间线梳理关键节点:
| 年份 | 里程碑 | 核心特征 |
|---|---|---|
| 2004 | Google MapReduce 论文 | 大规模批处理 |
| 2011 | Apache Storm 开源 | 实时流处理 |
| 2012 | Nathan Marz 提出 Lambda 架构 | 批处理 + 流处理双轨制 |
| 2014 | Jay Kreps 提出 Kappa 架构 | 以流为本的统一模型 |
| 2016 | Apache Flink 1.0 发布 | 事件驱动(Event-Driven)、精确一次语义 |
| 2018 | Spark Structured Streaming GA | 微批 + 连续处理模式 |
| 2019 | Delta Lake 开源、Apache Iceberg 进入孵化 | 数据湖上的 ACID |
| 2020 | Databricks 提出 Lakehouse 概念 | 湖仓一体 |
| 2023 | Apache Paimon 进入孵化 | 面向流的 Table Format |
二、Lambda 架构:双轨制的设计与困境
2.1 架构概览
Lambda 架构由 Nathan Marz 在其著作《Big Data: Principles and Best Practices of Scalable Realtime Data Systems》中系统阐述。其核心思想是将数据处理分为三层:
- 批处理层(Batch Layer):以 HDFS 存储不可变的全量数据(Master Dataset),定期运行 MapReduce / Spark 任务生成批视图(Batch View);
- 速度层(Speed Layer):以 Storm / Kafka Streams 处理最新到达的增量数据,生成实时视图(Real-time View);
- 服务层(Serving Layer):将批视图与实时视图合并,对外提供统一查询接口。
graph TD
A[数据源] -->|全量写入| B[批处理层<br/>Hadoop / Spark]
A -->|实时流入| C[速度层<br/>Storm / Flink]
B -->|定期生成| D[批视图<br/>Batch View]
C -->|持续更新| E[实时视图<br/>Real-time View]
D --> F[服务层<br/>Serving Layer]
E --> F
F --> G[查询接口]
2.2 Lambda 架构的优点
- 容错性强:批处理层持有全量数据,即使速度层出现 bug,只需等下一次批处理完成即可修复;
- 语义保证高:批处理层可以做到精确一次(Exactly-Once)语义,实时视图的误差由下一次批处理覆盖;
- 适配成熟生态:批处理用 Hive / Spark,流处理用 Storm / Kafka,各自使用最成熟的工具。
2.3 Lambda 架构的困境
在大规模工程实践中,Lambda 架构暴露出三个致命问题:
问题一:双倍开发与维护成本。 同一套业务逻辑需要用两种完全不同的编程模型实现——批处理通常用 SQL 或 Spark,流处理用 Java / Scala 的流式 API。两套代码的同步维护是噩梦。
// 批处理:Spark SQL 统计每小时订单量
spark.sql("""
SELECT hour(order_time) AS hr, COUNT(*) AS cnt
FROM orders
WHERE dt = '2026-04-12'
GROUP BY hour(order_time)
""");
// 流处理:Storm Bolt 实现相同逻辑
public class OrderCountBolt extends BaseRichBolt {
private Map<Integer, Long> hourlyCount = new HashMap<>();
@Override
public void execute(Tuple tuple) {
int hour = tuple.getIntegerByField("hour");
hourlyCount.merge(hour, 1L, Long::sum);
// 需要手动处理窗口、状态、容错...
}
}两段代码实现的是完全相同的业务逻辑,但编程模型、测试方式、部署运维流程完全不同。
问题二:数据一致性难以保证。 批视图与实时视图在合并时存在时间窗口的重叠或空白,可能导致数据重复计算或漏算。服务层的 merge 逻辑往往需要大量 ad-hoc 的修补代码。
问题三:运维复杂度高。 需要同时运维 HDFS 集群、批处理调度器(Oozie / Airflow)、流处理集群、服务层数据库,任何一个环节故障都可能导致数据链路断裂。
正是这些痛点,催生了 Kappa 架构。
三、Kappa 架构:以流为本的统一模型
3.1 核心思想
2014 年,LinkedIn 的 Jay Kreps(Apache Kafka 的作者)在一篇博客文章中提出了 Kappa 架构。其核心思想极其简洁:
既然所有数据都可以看作事件流(Event Stream),为什么不用一套流处理引擎同时处理实时数据和历史数据?
Kappa 架构去掉了 Lambda 的批处理层,只保留一条流处理管线:
- 所有数据写入 Kafka 这样的持久化消息队列(Durable Message Queue);
- 流处理引擎(Flink / Kafka Streams)从 Kafka 消费数据,实时计算并输出结果;
- 如果需要重算历史数据,只需将 Kafka 的消费偏移量(Offset)回拨到过去某个时间点,重新消费即可。
graph LR
A[数据源] --> B[Kafka<br/>持久化消息队列]
B --> C[流处理引擎<br/>Flink / Kafka Streams]
C --> D[结果存储<br/>实时视图]
D --> E[查询接口]
B -->|回拨 Offset| C
3.2 Kappa 架构的前提条件
Kappa 架构并非万能。它能成立依赖于两个技术前提:
- 消息队列能保存足够长的历史数据:Kafka 默认只保留 7 天数据,但可以通过配置延长保留期,或使用 Kafka 的分层存储(Tiered Storage)将冷数据迁移到对象存储(Object Storage);
- 流处理引擎能提供精确一次语义:这正是 Flink 等现代流处理引擎的核心突破。
3.3 从 Kappa 到批流一体
Kappa 架构虽然简化了架构,但在实践中仍存在局限:
- Kafka 保存全量历史数据的成本极高;
- 某些复杂的分析查询(如多表 JOIN、复杂聚合)在纯流模式下效率低于批处理。
因此,现代数据架构的最终方向是批流一体——不是”只用流”或”只用批”,而是用同一套引擎、同一套代码、同一套语义来处理批数据和流数据。Flink 和 Spark 在这个方向上走出了两条截然不同的技术路线。
四、Flink 与 Spark Structured Streaming 的架构对比
这是数据密集型架构中最核心的技术选型问题之一。两者都宣称支持批流一体,但底层架构理念截然不同。
4.1 架构理念的本质差异
Flink:流优先(Stream-First)。 Flink 将一切数据视为无界流(Unbounded Stream),批处理只是流处理的特例——有界流(Bounded Stream)。其核心执行引擎是基于事件驱动的流式引擎,天然支持低延迟处理。
Spark Structured Streaming:批优先(Batch-First)。 Spark 的核心引擎是批处理引擎,Structured Streaming 在其上构建了一个微批处理(Micro-Batch)模型——将流数据切分为一个个小的批次(通常 100ms ~ 数秒),然后用 Spark 的批处理引擎逐批执行。
4.2 精确一次语义的实现
精确一次(Exactly-Once)语义是数据一致性的基石。两者的实现路径不同:
Flink 的检查点机制(Checkpointing):
Flink 使用 Chandy-Lamport 分布式快照算法的变体。其工作流程如下:
- JobManager 定期向数据流注入屏障(Barrier);
- 当算子(Operator)收到屏障时,将当前状态异步写入外部存储(如 HDFS、S3);
- 一旦所有算子完成快照,该检查点被标记为完成;
- 故障恢复时,从最近一个完成的检查点恢复状态,并从该位置重新消费上游数据。
// Flink 检查点配置
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(60000); // 每 60 秒做一次检查点
env.getCheckpointConfig().setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE);
env.getCheckpointConfig().setMinPauseBetweenCheckpoints(30000);
env.getCheckpointConfig().setCheckpointTimeout(120000);
env.getCheckpointConfig().setMaxConcurrentCheckpoints(1);
// 配合 Kafka Source 的精确一次消费
KafkaSource<String> source = KafkaSource.<String>builder()
.setBootstrapServers("kafka:9092")
.setTopics("orders")
.setGroupId("order-processor")
.setStartingOffsets(OffsetsInitializer.committedOffsets())
.setValueOnlyDeserializer(new SimpleStringSchema())
.build();
DataStream<String> stream = env.fromSource(
source,
WatermarkStrategy.forBoundedOutOfOrderness(Duration.ofSeconds(5)),
"Kafka Source"
);Spark Structured Streaming 的 WAL 与幂等写入:
Spark 通过预写日志(Write-Ahead Log, WAL)记录每个微批次消费的 Offset 范围,结合幂等写入(Idempotent Write)或两阶段提交(Two-Phase Commit)实现端到端精确一次。
# Spark Structured Streaming 精确一次配置
df = spark \
.readStream \
.format("kafka") \
.option("kafka.bootstrap.servers", "kafka:9092") \
.option("subscribe", "orders") \
.option("startingOffsets", "earliest") \
.load()
query = df \
.selectExpr("CAST(value AS STRING)") \
.writeStream \
.outputMode("append") \
.format("delta") \
.option("checkpointLocation", "/checkpoint/orders") \
.trigger(processingTime="10 seconds") \
.start("/data/orders")4.3 状态管理
状态管理(State Management)是流处理中最具挑战性的问题。窗口聚合、去重、模式匹配等操作都需要维护跨事件的状态。
Flink 的状态后端(State Backend):
Flink 提供三种状态后端:
| 状态后端 | 存储位置 | 适用场景 | 状态大小上限 |
|---|---|---|---|
| HashMapStateBackend | JVM 堆内存 | 状态较小、低延迟 | 受 JVM 堆大小限制 |
| EmbeddedRocksDBStateBackend | 本地磁盘(RocksDB) | 状态极大 | 受磁盘大小限制 |
| ForStStateBackend | 本地磁盘(ForSt) | 状态极大、云原生优化 | 受磁盘大小限制 |
RocksDB 状态后端是生产环境的主流选择。它将状态存储在本地 SSD 上的 RocksDB 实例中,支持增量检查点(Incremental Checkpoint),只上传自上次检查点以来发生变化的 SST 文件,大幅降低检查点开销。
// 配置 RocksDB 状态后端与增量检查点
env.setStateBackend(new EmbeddedRocksDBStateBackend(true)); // true 表示启用增量检查点
env.getCheckpointConfig().setCheckpointStorage("s3://my-bucket/flink-checkpoints");Spark 的状态管理:
Spark Structured Streaming 的状态管理相对简单,状态存储在 HDFS/S3 上的检查点目录中。每个微批次结束时,状态快照写入检查点。这种方式的优点是简单可靠,但缺点是状态更新的粒度较粗——如果某个微批次处理时间过长,状态写入会成为瓶颈。
4.4 全面对比
| 维度 | Apache Flink | Spark Structured Streaming |
|---|---|---|
| 架构理念 | 流优先,批是有界流 | 批优先,流是连续微批 |
| 延迟 | 毫秒级(真正的事件驱动) | 秒级至分钟级(微批间隔) |
| 精确一次 | 分布式快照(Chandy-Lamport) | WAL + 幂等写入 |
| 状态管理 | RocksDB / 堆内存,支持增量检查点 | HDFS 检查点,全量快照 |
| 事件时间处理 | 原生 Watermark 机制,灵活度高 | 支持 Watermark,但配置灵活度较低 |
| SQL 支持 | Flink SQL,持续查询(Continuous Query) | Spark SQL,微批查询 |
| 生态成熟度 | CDC(Change Data Capture)生态完善 | 与 MLlib、GraphX 等批处理生态集成好 |
| 运维复杂度 | 较高(TaskManager、JobManager 管理) | 中等(复用 Spark 集群) |
| 社区活跃度 | 阿里巴巴主导,中国社区活跃 | Databricks 主导,全球社区广泛 |
| 典型用户 | 阿里巴巴、字节跳动、Uber | Netflix、Apple、Databricks 客户 |
4.5 选型建议
- 如果核心需求是亚秒级延迟(实时风控、实时推荐),选 Flink;
- 如果团队已有成熟的 Spark 批处理基础设施,且延迟要求在秒级到分钟级,选 Spark Structured Streaming;
- 如果需要复杂的事件驱动处理(CEP,Complex Event Processing),Flink 的 CEP 库更成熟;
- 如果数据团队同时做机器学习训练和流处理,Spark 的 MLlib 集成优势明显。
五、Lakehouse:数据湖与数据仓库的融合
5.1 数据湖与数据仓库的矛盾
在 Lakehouse 概念出现之前,企业的数据存储通常分为两个系统:
数据湖(Data Lake): 以 HDFS 或对象存储(S3、OSS)为基础,存储原始的、未经结构化处理的数据(Parquet、ORC、JSON、日志等)。优点是成本低、灵活,缺点是缺乏事务支持、数据质量差、查询性能低。
数据仓库(Data Warehouse): 以 Snowflake、Redshift、BigQuery 等为代表,提供高性能的 SQL 查询、ACID 事务、Schema 管理。优点是查询快、数据质量高,缺点是存储成本高、数据需要从数据湖 ETL 过来(引入额外延迟和复杂度)。
这种”两套系统”的架构带来了与 Lambda 架构类似的问题——数据冗余、一致性难保证、运维复杂。
5.2 Lakehouse 的定义
2020 年,Databricks 在论文《Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics》中正式提出 Lakehouse 概念。其核心思想是:
在数据湖的低成本存储之上,叠加数据仓库的管理能力(ACID 事务、Schema 管理、索引优化),实现一套系统同时满足 BI 分析(Business Intelligence)和机器学习(Machine Learning)的需求。
graph TB
subgraph Lakehouse 架构
A[对象存储<br/>S3 / HDFS / OSS] --> B[Table Format<br/>Delta Lake / Iceberg / Hudi]
B --> C[计算引擎<br/>Spark / Flink / Trino / Presto]
C --> D1[BI 分析]
C --> D2[机器学习]
C --> D3[实时数仓]
end
subgraph 传统双系统架构
E[数据湖<br/>S3 / HDFS] -->|ETL| F[数据仓库<br/>Redshift / Snowflake]
E --> G[ML 引擎<br/>TensorFlow / PyTorch]
F --> H[BI 工具]
end
5.3 Lakehouse 的关键技术层
Lakehouse 之所以能在数据湖上实现数据仓库级别的管理能力,核心在于 Table Format 层。Table Format 在原始数据文件(Parquet/ORC)之上引入了一层元数据管理,提供以下关键能力:
- ACID 事务:支持并发读写,写操作原子性提交;
- 时间旅行(Time Travel):可以查询任意历史版本的数据;
- Schema 演化(Schema Evolution):支持在不重写数据的前提下添加、删除、重命名列;
- 分区演化(Partition Evolution):支持在不重写数据的前提下修改分区策略;
- 数据跳过(Data Skipping):通过列统计信息(min/max/count)跳过不相关的数据文件,加速查询。
六、Table Format 之争:Delta Lake vs Iceberg vs Hudi
Table Format 是 Lakehouse 架构的核心支撑层。当前行业存在三个主要的开源 Table Format,各有侧重。
6.1 Delta Lake
Delta Lake 由 Databricks 开源,与 Spark 生态深度绑定。
核心机制: Delta Lake
使用事务日志(Transaction Log)——_delta_log
目录下的 JSON
文件——记录每次对表的操作(添加文件、删除文件、修改元数据等)。每次写操作生成一个新的
JSON 文件(如 000001.json),每 10 个 JSON
文件自动合并为一个 Checkpoint 文件(Parquet
格式)以加速元数据读取。
-- Delta Lake 时间旅行:查询 1 小时前的数据
SELECT * FROM orders VERSION AS OF 42;
-- 或者按时间戳查询
SELECT * FROM orders TIMESTAMP AS OF '2026-04-12 14:00:00';
-- Delta Lake MERGE(Upsert)操作
MERGE INTO target_table AS t
USING source_table AS s
ON t.order_id = s.order_id
WHEN MATCHED THEN
UPDATE SET t.status = s.status, t.update_time = s.update_time
WHEN NOT MATCHED THEN
INSERT (order_id, user_id, amount, status, update_time)
VALUES (s.order_id, s.user_id, s.amount, s.status, s.update_time);6.2 Apache Iceberg
Apache Iceberg 由 Netflix 开源,设计目标是引擎无关(Engine-Agnostic)。
核心机制: Iceberg 使用分层的元数据结构:
- Catalog:指向当前元数据文件的指针(如 Hive Metastore、Nessie、REST Catalog);
- Metadata File(JSON):记录表的 Schema、分区规范、快照列表;
- Manifest List(Avro):每个快照对应一个 Manifest List,列出该快照包含的所有 Manifest 文件;
- Manifest File(Avro):列出数据文件及其列统计信息(min/max/null count);
- Data File(Parquet/ORC/Avro):实际的数据文件。
这种分层设计的关键优势是分区演化。传统 Hive 分区方案一旦确定便无法更改(如从按天分区改为按小时分区需要重写所有数据),而 Iceberg 的分区信息存储在 Manifest 级别,可以在不重写数据的前提下修改分区策略。
-- Iceberg 分区演化:从按天分区改为按小时分区
ALTER TABLE orders WRITE ORDERED BY (order_time);
ALTER TABLE orders SET PARTITION SPEC (
hours(order_time)
);
-- 后续写入的数据使用新的分区策略,旧数据保持不变
-- Iceberg 快照查询
SELECT * FROM orders FOR SYSTEM_TIME AS OF TIMESTAMP '2026-04-12 14:00:00';
-- Iceberg 增量读取(用于构建流式管线)
SELECT * FROM orders
WHERE snapshot_id BETWEEN 1001 AND 1005;6.3 Apache Hudi
Apache Hudi(Hadoop Upserts Deletes and Incrementals)由 Uber 开源,最初的设计目标是解决数据湖上的 Upsert 问题。Hudi 提供两种存储类型:
- Copy-on-Write(COW):写入时合并数据文件,读取性能高但写入较慢;
- Merge-on-Read(MOR):写入时先写增量日志(Delta Log),读取时合并,写入快但读取需额外开销。
6.4 三者对比
| 维度 | Delta Lake | Apache Iceberg | Apache Hudi |
|---|---|---|---|
| 开源方 | Databricks | Netflix / Apple | Uber |
| 引擎绑定 | 与 Spark 深度绑定 | 引擎无关 | 主要支持 Spark |
| 元数据管理 | 事务日志(JSON + Parquet Checkpoint) | 分层元数据(Catalog → Manifest) | 时间线(Timeline)机制 |
| 分区演化 | 不支持(需重写数据) | 原生支持 | 部分支持 |
| Schema 演化 | 支持(添加列、重命名列) | 完整支持(基于 ID 的 Schema) | 支持(添加列) |
| 时间旅行 | 支持 | 支持 | 支持 |
| 流式读写 | 支持(Spark Structured Streaming) | 支持(Flink Iceberg Connector) | 原生支持(核心设计目标) |
| 并发控制 | 乐观并发(Optimistic Concurrency) | 乐观并发 + 序列化隔离 | 基于时间线的 MVCC |
| 社区与生态 | Databricks 主导,商业化成熟 | Apache 基金会,厂商中立 | Uber 主导,Onehouse 商业化 |
6.5 选型建议
- 如果团队重度使用 Databricks / Spark,Delta Lake 是最自然的选择;
- 如果需要多引擎支持(Spark + Flink + Trino + Presto),Iceberg 的引擎无关设计更合适;
- 如果核心场景是高频 Upsert(如 CDC 数据入湖),Hudi 的 MOR 模式写入性能最优;
- 长期来看,Iceberg 的分区演化和厂商中立特性使其在多云(Multi-Cloud)架构中越来越受欢迎。
七、实时数仓的落地架构
7.1 实时数仓的分层模型
传统离线数仓遵循经典的分层模型:ODS → DWD → DWS → ADS。实时数仓的分层模型与之对应,但每一层都需要实时更新:
| 层次 | 离线数仓 | 实时数仓 | 技术选型 |
|---|---|---|---|
| ODS(Operational Data Store) | HDFS 原始文件 | Kafka Topic | Kafka / Pulsar |
| DWD(Data Warehouse Detail) | Hive 明细表 | Flink 清洗后写入 Kafka/Iceberg | Flink SQL |
| DWS(Data Warehouse Summary) | Hive 汇总表 | Flink 聚合后写入 OLAP 引擎 | Flink SQL + StarRocks |
| ADS(Application Data Store) | MySQL 结果表 | OLAP 引擎直接服务查询 | StarRocks / ClickHouse |
7.2 典型落地架构
下面给出一个生产级实时数仓架构的完整技术栈:
数据源(MySQL / MongoDB / 日志)
│
▼ CDC(Debezium / Flink CDC)
│
Kafka(ODS 层:原始事件流)
│
▼ Flink SQL(数据清洗、维表关联)
│
Iceberg / Kafka(DWD 层:明细事实表)
│
▼ Flink SQL(窗口聚合、指标计算)
│
StarRocks / ClickHouse(DWS + ADS 层:汇总与服务)
│
▼
BI 工具 / 数据 API / 实时大屏
7.3 Flink CDC 实现全链路实时化
Flink CDC(Change Data Capture)是打通实时数仓全链路的关键组件。它可以直接读取 MySQL 的 Binlog、PostgreSQL 的 WAL、MongoDB 的 Oplog,将数据变更事件转化为 Flink 的数据流。
-- Flink SQL:创建 MySQL CDC Source 表
CREATE TABLE orders_source (
order_id BIGINT,
user_id BIGINT,
product_id BIGINT,
amount DECIMAL(10, 2),
status STRING,
order_time TIMESTAMP(3),
update_time TIMESTAMP(3),
PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
'connector' = 'mysql-cdc',
'hostname' = 'mysql-host',
'port' = '3306',
'username' = 'flink_reader',
'password' = '${secret}',
'database-name' = 'ecommerce',
'table-name' = 'orders',
'scan.startup.mode' = 'initial'
);
-- Flink SQL:创建 Iceberg Sink 表(DWD 层)
CREATE TABLE dwd_orders (
order_id BIGINT,
user_id BIGINT,
product_id BIGINT,
amount DECIMAL(10, 2),
status STRING,
order_time TIMESTAMP(3),
update_time TIMESTAMP(3),
user_name STRING,
user_level STRING,
PRIMARY KEY (order_id) NOT ENFORCED
) WITH (
'connector' = 'iceberg',
'catalog-name' = 'iceberg_catalog',
'catalog-type' = 'hive',
'warehouse' = 's3://data-lake/iceberg',
'format-version' = '2',
'write.upsert.enabled' = 'true'
);
-- Flink SQL:维表关联,将订单与用户信息打宽
INSERT INTO dwd_orders
SELECT
o.order_id,
o.user_id,
o.product_id,
o.amount,
o.status,
o.order_time,
o.update_time,
u.user_name,
u.user_level
FROM orders_source AS o
LEFT JOIN users_dim FOR SYSTEM_TIME AS OF o.proctime AS u
ON o.user_id = u.user_id;7.4 物化视图与增量计算
实时数仓的核心技术挑战之一是增量计算(Incremental Computation)。传统批处理每次全量重算所有数据,而实时数仓需要在新数据到达时只计算增量部分。
物化视图(Materialized View) 是增量计算的典型载体。以 StarRocks 为例:
-- StarRocks:创建异步物化视图实现增量聚合
CREATE MATERIALIZED VIEW mv_hourly_revenue
REFRESH ASYNC EVERY (INTERVAL 1 MINUTE)
AS
SELECT
DATE_TRUNC('hour', order_time) AS hour_key,
product_category,
COUNT(*) AS order_count,
SUM(amount) AS total_revenue,
AVG(amount) AS avg_order_amount
FROM dwd_orders
WHERE status = 'completed'
GROUP BY
DATE_TRUNC('hour', order_time),
product_category;
-- 查询时自动路由到物化视图
SELECT * FROM mv_hourly_revenue
WHERE hour_key >= '2026-04-12 10:00:00'
ORDER BY total_revenue DESC;Flink 的持续查询(Continuous Query)也是增量计算的一种实现。Flink SQL 中的 GROUP BY 聚合本质上维护了一个持续更新的物化视图:
-- Flink SQL:持续计算每分钟每品类的订单量和 GMV
CREATE TABLE dws_realtime_metrics (
window_start TIMESTAMP(3),
product_category STRING,
order_count BIGINT,
gmv DECIMAL(18, 2),
PRIMARY KEY (window_start, product_category) NOT ENFORCED
) WITH (
'connector' = 'starrocks',
'jdbc-url' = 'jdbc:mysql://starrocks:9030',
'load-url' = 'starrocks:8030',
'database-name' = 'realtime_dw',
'table-name' = 'dws_realtime_metrics',
'sink.properties.format' = 'json',
'sink.buffer-flush.interval-ms' = '5000'
);
INSERT INTO dws_realtime_metrics
SELECT
TUMBLE_START(order_time, INTERVAL '1' MINUTE) AS window_start,
product_category,
COUNT(*) AS order_count,
SUM(amount) AS gmv
FROM dwd_orders
GROUP BY
TUMBLE(order_time, INTERVAL '1' MINUTE),
product_category;7.5 工程案例:某电商平台实时数仓建设
背景: 某头部电商平台日均订单量 3 亿笔,峰值 QPS(Queries Per Second)达 50 万。原有架构为经典 Lambda 架构:离线链路使用 Spark + Hive(T+1),实时链路使用 Storm + HBase。两条链路的数据不一致率约 3%~5%,且实时链路在大促期间经常出现反压(Backpressure)导致数据延迟飙升至分钟级。
改造目标:
- 统一批流处理链路,消除数据不一致;
- 端到端延迟从分钟级降至秒级;
- 存储成本降低 40%。
技术方案:
| 组件 | 改造前 | 改造后 |
|---|---|---|
| 消息队列 | Kafka 0.10 | Kafka 3.x |
| 流处理引擎 | Storm(实时) + Spark(离线) | Flink 1.18(统一) |
| 数据湖存储 | HDFS + Hive | S3 + Apache Iceberg |
| OLAP 引擎 | Druid | StarRocks |
| CDC 工具 | 自研 Binlog 解析 | Flink CDC 3.0 |
| 调度系统 | Oozie | 取消(Flink 常驻任务) |
改造效果:
| 指标 | 改造前 | 改造后 | 改善幅度 |
|---|---|---|---|
| 端到端延迟 | 1~5 分钟 | 3~8 秒 | 降低 97% |
| 批流数据一致率 | 95%~97% | 99.99% | 显著提升 |
| 存储成本(月均) | 280 万元 | 168 万元 | 降低 40% |
| ETL 代码行数 | 12 万行(两套) | 4.5 万行(一套) | 减少 62% |
| 故障恢复时间(RTO) | 30~60 分钟 | 2~5 分钟 | 降低 90% |
关键技术决策:
- 选择 Iceberg 而非 Delta Lake,因为需要同时支持 Flink 写入和 Trino 查询,Iceberg 的引擎无关特性是关键;
- Flink CDC 替代自研 Binlog 解析器,减少约 2 万行自研代码的维护成本;
- StarRocks 替代 Druid,因为 StarRocks 的 SQL 兼容性更好,且支持物化视图自动刷新。
7.6 实时数仓的挑战与应对
实时数仓虽然在延迟和一致性上有巨大优势,但也引入了新的挑战:
挑战一:数据回溯(Data Backfill)。 当业务逻辑变更需要重算历史数据时,纯流式架构需要从 Kafka 回拨 Offset 重消费,但 Kafka 通常只保留有限天数的数据。解决方案是使用 Iceberg 的快照读取能力从数据湖回溯:
-- 使用 Flink 读取 Iceberg 历史快照进行回溯计算
CREATE TABLE orders_backfill (
order_id BIGINT,
user_id BIGINT,
amount DECIMAL(10, 2),
order_time TIMESTAMP(3)
) WITH (
'connector' = 'iceberg',
'catalog-name' = 'iceberg_catalog',
'catalog-type' = 'hive',
'warehouse' = 's3://data-lake/iceberg',
'streaming' = 'false',
'snapshot-id' = '3821550127947089987'
);挑战二:小文件问题(Small File Problem)。 流式写入会产生大量小文件,严重影响读取性能和存储效率。Iceberg 提供了自动合并(Auto-Compaction)功能:
-- Iceberg 小文件合并
CALL iceberg_catalog.system.rewrite_data_files(
table => 'db.dwd_orders',
strategy => 'sort',
sort_order => 'order_time ASC',
options => map(
'target-file-size-bytes', '268435456',
'min-file-size-bytes', '67108864',
'max-file-size-bytes', '536870912'
)
);挑战三:精确一次的端到端保障。 从 Source 到 Sink 的精确一次语义需要整条链路的配合。以 Flink + Kafka + Iceberg 链路为例,需要同时满足:
- Kafka Source 使用 Committed Offset 追踪消费进度;
- Flink 启用 Exactly-Once 检查点模式;
- Iceberg Sink 使用两阶段提交协议(Two-Phase Commit),在检查点完成时才将数据文件提交为可见。
7.7 批流一体的数据质量保障
数据质量(Data Quality)在实时场景下更难保障,因为数据一旦流过便不可回头。常见的实时数据质量检查手段包括:
-- Flink SQL:实时数据质量监控
-- 检测订单金额异常(金额为负或超过阈值)
SELECT
TUMBLE_START(order_time, INTERVAL '5' MINUTE) AS check_window,
COUNT(*) AS total_orders,
SUM(CASE WHEN amount <= 0 THEN 1 ELSE 0 END) AS negative_amount_count,
SUM(CASE WHEN amount > 100000 THEN 1 ELSE 0 END) AS high_amount_count,
SUM(CASE WHEN user_id IS NULL THEN 1 ELSE 0 END) AS null_user_count
FROM orders_source
GROUP BY TUMBLE(order_time, INTERVAL '5' MINUTE)
HAVING
SUM(CASE WHEN amount <= 0 THEN 1 ELSE 0 END) > 10
OR SUM(CASE WHEN user_id IS NULL THEN 1 ELSE 0 END) > 100;
-- 异常数据写入告警队列八、总结
数据密集型架构的演进历程可以用一句话概括:从分裂走向统一。
| 演进阶段 | 核心思想 | 解决的问题 | 引入的新问题 |
|---|---|---|---|
| 纯批处理 | 全量定期计算 | 大规模数据处理 | 延迟高(T+1) |
| Lambda 架构 | 批流双轨制 | 兼顾延迟和准确性 | 双倍开发成本、数据不一致 |
| Kappa 架构 | 以流为本 | 统一处理链路 | 历史回溯困难、复杂查询低效 |
| 批流一体 | 同一引擎、同一代码 | 消除批流割裂 | 引擎选型复杂 |
| Lakehouse | 湖仓融合 | 消除数据湖与仓库割裂 | Table Format 选型、生态碎片化 |
选择哪条路线,取决于业务的具体约束:
- 延迟要求:亚秒级选 Flink,秒级到分钟级 Spark 足矣;
- 数据规模:PB 级数据选 Lakehouse + Iceberg,TB 级以下用传统数仓可能更简单;
- 团队能力:Flink 的运维门槛高于 Spark,需评估团队的技术储备;
- 生态兼容:多引擎场景选 Iceberg,纯 Spark 场景选 Delta Lake;
- 成本预算:Lakehouse 的存储成本远低于传统数仓,但计算成本需根据查询模式评估。
没有万能的架构,只有在具体约束下的最优解。理解每一步演进背后的技术动因和工程取舍,比记住某个框架的 API 更为重要。
下一篇:【系统架构设计百科】多租户架构
参考资料
- Marz N, Warren J. Big Data: Principles and Best Practices of Scalable Realtime Data Systems. Manning Publications, 2015.
- Kreps J. Questioning the Lambda Architecture. O’Reilly Radar, 2014.
- Armbrust M, et al. Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics. CIDR, 2021.
- Carbone P, et al. Apache Flink: Stream and Batch Processing in a Single Engine. IEEE Data Engineering Bulletin, 2015.
- Zaharia M, et al. Apache Spark: A Unified Engine for Big Data Processing. Communications of the ACM, 2016.
- Armbrust M, et al. Delta Lake: High-Performance ACID Table Storage over Cloud Object Stores. PVLDB, 2020.
- Apache Iceberg Documentation. https://iceberg.apache.org/docs/latest/
- Apache Flink Documentation. https://nightlies.apache.org/flink/flink-docs-stable/
- Databricks. The Rise of the Lakehouse. https://www.databricks.com/research/lakehouse
- StarRocks Documentation. https://docs.starrocks.io/
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【系统架构设计百科】数据湖与数据仓库:分析架构的演进路线
Lambda、Kappa、Lakehouse 三种架构的本质区别和适用场景是什么?本文深入 Delta Lake 和 Apache Iceberg 的设计原理,分析流批一体的工程挑战,并提供数据质量保证的架构方案。
【系统架构设计百科】流处理架构:从批处理到实时的范式迁移
流处理的 exactly-once 语义在工程上到底有多难?窗口计算的语义陷阱是什么?本文深入 Flink 的 checkpoint 机制、事件时间与处理时间的工程影响,对比 Kafka Streams 与 Flink 的架构差异。
【系统架构设计百科】架构质量属性:不只是"高可用高性能"
需求评审时写下的'高可用、高性能、高并发',到了架构设计阶段几乎无法落地——因为它们不是可执行的需求。本文从 SEI/CMU 的质量属性理论出发,用 stimulus-response 场景模型把模糊需求变成可量化、可验证的架构约束,并拆解属性之间的冲突与联动关系。
【系统架构设计百科】告警策略:如何避免"狼来了"
大多数团队的告警系统都在制造噪声而不是传递信号。阈值告警看似直观,实则产生大量误报和漏报,值班工程师在凌晨三点被叫醒,却发现只是一次无害的毛刺。本文从告警疲劳的工业数据出发,拆解基于 SLO 的多窗口燃烧率告警算法,深入 Alertmanager 的路由、抑制与分组机制,结合 PagerDuty 的告警疲劳研究和真实工程案例,给出一套可落地的告警策略设计方法。