最近总有人问我,湖仓一体都这么火了,那些传统数仓的四层模型是不是可以直接拆了?说实话,“拆”这个字本身就用错了。
湖仓一体和分层架构从来不是对立的。数据仓库分层的价值在于把混乱的业务数据整理成可分析、可复用的资产;湖仓架构解决的是“在数据量快速增长、实时需求变多、数据形态越来越复杂的前提下,把分层这件事做得更便宜、更灵活、更快”。这也是为什么我更倾向于把两者的关系看成“从分层架构走向数据湖仓架构”的演进,而不是后者对前者的彻底替换。下面我会把整个演进链路一层层拆开,讲清楚每一层为什么存在、在哪里发生了变化、具体落地时该怎么操作。
1. 数据仓库为什么一定要“分层”:从业务场景倒推设计逻辑
1.1 分层不是架构师的洁癖,而是被业务需求逼出来的
很多刚接触数仓的人会觉得分层是文档里的固定套路,背下来就行:ODS贴源、DWD明细、DWS汇总、ADS应用。实际上,分层是被现实逼出来的。
业务系统的数据库是为交易设计的,表结构遵循范式模型,索引按主键和常用查询配置。分析需求恰恰相反,经常要全表扫描、要跨表关联几十个维度、要按任意时间窗口做聚合。把这种查询直接扔到业务库上,轻则一条SQL拖垮库,重则把面向用户的交易链路都给锁死。所以必须把数据搬出来,单独为分析场景建一套数据组织方式,分层的雏形就这么出现了。
拿生活化的事情来类比:业务库像柜台上的记账本,分析需求像做财务年度报表。正常人不会直接翻开正在使用的记账本边算边给客户办业务,而是先把账目抄录归档、做分类汇总,形成专门的分析台账。数仓分层做的就是这件事。
1.2 每一层解决一个具体问题:从ODS到ADS的职责划分
传统数仓最常见的是四层架构,每一层都有明确的“使命”:
- ODS(操作数据存储)层:解决“数据能不能回溯”的问题。这层原样保存源系统的数据,包括字段、类型、删除标记甚至变更日志,尽量不做复杂清洗。作用是在出问题的时候可以回到原始数据重新加工,也为将来无法预知的分析需求保留一手资料。
- DWD(明细数据层)层:解决“这行数据可不可信”的问题。这层做格式统一、编码统一、口径修正、去重降维,把不同业务系统的数据整理成一个规范的明细事实。比如“订单金额是否含税”“会员等级按什么规则定义”,都在这一层沉淀成统一口径。
- DWS(汇总数据层)层:解决“指标能不能复用”的问题。按业务主题构建宽表和轻度汇总,比如按天、按门店、按品类汇总的订单表。通过预聚合,下游报表直接查汇总结果就行,不用每次都扫一遍明细。
- ADS(应用数据层)层:解决“报表好不好查”的问题。面向具体应用输出定制数据集市,字段冗余度高,查询路径短,性能表现好。
四层逻辑可以用一张简单的职责表来对照:
| 分层 | 核心问题 | 数据形态 | 典型场景 |
|---|---|---|---|
| ODS | 能否回溯 | 原样贴源数据 | 数据追溯、重跑修复 |
| DWD | 是否可信 | 规范化明细事实 | 自助分析、指标口径统一 |
| DWS | 能否复用 | 主题宽表、轻度汇总 | 标准报表、多维分析 |
| ADS | 是否好查 | 应用专项数据集 | 数据大屏、接口服务 |
1.3 分层不是免费的,它也有代价
任何一个设计都有成本,分层也不例外。
每一层都物理落表,导致存储冗余非常高。同样一份订单数据,ODS有一份、DWD有一份、DWS又汇总了一份,如果按年保留全量,存储会成倍增长。同时,层与层之间是强依赖的ETL任务链路,上游哪一层失败,下游全部阻塞。为了保障数据一致性,调度系统还要做复杂的重跑和回溯,运维成本直接拉满。
这就不难理解为什么有人开始怀疑分层:链路变长、时效变差、成本变高,是不是砍掉几层反而更快?答案是:分层依然必要,但传统“物理表+任务依赖”的实现方式需要升级。这也是数据湖仓架构登场的根本原因。
2. 数据体量膨胀之后:传统分层架构真正扛不住的是什么
2.1 存储成本与冗余:三层全量副本叠加的扩容压力
传统数仓面临的第一座山是存储成本。
举个例子:某业务每天新增1TB的订单明细和日志数据,保留180天。在ODS、DWD、DWS三层各保留一份全量数据的情况下,光这三份就是1TB×180×3,约540TB。如果再加上副本、备份、临时表和一些测试数据,冲到1PB以上很常见。
传统MPP数仓大多采用“计算存储一体”的集群架构,扩容必须把存储和计算一起加。数据量涨了,CPU和内存也跟着被迫扩容,成本非常高,扩容周期还长。数据湖的优势在于把数据放到廉价的对象存储或分布式文件系统上,计算和存储可以各自伸缩,这个瓶颈直接被解开了。
2.2 计算时效:T+1调度链路和实时需求之间的矛盾
第二个扛不住的是时效。
一条传统ETL链路由多个任务串联:源库抽取、质检、进ODS、清洗生成DWD、聚合生成DWS、输出ADS。如果每个环节平均耗时20分钟,整条链路端到端延迟就要100分钟起步。一旦某个环节因数据质量问题失败,重跑又要几个小时。对纯离线报表来说可以忍,对实时看板、实时风控、大促监控来说,这种链路根本无法满足要求。
说到实时化,还有一个绕不开的痛点:很多团队为了做实时指标,会绕过分层另外建一套实时计算链路。两套链路同时跑,口径还不一致,比如GMV一个按“下单时间”算,一个按“支付时间”算,业务方同时看两个数字自然会对不上。这种“实时与离线口径撕裂”的问题,比单纯的性能问题难解决得多。
2.3 半结构化数据和建模灵活性的挑战
传统数仓擅长处理结构化关系模型,但现在的数据源越来越复杂:服务日志、移动端埋点、设备上报的JSON、外部接口返回的嵌套结构,都要进数仓。
传统做法要求先建模、再导入,数据落地前就得定义好schema。问题在于数据探索阶段往往根本不知道未来需要什么字段,硬要提前定义就会反复改表结构,效率极低。数据湖“先存储、后解析”的模式更适合这种场景,却由此带来了新的管理问题,这就该引入湖仓架构了。
3. 湖仓一体架构的底层变革:文件、表格式和计算引擎如何重新分工
3.1 数据湖先解决了“存得下”,却掉进了数据沼泽
数据湖的初衷很直接:把各种格式的数据低成本地放到一起,将来再想怎么用。但实践一段时间后大家发现,一个毫无约束的数据湖很快会变成“数据沼泽”。
文件堆了一堆,却不知道哪份数据和哪份数据有关联;多个任务同时写文件,可能覆盖或读到半成品数据;没有事务机制,同一张表并发提交时很容易脏读;表结构也没有统一约定,下游消费全靠猜。这些问题决定了,单纯把数据扔进湖里是走不远的,必须给数据补上“表语义”。
3.2 表格式是湖仓架构的灵魂
湖仓一体核心的底层创新,不在存储层,而在存储之上新增的“表格式”(Table Format)这一层。
表格式不是新的物理文件格式,而是在Parquet、ORC这些数据文件之上,增加一层元数据和事务管理。它要回答几个核心问题:哪些文件属于哪张表的哪个快照?表结构是怎么演进的?哪些快照可以回滚?哪些文件是增量提交的?当这些问题都有了明确答案,数据湖才真正拥有了接近数仓的“表能力”。
做个类比:没有表格式的数据湖,像把照片直接堆在网盘文件夹里,要找到某一张只能靠文件名;有了表格式,等于给照片加了一套带版本管理的相册,每次增删改都有记录,每一版相册状态都能还原。
3.3 三种主流表格式的选型对比:Delta、Iceberg、Hudi
目前主流的表格式有三类:Delta Lake、Apache Iceberg、Apache Hudi。很多团队会在选型上纠结,我给一个相对客观的对比:
| 维度 | Delta Lake | Apache Iceberg | Apache Hudi |
|---|---|---|---|
| 生态主导 | Databricks生态,Spark集成最顺 | 开源中立,跨引擎支持广 | 社区驱动,偏实时增量更新 |
| 元数据实现 | 事务日志(Transaction Log) | Manifest清单结构 | Timeline时间线+文件组 |
| ACID事务 | 支持 | 支持快照隔离 | 支持,基于时间线管理 |
| Schema演进 | 支持 | 支持 | 支持 |
| 增量读取 | 支持 | 支持 | 增量视图最成熟 |
| 典型优势 | 与Spark深度结合 | 大规模分析、统一底座 | 流批一体、频繁upsert |
选型没有绝对标准答案,主要看业务模式。如果团队深度绑定Spark和Databricks,Delta很顺;如果要建一个跨Spark、Flink、Trino的公共数据底座,Iceberg更中立;如果核心诉求是分钟级更新、增量入湖、多版本更新,Hudi的优势会明显。
3.4 计算引擎与表格式的协作:写一个增量入湖任务的真实样子
很多人误以为引入湖仓就要把整套ETL重写,其实不是。表格式兼容SQL和DataFrame API,既有逻辑可以平滑迁移。以Spark写Hudi表为例:
// 使用Spark把清洗后的数据写入Hudi表 df.write .format("hudi") .option("hoodie.table.name", "dwd_order_detail") .option("hoodie.datasource.write.recordkey.field", "order_id") .option("hoodie.datasource.write.partitionpath.field", "dt") .option("hoodie.datasource.write.precombine.field", "etl_time") .mode(Append) .save("/warehouse/dwd/order_detail")这几个核心配置项要理解清楚:recordkey是记录主键,用来唯一标识一行;partitionpath是分区字段,控制数据落在哪个目录;precombine是同一主键多条记录时的合并优先级字段,比如取最新的etl_time。理解了这三点,就基本掌握了数据入库的语义。
读的时候同样简单,以Iceberg为例:
SELECT dt, shop_id, order_id, order_amount FROM iceberg_catalog.prod.dwd_order_detail FOR SYSTEM_TIME AS OF '2025-04-01 00:00:00' WHERE dt >= '2025-04-01';这里读的是指定时间快照,也就是时间旅行能力。传统数仓一旦数据被覆盖,想看历史状态非常困难,在湖仓里只是一条SQL的事。
4. 湖仓时代的分层设计演进:层级职责迁移与落地形态
4.1 ODS层:从“贴源表”到“入湖原样区”
湖仓架构下,ODS层的目标没变,还是“原样保留、可回溯”,但承载方式完全不同。
传统ODS表要按源系统结构建库建表,有明确的schema约束。湖仓的ODS则更像“原样入湖区”:业务数据通过CDC写入Kafka,再由Flink任务落到Hudi或Iceberg表,尽量保留原始payload和关键元数据字段(比如数据来源、采集时间、操作类型)。后续真要重新解析,随时可以基于原始数据再做一遍。
这层落地时有一个很实用的细节:给每个ODS表都加上“采集时间”和“操作类型”字段,使这层存储天然支持流式增量写,而不是每天全量覆盖。这样做的价值在于,未来调整DWD口径时不需要回源系统重新抽数,直接从ODS重放即可。
4.2 DWD和DWS层:从“物理固定表”到“增量构建视图”
DWD层的核心变化是从“每日全量重刷”变为“增量upsert”。
以前DWD表每天凌晨全量计算,任务重、窗口长、失败后重跑代价大。湖仓场景下,DWD表以业务主键(比如order_id)作为Hudi记录键,每天只处理变化的数据:新订单插入,订单状态变更做更新。已经处理过的主键不需要再算一遍。任务耗时从几小时降到几十分钟,效果非常明显。
DWS层可以继续保留“物理汇总表”的模式,每天增量更新一次,也可以用物化视图的方式自动维护。如果报表分析比较灵活,推荐把DWS设计成“基础汇总维度+可扩展主题宽表”的组合:基础汇总控制成本,主题宽表提高灵活性。传统数仓里“物化视图”受限于引擎,湖仓中表格式天然支持这类场景,工程实现简单很多。
4.3 实时分层:流批一体让数据新鲜度从T+1走向分钟级
湖仓的分层不再把“离线”和“实时”分成两套体系,而是通过流批一体把它们合到一张表上。
比如用Flink流式写Hudi的MOR表,每5分钟提交一批数据。下游要做“近乎实时”的报表,直接读MOR表的读取优化视图,看到的就是已提交的最新数据;如果还要往下游的OLAP引擎同步状态,则读增量视图,只消费变更记录再做二次加工。整个过程可以提炼成一句话:分层还在,DWD还是DWD,但已经不需要维护一条“实时中心”和一条“离线中心”了。
4.4 一个可参考的完整湖仓分层落地形态
结合前面几节的思路,给出一套可参考的虚拟零售企业湖仓分层方案:
| 分区 | 存储载体 | 表格式 | 时效性 | 典型场景 |
|---|---|---|---|---|
| 原始区 | 对象存储+分布式文件系统 | Hudi/Iceberg | 分钟级 | 数据追溯、数据重放 |
| 明细区 | 数据湖目录 | Hudi MOR/Iceberg | 分钟-小时级 | 自助分析、指标口径统一 |
| 汇总区 | 数据湖目录 | Hudi/Iceberg | 小时级 | 标准报表、多维分析 |
| 应用区 | Trino视图或者同步到OLAP引擎 | SQL视图/外部表 | 秒级 | 大屏、数据接口 |
这只是一个常见形态,不是唯一标准。比如应用区到底要不要同步到ClickHouse、Doris这类OLAP引擎,取决于查询特征:低频大范围分析放湖里没问题,高频点查和复杂聚合还是交给专门引擎更稳。湖仓架构从来不排斥合适的专用组件。
5. 从传统数仓迁移到湖仓的落地路线与实战经验
5.1 迁移前先回答三个问题
第一个问题:到底想解决什么?想降低存储成本、想提升时效、想统一口径、还是想支持更灵活的分析?目标不同,表格式和落地方案的选择完全不同。
第二个问题:团队掌握什么技能?湖仓的日常运维离不开Spark、Flink,如果团队只会写SQL,迁移难度会大很多。至少要提前培养两三个能写Spark/Flink作业的工程师。
第三个问题:怎么才算迁移成功?建议在项目启动前定义可量化的验收标准,比如数据对账通过率必须达到100%、核心报表查询耗时下降多少、每日任务跑批耗时控制在多少分钟以内。没有验收标准就盲目迁移,最后很可能两边都维护,越来越乱。
5.2 分四个阶段迁移的完整路线
我实践下来比较稳妥的做法是分四步走,每一步都有独立的验证点。
第一阶段,先把ODS层迁入湖仓。保留原有ETL逻辑,只把目标存储从传统数仓表切换到Hudi或者Iceberg表,同时保持旧链路并行运行。验证重点是比较两个链路写入后的数据条数和抽样字段是否完全一致。
第二阶段,改造DWD层增量更新。这步的关键是先给明细数据找到业务主键和增量时间字段,比如订单表用order_id,用户表用user_id加上updated_at。任务从全量扫描改成增量过滤,只处理变化记录。跑通之后,任务耗时会明显下降。
第三阶段,把DWS的计算逻辑下推到湖仓。原来的日任务里,明细关联和聚合运算在结果表生成前集中完成;改造后改为对湖表的新增分区执行聚合,再按维度主键upsert进汇总表。这样既减少了重复扫描明细,又避免了全表重建。
第四阶段,切换流量。低频报表先迁,高频接口后迁。从传统数仓切到湖仓后,要留出一到两周的新老并行对账期,每天比对汇总值和异常记录数。连续一周稳定一致,再逐步下线旧链路。
5.3 那些文档里不会写的踩坑经验
迁移和技术选型过程中有几个坑,几乎每个湖仓项目都会遇到,这里集中说一下。
第一个坑是小文件问题。流式任务和频繁upsert会产生大量小文件,导致文件系统元数据膨胀、查询性能下降。Hudi可以开启自动文件大小控制,例如设置目标文件大小上限,并定期执行Clustering或者Compaction任务合并小文件。Iceberg则可以通过rewriteDataFiles周期性整理。很多人一开始只顾着写数据,忘了维护任务,三个月后才发现表上挂了几十万个小文件,查询慢到没法用。
第二个坑是跨引擎读写时的元数据可见性。多个引擎共享同一张湖表时,不是每次提交后其他引擎都能立刻看到新数据。像Trino这类引擎的元数据可能有一段时间的缓存,需要配置合理的刷新频率,或者在业务上错开大查询和写入高峰。否则会出现“明明任务跑成功了,报表却看不到新数据”的情况。
第三个坑是并发写冲突。Hudi和Iceberg对同一张表的多写者并发支持各有不同,Hudi早期版本并发写经常冲突,Iceberg依赖乐观并发重试机制。工程上的稳妥做法是:同一张湖表尽量只保留一个写入任务,如果必须要多任务写同一张表,优先确保分区和主键范围不重叠。
第四个坑是Schema变更对下游的影响。湖仓支持灵活的Schema演进,但这不等于可以随意改类型、删字段。新增可空字段比较安全,修改字段类型会影响历史快照,删除字段更是会破坏下游所有消费方。实际项目里最好在ODS层保留最原始的字符串或JSON字段,解析和类型转换放到DWD层做,这样上游变更时不至于崩到下游。
5.4 迁移这件事,最后我的一点个人体会
我个人在做这类迁移时有个坚持了多年的习惯:新老两套链路并行跑,至少两周起步,每天做数据对账。不是简单看一眼总数,而是按天、按业务维度、按关键指标交叉比对,比如订单明细条数、GMV汇总、主键重复率、空值率,这些都对上了再切流量。
这套方法看起来很笨,却是最可靠的。湖仓架构可以带来成本、时效、灵活性上的提升,但它本质上还是数据工程,分层逻辑不会消失,只是换了一种更轻量、更实时的实现方式。谁能在“逻辑分层”和“物理实现”之间找到平衡,谁就能真正吃到湖仓一体带来的红利。