4、离线链路基石:HDFS与Hive数仓分层设计(ODS/DWD/DWS/ADS)

聊完了实时链路,咱们得回头看看离线这边。说实话,离线数仓才是大多数公司的“压舱石”。实时再快,最终对账、报表、BI分析,还是得靠离线。而离线链路的基石,就是HDFS和Hive。

我个人习惯把数仓分层比作“厨房流水线”。原始食材(ODS)进来,经过清洗切配(DWD),做成半成品(DWS),最后端上桌(ADS)。每一层都有它的职责,乱不得。

核心原则:分层不是为了炫技,而是为了解耦和复用。你想想看,如果所有逻辑都堆在一张表里,改一个指标得重跑全量数据,那得多痛苦?

4.1 存储基石:HDFS 的选型与避坑

HDFS 是离线数仓的根。根不稳,上面全白搭。我在项目中遇到过几次因为 HDFS 小文件问题导致 NameNode 内存爆掉的故障,那叫一个惨烈。

这里有几个关键点,你得记住:

  • 块大小:默认 128MB,别乱改。除非你确定你的数据平均文件大小远超这个值,否则别动。
  • 副本数:默认 3 副本。如果集群机器紧张,可以降为 2,但别低于 2。我曾经为了省存储把副本改成 1,结果一台机器磁盘坏了,数据直接丢了一整天。
  • 小文件治理:这是个大坑。HDFS 不适合存大量小文件。每个小文件都会在 NameNode 内存里占一条记录(约 150 字节)。1000 万个文件,光元数据就得 1.5GB 内存。

我的小技巧:在 Hive 建表时,尽量用 ORC 或 Parquet 格式,配合 Snappy 压缩。既能省空间,又能提升查询速度。别再用 TextFile 了,除非你只是为了临时看数据。

4.2 Hive 数仓分层:ODS / DWD / DWS / ADS

好了,存储层搞定,咱们聊聊分层。这是数仓设计的灵魂。我见过很多团队上来就建表,完全不考虑分层,最后数据血缘乱成一锅粥。

标准的四层架构,每一层都有明确的定位:

分层 全称 核心职责 数据粒度 存储策略
ODS 操作数据层 原封不动接入业务数据 原始明细 增量/全量,保留原始格式
DWD 明细数据层 清洗、去重、标准化、维度退化 明细 ORC + Snappy,分区表
DWS 汇总数据层 按主题轻度汇总(天/小时) 轻度聚合 ORC + Snappy,分区表
ADS 应用数据层 面向业务报表、即席查询 高度聚合 ORC + Snappy,小表可存 MySQL

4.2.1 ODS 层:原样接入,不做任何加工

ODS 层说白了就是“垃圾桶”。业务库的数据怎么来,你就怎么存。别做任何清洗、转换。为什么?因为一旦你改了原始数据,出了问题你根本没法回溯。

我记得有一次,业务方说某个字段值不对。我直接去 ODS 层查原始日志,发现是业务系统本身写错了。如果我在 ODS 层做了清洗,这个锅就得我来背。

-- ODS 层建表示例
CREATE TABLE ods_user_event (
    user_id      STRING,
    event_type   STRING,
    event_time   STRING,
    event_data   STRING,  -- 原始 JSON 字符串
    dt           STRING   -- 分区字段
)
PARTITIONED BY (dt STRING)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\t'
STORED AS TEXTFILE;  -- 保留原始格式,方便排查

注意:ODS 层表结构必须与源系统严格一致。字段名、类型、分隔符都不能改。改了就是给自己挖坑。

4.2.2 DWD 层:清洗、标准化、维度退化

DWD 层是数仓最核心的一层。这里要做三件事:

  1. 清洗:去掉空值、脏数据、格式不对的记录。
  2. 标准化:统一时间格式、枚举值、编码。
  3. 维度退化:把常用的维度字段(如城市、渠道)直接拉平到事实表中,避免每次查询都去 Join 维表。

我见过最坑的情况是,DWD 层没做维度退化,结果下游每个报表都要 Join 十几张维表,跑一次要半小时。后来我把常用的几个维度直接退化到事实表里,查询时间直接降到 2 分钟。

