1. 流量域在数仓分层里到底处于什么位置
流量域是整个企业级大数据项目里最“接地气”的一块。用户每一次点击、每一次页面停留、每一次搜索,都会在日志里留下痕迹,这些痕迹经过采集、清洗、加工,最终变成业务方每天盯着的核心看板。而流量域的DWD层和DWS层,恰好是这条数据链路里承上启下的关键环节——往上看,它要对接ADS层的报表和指标;往下看,它要消费ODS层的原始日志。
很多人做数仓项目时容易犯一个错误:把DWD和DWS当成两个孤立的阶段,先做完DWD再做DWS,中间缺少统一的建模思路。结果就是DWD层字段命名混乱,DWS层又要重新做一遍清洗逻辑,重复劳动不说,数据口径还容易对不上。我在实际项目里踩过这个坑,后来总结出一个原则:DWD层解决“一条日志怎么变成一行干净的事实记录”,DWS层解决“一个业务主题怎么聚合成可复用的指标宽表”。这两层的设计必须同步考虑,不能割裂。
流量域的数据来源通常是埋点日志,格式以JSON为主,字段多、嵌套深、脏数据比例高。一个中等规模的App,每天产生的原始日志动辄几十GB到上百GB,字段数量轻松超过两百个。如果DWD层不做合理的裁剪和规范化,下游DWS层就会变成一个巨大的“字段垃圾场”,查询性能急剧下降,维护成本飙升。
这篇文章我会围绕流量域DWD和DWS的开发全过程展开,从日志解析、字段标准化、维度补全,到主题聚合、指标计算、性能调优,把每个环节的实操细节和踩坑经验都讲清楚。适合正在做数仓项目、或者准备接手流量域开发的同学参考。不管你用的是Hive、Spark SQL还是Flink SQL,底层的建模思路是相通的。
2. 流量域DWD层开发:从原始日志到干净事实表
2.1 原始日志长什么样,为什么不能直接用
先来看看典型的埋点日志结构。一条用户行为日志通常包含公共字段和事件字段两部分。公共字段是所有事件都有的,比如设备ID、用户ID、会话ID、时间戳、App版本、操作系统、网络类型、页面ID等;事件字段则根据具体行为不同而变化,比如点击事件有“点击元素ID”,曝光事件有“曝光时长”,搜索事件有“搜索关键词”。
这些日志在ODS层通常以JSON字符串的形式存储,一个分区对应一天的数据。直接查ODS层的原始数据,你会发现几个问题:第一,字段嵌套层级深,取一个“页面名称”可能要写三四层get_json_object;第二,同一个含义的字段在不同事件里命名不一致,比如有的叫page_id,有的叫pageId,有的叫pg_id;第三,存在大量空值和异常值,比如时间戳为0、设备ID为空字符串、页面ID为null。
DWD层的核心任务就是解决这些问题,把原始日志变成一张字段命名统一、类型明确、脏数据已处理、维度已补全的明细事实表。这张表通常按天分区,保留最细粒度的一条行为一行记录。
2.2 日志解析的三种方案与选型逻辑
解析JSON日志,常见的有三种方案,我分别说说各自的适用场景和坑点。
第一种是get_json_object函数直接解析。这是最原始的方式,在Hive或Spark SQL里写一长串get_json_object嵌套调用。优点是上手快,不需要额外依赖;缺点是性能差,每调用一次都要解析一遍JSON字符串,字段多了之后SQL会变得极其冗长,可维护性几乎为零。我见过一个项目里DWD层的SQL写了八百多行,全是get_json_object,后来改一个字段要找半天。
第二种是自定义UDF解析。用Java或Scala写一个UDF,输入JSON字符串,输出一个结构体或者Map。这种方式性能比get_json_object好很多,因为只解析一次。但缺点是UDF的维护需要编译打包,每次改字段都要重新部署,灵活性差。而且UDF的异常处理要特别小心,遇到格式错误的JSON不能直接抛异常,否则整个任务会挂掉。
第三种是建外部表映射JSON。如果用的是Hive,可以用JsonSerDe建外部表,直接映射JSON文件;如果用的是Spark,可以用schema推断或者显式指定schema来读取。这种方式最优雅,字段定义清晰,查询性能也好。但前提是JSON格式要相对规整,如果日志里存在大量格式不一致的情况,schema推断容易出错。
我的建议是:如果日志格式稳定、字段结构清晰,优先用外部表映射;如果格式有波动但整体可控,用自定义UDF;get_json_object只适合临时排查问题,不要用在正式的数仓开发里。
2.3 字段标准化:命名统一和类型收敛
字段标准化是DWD层最容易被忽视但影响最深远的工作。我见过太多项目因为字段命名不统一,导致下游开发反复确认口径,效率极低。
命名统一的核心原则是:同一含义的字段在所有事件里必须用同一个名字。比如页面ID,不管原始日志里叫page_id、pageId还是pg_id,在DWD层统一叫page_id。设备ID统一叫device_id,用户ID统一叫user_id。这个映射关系需要和埋点团队确认清楚,最好维护一份字段字典文档。
类型收敛同样重要。原始日志里的时间戳可能是字符串、可能是数字、可能是毫秒也可能是秒。在DWD层要统一成标准格式,比如统一用bigint类型的毫秒时间戳,或者统一用string类型的yyyy-MM-dd HH:mm:ss格式。数值型字段要明确是int还是bigint,避免下游做聚合时出现溢出。
还有一个细节是空值处理。原始日志里的空值可能是null、空字符串、字符串“null”、“NULL”、“-”等多种形式。在DWD层要统一处理成真正的null,这样下游做count、sum的时候才不会把无效值算进去。
2.4 维度补全:什么时候做,怎么做
DWD层要不要做维度补全,这个问题在社区里一直有争议。一种观点认为DWD层应该保持最纯粹的明细,维度关联放到DWS层做;另一种观点认为DWD层就应该把常用维度补全,方便下游直接使用。
我的实践经验是:高频使用的、变化缓慢的维度,在DWD层补全;低频使用的、变化频繁的维度,放到DWS层或ADS层关联。比如页面名称、频道名称、App版本这些维度,变化频率低,几乎每条查询都要用,放在DWD层补全最划算。而像用户等级、用户标签这类变化频繁的维度,放在DWD层会导致每天都要重新关联,反而增加成本。
维度补全的方式通常是left join维度表。这里有个性能优化的点:如果维度表不大,可以用map join或者broadcast join,避免shuffle。如果维度表很大,就要考虑分区裁剪和索引优化。
2.5 DWD层建表规范与分区策略
DWD层的表通常按天分区,分区字段用dt,格式是yyyyMMdd。建表时建议用ORC或Parquet列式存储,压缩比高,查询性能好。如果下游有实时需求,可以考虑用Hudi或Iceberg做增量更新,但大多数离线场景用普通分区表就够了。
建表语句里要特别注意字段注释,每个字段都要写清楚含义和来源。我见过很多项目的DWD表字段注释是空的,过两个月连作者自己都忘了某个字段是干什么的。另外,表名建议用dwd_流量域_主题_粒度 的格式,比如dwd_traffic_page_view_di,一看就知道是流量域的页面浏览明细表,粒度是日增量。
3. DWS层开发:从明细到主题宽表的聚合逻辑
3.1 DWS层到底该建哪些表
DWS层的核心思路是按业务主题聚合,而不是按事件类型聚合。流量域最常见的主题包括:用户访问主题、页面访问主题、会话主题、搜索主题、渠道主题。每个主题对应一张或多张DWS表,表的粒度通常是“天+维度”,比如“天+用户”、“天+页面”、“天+会话”。
建哪些DWS表,取决于下游ADS层的需求。我的做法是先把ADS层所有报表和指标列出来,倒推需要哪些DWS表。比如报表需要“每日活跃用户数”,那DWS层就要有一张按天聚合的用户访问表;报表需要“页面跳出率”,那DWS层就要有一张按天+页面聚合的访问表。
这里有个常见的误区:有人会把DWS层建得非常宽,一张表里塞几百个字段,试图覆盖所有场景。结果就是表维护困难,每次加指标都要改表结构,而且查询时大部分字段用不上,浪费存储和计算资源。正确的做法是按主题拆分,每张表聚焦一个业务场景,字段控制在合理范围内。
3.2 用户访问主题的聚合细节
用户访问主题是最核心的DWS表之一,通常叫dws_traffic_user_visit_di,粒度是“天+用户”。核心字段包括:用户ID、设备ID、首次访问时间、最后访问时间、访问页面数、访问时长、访问深度、来源渠道等。
聚合逻辑上,有几个细节需要特别注意。访问时长的计算不能简单用最后访问时间减首次访问时间,因为用户可能在中间长时间无操作。通常的做法是设置一个会话超时阈值,比如30分钟,超过30分钟没有新事件就认为会话结束。这个阈值要根据业务特点调整,内容型App可以设长一点,工具型App可以设短一点。
访问深度的计算通常用页面浏览数(PV)来衡量,但要注意去重。同一个页面反复刷新不应该重复计算,需要用页面ID去重。另外,有些页面是弹窗或浮层,不应该计入访问深度,这需要在DWD层就做好标记。
来源渠道的判定是个难点。用户可能从多个渠道进入,比如先通过搜索进入,然后点击推荐位跳转。通常的做法是取首次访问的渠道作为来源渠道,或者按业务规则定义“有效渠道”。这个规则要和业务方确认清楚,不同口径算出来的渠道效果可能差很多。
3.3 页面访问主题的指标计算
页面访问主题的DWS表通常叫dws_traffic_page_visit_di,粒度是“天+页面”。核心指标包括:页面PV、页面UV、平均停留时长、跳出率、退出率、入口页面次数等。
跳出率的定义是:只访问了一个页面就离开的会话数除以总会话数。这个指标在DWS层计算时,需要先判断每个会话是否只访问了一个页面,然后再聚合。实现上可以用窗口函数或者先算会话粒度再汇总。
退出率的定义是:某个页面作为会话最后一个页面的次数除以该页面的总访问次数。这个指标反映的是页面的“劝退”能力,和跳出率不同,退出率高不一定说明页面不好,可能是流程的最后一个页面。
平均停留时长的计算要排除异常值。有些用户可能打开页面后手机锁屏了,停留时长显示几小时,这种数据要过滤掉。通常的做法是设置一个上限,比如超过30分钟的停留时长按30分钟算,或者直接剔除。
3.4 会话主题的切分与聚合
会话主题的DWS表粒度是“天+会话”,核心字段包括:会话ID、用户ID、会话开始时间、会话结束时间、会话时长、会话内页面数、会话内事件数、会话来源、会话是否转化等。
会话切分的逻辑是DWS层开发里最复杂的部分之一。基本规则是:同一用户,相邻两个事件时间差超过阈值(通常30分钟),就切分成两个会话。但实际场景里还有更多细节:跨天怎么处理?通常按自然天切分,跨天会话拆成两个;新用户第一次访问怎么算?通常以首次事件为会话开始;会话内的事件排序怎么保证?需要按时间戳排序,时间戳相同时按事件类型优先级排序。
会话聚合的SQL通常需要用到窗口函数,比如row_number()、lag()、sum() over()等。这里有个性能优化的点:如果数据量很大,窗口函数的开销会很高,可以考虑先用MapReduce或Spark的groupByKey做预处理,再在Hive里做聚合。
3.5 DWS层的存储与查询优化
DWS层的表通常比DWD层小很多,因为已经做了聚合。但查询频率更高,所以存储格式和索引设计要更讲究。建议用ORC格式,开启谓词下推和列裁剪。如果查询模式比较固定,可以考虑用物化视图或者预计算 cube。
分区策略上,DWS层通常也是按天分区,但可以考虑增加一层“月分区”或“周分区”来加速跨天查询。另外,对于维度字段,如果基数不大,可以用字典编码或者bitmap来压缩存储。
查询优化方面,DWS层的SQL要尽量避免全表扫描,尽量带上分区条件和维度过滤条件。如果下游ADS层需要频繁查询某些维度组合,可以在DWS层建对应的聚合表或者索引。
4. 开发过程中最容易踩的五个坑
4.1 时间戳单位不统一导致的数据错乱
这是我在多个项目里都遇到过的问题。原始日志里的时间戳有的是秒级,有的是毫秒级,有的甚至是微秒级。如果不做统一处理,DWD层的时间字段就会混乱,下游按时间聚合时会出现“同一条记录被分到不同天”的情况。
解决方案是在DWD层做一次时间戳归一化,统一转成毫秒级bigint。判断逻辑是:如果时间戳长度是10位,说明是秒级,乘以1000;如果是13位,说明是毫秒级,直接用;如果是16位,说明是微秒级,除以1000。这个逻辑要写在ETL脚本里,并且做好异常处理。
4.2 会话切分阈值设置不当
会话超时阈值设成多少,直接影响会话数和会话时长的计算结果。设得太短,会话数虚高,平均会话时长偏短;设得太长,会话数偏少,平均会话时长偏长。我见过一个项目把阈值设成5分钟,结果一个正常的浏览行为被切成了七八个会话,数据完全不可用。
通常的参考值是30分钟,但要根据业务特点调整。内容型产品可以设成30到60分钟,工具型产品可以设成10到15分钟。最好的做法是做一次A/B测试,看看不同阈值下指标的变化趋势,选择一个相对稳定的值。
4.3 维度关联时的数据倾斜
DWD层做维度补全时,如果某个维度的值分布极不均匀,比如某个热门页面的访问量占了总访问量的80%,就会导致数据倾斜。表现是某个reduce任务卡住不动,整体任务耗时远超预期。
解决方案有几种:一是对热点key做加盐处理,把一个大key拆成多个小key;二是用map join代替reduce join,把小维度表广播到每个节点;三是把热点数据单独拿出来处理,最后再union回去。具体用哪种,要看数据特征和集群资源。
4.4 字段命名冲突导致的覆盖
DWD层做多表关联时,如果两个表有同名字段,比如都叫“name”,关联后就会出现字段冲突,后出现的字段会覆盖前面的。这种问题很隐蔽,因为SQL不会报错,但结果字段的值可能不对。
避免的方法是:在关联时给每个字段加上表别名前缀,比如a.name as page_name,b.name as channel_name。或者在DWD层建表时就做好字段命名规范,不同含义的字段用不同的名字,从源头避免冲突。
4.5 分区过多导致的小文件问题
DWD层和DWS层如果按天分区,一年就是365个分区,每个分区里如果还有多个reduce输出的小文件,文件数量会爆炸。小文件过多会导致NameNode压力大,查询时打开文件的开销也高。
解决方案是:在ETL任务最后加一个合并小文件的步骤,用distribute by或者repartition来控制输出文件数量。通常每个分区控制在1到10个文件比较合适,具体看数据量。如果用的是Spark,可以在写入前用coalesce或repartition来调整分区数。
5. 性能调优的实战经验
5.1 SQL层面的优化技巧
DWD层和DWS层的SQL优化,核心是减少数据扫描量和shuffle量。具体来说有几个常用技巧。
分区裁剪是最基本的,查询时一定要带上分区条件,比如where dt='20240101'。如果查询跨多天,可以用where dt between '20240101' and '20240107',但要注意分区数量不要太多。
列裁剪也很重要,select的时候只取需要的字段,不要用select *。列式存储下,不查的列不会读取,能大幅减少IO。
谓词下推是指把过滤条件下推到数据扫描阶段,减少参与join和聚合的数据量。比如先过滤掉无效的设备ID,再做聚合。
join优化方面,小表join大表用map join,大表join大表要控制好分区和并行度。如果join key有倾斜,要做加盐处理。
5.2 资源参数的调整策略
Hive或Spark任务的资源参数,直接影响执行效率和稳定性。常见的参数包括:executor数量、每个executor的内存和CPU核数、并行度、shuffle分区数等。
我的经验是:先跑一个小规模测试,观察每个阶段的耗时和资源使用情况,再逐步调整。比如发现某个stage的shuffle数据量特别大,就增加shuffle分区数;发现某个executor频繁GC,就增加内存;发现任务并行度不够,就增加executor数量。
不要一上来就把参数设得很大,那样反而会浪费资源,甚至导致任务失败。另外,不同集群的配置不同,参数要因地制宜,不能照搬网上的模板。
5.3 数据倾斜的排查与解决
数据倾斜是数仓开发里最头疼的问题之一。排查的方法是:看任务日志里各个reduce的耗时,如果某个reduce耗时远超其他,基本就是倾斜了。或者看shuffle数据量,某个key的数据量特别大,也是倾斜。
解决数据倾斜,首先要定位是哪个key导致的。可以在SQL里加一个group by key的统计,看看哪些key的记录数特别多。然后根据key的特征选择解决方案:如果是热点key,加盐打散;如果是join导致的,改用map join或broadcast join;如果是聚合导致的,先做局部聚合再做全局聚合。
5.4 任务调度的依赖管理
DWD层和DWS层的任务通常有依赖关系,DWD层跑完才能跑DWS层。调度工具里要配置好依赖,避免上游没跑完下游就启动,导致数据不完整。
另外,要设置合理的重试策略和告警机制。如果某个任务失败了,要能自动重试,重试几次还失败就告警。告警要发给具体的人,不能发到一个没人看的群里。我见过一个项目因为告警没人处理,数据断了三天才发现,业务方直接投诉到老板那里。
6. 数据质量校验:别等业务方来投诉
6.1 DWD层的数据质量检查点
DWD层的数据质量检查,重点是完整性、一致性和准确性。完整性检查包括:分区是否有数据、记录数是否在合理范围内、关键字段是否有大量空值。一致性检查包括:字段类型是否匹配、枚举值是否在预期范围内、时间戳是否在合理区间。准确性检查包括:抽样对比原始日志和DWD表、验证关键字段的解析逻辑是否正确。
我通常会在ETL任务后面加一个质量检查步骤,用SQL跑几个校验规则,比如:记录数波动超过20%就告警、关键字段空值率超过5%就告警、时间戳超出当天范围就告警。这些规则要随着业务变化不断调整。
6.2 DWS层的指标口径校验
DWS层的质量检查,重点是指标口径的一致性。同一个指标在不同表里算出来的值应该一致,比如用户访问表里的“访问用户数”和页面访问表里按用户去重后的“访问用户数”应该相等。如果不相等,说明口径有问题,要排查。
另外,DWS层的指标要和业务方的预期做对比。比如业务方说昨天大概有100万活跃用户,DWS表算出来只有80万,那就要排查是数据丢了还是口径不对。这种对比最好做成自动化的,每天跑完任务自动对比,有异常就告警。
6.3 数据质量问题的排查链路
发现数据质量问题后,排查的链路通常是:先确认问题现象,比如哪个指标不对、偏差多少;然后定位问题范围,是单个分区还是所有分区、是单个字段还是所有字段;接着回溯上游,看DWD层的数据是否正常;最后定位到具体的ETL步骤,检查SQL逻辑或数据源。
这个排查过程要养成记录的习惯,每次问题的原因和解决方案都记下来,形成知识库。下次遇到类似问题就能快速定位,不用从头查起。
7. 写在最后的一些个人体会
做流量域的数仓开发,技术只是一部分,更重要的是对业务的理解。同样的点击日志,不同业务方关注的点完全不同:产品关注页面停留和转化,运营关注渠道来源和用户留存,技术关注性能和异常。DWD和DWS层的设计,要能支撑这些不同的视角,而不是只满足某一个需求。
另外,数仓开发不是一锤子买卖,需求会变、数据会变、业务会变。DWD和DWS层的表结构要留有一定的扩展性,字段命名和分层规范要提前定好,不然后期改起来成本极高。我在项目里坚持的一个原则是:宁可前期多花两天设计表结构,也不要后期花两周改字段。
最后说一个实操小技巧:DWD层和DWS层的ETL脚本,建议用版本管理工具管起来,每次变更都记录清楚改了什么、为什么改。这样出问题的时候可以快速回滚,也方便交接给其他人。我见过太多项目因为脚本没有版本管理,改出问题后找不到原来的版本,只能凭记忆恢复,风险极大。