-- DWD 层建表示例(ORC 格式 + 分区)
CREATE TABLE dwd_user_event (
    user_id      STRING,
    event_type   STRING,
    event_time   TIMESTAMP,
    city_id      STRING,   -- 退化维度
    channel      STRING,   -- 退化维度
    event_data   MAP<STRING,STRING>  -- 解析后的结构化数据
)
PARTITIONED BY (dt STRING)
STORED AS ORC
TBLPROPERTIES ('orc.compress'='SNAPPY');

4.2.3 DWS 层:按主题轻度汇总

DWS 层是“承上启下”的一层。它不做全量明细,而是按主题做轻度汇总。比如按天、按小时统计每个用户的 PV、UV、下单数。

为什么要做这一层?因为下游的报表、BI 工具,很少需要看单条明细。它们要的是聚合后的指标。如果每次都从 DWD 层全量扫描聚合,性能扛不住。

-- DWS 层建表示例(按天汇总用户行为)
CREATE TABLE dws_user_daily (
    user_id      STRING,
    pv_count     BIGINT,
    uv_count     BIGINT,
    order_count  BIGINT,
    total_amount DECIMAL(18,2),
    dt           STRING
)
PARTITIONED BY (dt STRING)
STORED AS ORC
TBLPROPERTIES ('orc.compress'='SNAPPY');

经验之谈:DWS 层的粒度不要太小。按天、按小时就够了。别搞什么按分钟汇总,那跟 DWD 层没区别,还浪费存储。

4.2.4 ADS 层:面向业务,按需定制

ADS 层是给业务方直接用的。它可以是宽表,也可以是报表。这一层的数据量通常很小,但查询频率很高。

我建议 ADS 层的表尽量做成“大宽表”。把业务方需要的所有字段都放在一张表里。别让他们自己去 Join。业务方不是数据工程师,他们只想看到结果。

-- ADS 层建表示例(业务宽表)
CREATE TABLE ads_user_portrait (
    user_id          STRING,
    total_orders     BIGINT,
    total_amount     DECIMAL(18,2),
    last_active_date STRING,
    favorite_category STRING,
    risk_level       STRING,
    dt               STRING
)
PARTITIONED BY (dt STRING)
STORED AS ORC
TBLPROPERTIES ('orc.compress'='SNAPPY');

4.3 数仓分层架构图

说了这么多,不如一张图来得直观。下面这张图展示了从 ODS 到 ADS 的完整数据流转过程。

离线数仓分层架构图 离线数仓分层架构图 数据源层:业务数据库、日志、埋点、第三方 API ODS 层:原始数据接入,原样存储(TextFile) DWD 层:清洗、去重、标准化、维度退化(ORC) DWS 层:按主题轻度汇总(天/小时粒度) ADS 层:业务宽表、报表、即席查询 每一层只与相邻层交互,禁止跨层访问。ODS 不能直接给 ADS 用。

4.4 分层设计的最佳实践与避坑

最后,分享几个我在实战中踩过的坑,希望能帮你少走弯路。

  • 不要跨层访问:ADS 层只能读 DWS 层,DWS 层只能读 DWD 层。ODS 层的数据不能直接给报表用。一旦破了这个规矩,数据血缘就乱了。
  • 分区策略要统一:所有层都按 dt 字段分区。这样回溯数据时,只需要改一个分区条件,不用到处改 SQL。
  • 命名规范要严格:表名必须带分层前缀(ods_、dwd_、dws_、ads_)。字段名统一用下划线命名法。别搞什么大小写混用,那是在给自己找麻烦。
  • 数据质量监控:每一层的数据产出后,都要做校验。比如 ODS 层的数据量不能为 0,DWD 层的去重率不能超过 5%。我曾经因为没做监控,导致 DWS 层的数据重复了三天才发现。

血的教训:千万别在 DWD 层做复杂的业务逻辑计算。DWD 层只做清洗和标准化。业务逻辑应该放在 DWS 层或 ADS 层。否则,一旦业务规则变了,你得重跑整个 DWD 层,那代价太大了。

嗯,离线数仓的分层设计,说白了就是“分而治之”。每一层做好自己的事,层与层之间通过分区和命名规范解耦。这样,无论业务怎么变,你都能快速响应。