ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零构建数据仓库:架构选型、维度建模与ETL实战

从零构建数据仓库:架构选型、维度建模与ETL实战 说实话数据仓库这个词在技术圈里已经被用得很泛了。有人把一张宽表叫数仓有人把BI报表的底层查询库叫数仓还有人把整个大数据平台都算作数据仓库的一部分。作为一名做过多年数据平台建设的一线工程师我想结合自己的经验聊聊从零开始构建一套大数据存储系统时真正需要弄明白的那些事。这篇文章会覆盖数仓的基础概念、架构选型、维度建模方法以及一个最小可用系统的完整搭建过程。适合正在入门数据方向的数据工程师、后端开发也包括业务侧想理解数据架构的同事。看完之后你至少能把数仓的整体轮廓搞清楚并且照着文中的步骤搭出第一版可跑通的数据仓库。1. 数据仓库到底解决什么问题1.1 业务数据库和大数据仓库的差别先理清楚一个最容易被混淆的概念业务库和数仓不是一回事。我自己刚转行时也犯过糊涂以为数据仓库就是把MySQL的数据导到Hive里放着其实这只是最表层的理解。业务数据库比如MySQL、PostgreSQL、Oracle本质上是为了支撑业务流程而存在的。用户在电商平台下单、支付、退款这些操作都要实时落库保证事务一致性所以这类系统叫OLTP在线事务处理。它们的特点是单条读写快表结构设计高度规范化一条订单拆成订单表、订单明细表、商品表、用户表多张表避免数据冗余。但分析场景和事务场景的需求完全相反。业务方问上个月各品类的GMV趋势怎么样这种查询往往要把半年的订单明细全部拿出来做聚合涉及多张表关联、大量的全表扫描。放在业务库上做不仅慢还会直接影响线上写入性能这就是数仓出现的根本原因。数据仓库OLAP在线分析处理不是为单条记录设计的而是围绕主题、面向分析场景设计的存储和计算系统。它会提前完成必要的清洗、转换、整合把数据组织成更适合聚合查询的结构。你可以把业务库想象成记账本每个操作都要精确记录数仓则更像是基于账本加工出的经营分析报表用来发现问题、辅助决策。1.2 数据量到了什么程度才需要数仓很多读者会问我的业务才几千张订单表有必要建数仓吗我的看法是数仓的引入不仅看数据量更要看分析需求复杂度和直接查询的痛感。当你的订单数据只有几千条时直接在MySQL里写复杂SQL没有问题索引建好基本秒出。但当数据量到了几千万甚至上亿条业务库的分页查询已经明显变慢数仓的价值就开始体现出来了。这时候即使你只用单机版的Doris或者ClickHouse也远比在MySQL上跑聚合舒服得多。还有更重要的一点离线数仓实现了计算和业务写入的隔离。你跑凌晨的批量任务不影响白天的业务高峰你做复杂的关联查询不会把线上用户下单的接口拖垮。这种物理隔离带来的稳定性是数据量还没爆发时不容易感知、但一旦出过事故就会刻骨铭心的。1.3 OLTP 与 OLAP 的核心差异用一张表来对比会很清楚对比项OLTP 业务库OLAP 数据仓库核心用途支撑业务流程支撑分析决策数据形态规范化、少冗余反范式化、可冗余典型操作单行插入/更新/删除批量扫描、聚合、关联响应要求毫秒级实时秒级到分钟级均可存储设计行式存储为主列式存储为主数据时间范围往往只保留近期长期历史数据并发特点高并发小事务低并发大查询理解这层差异后你就知道为什么业界常说业务库和数仓要分开。这不是技术洁癖而是从架构上规避两类负载之间的相互干扰。构建大数据存储系统的第一步是先在意识上把分析场景和事务场景切开。后续所有的选型和建模都是在这个前提下展开的。2. 技术选型先想清楚架构再动手2.1 数据仓库的整体分层架构很多入门教程一上来就让你装Hadoop、Hive却不说清楚为什么需要这些组件。我建议换个思路先不看具体工具而是理解数仓在架构上必须包含哪几部分。数据源层、数据接入层、存储计算层、调度层、元数据管理层这五块构成了一套离线数仓的最小骨架。数据源就是你的业务库、日志文件、第三方接口数据。接入层负责把数据从源头搬运到数仓中并对数据做初步校验。存储计算层是最核心的部分既要存数据也要提供计算能力把数据加工成分析可用的形态。调度层负责定时触发任务数仓里的数据几乎都是周期性更新的没有调度系统血缘再清晰的数据也无法保证准点产出。元数据管理层则记录了表结构、字段含义、数据血缘、任务依赖关系这部分在初期容易被忽略但等规模上来之后没有元数据的数仓基本等于黑盒。整个系统跑起来的流程大致是调度系统按时间触发同步任务把数据源的数据抽取到数仓贴源层然后由计算引擎执行ETL清洗、转换、加载一步步加工到明细层、汇总层最后分析人员或报表工具从汇总层取数。2.2 各层组件怎么选技术选型是入门者最纠结的事因为网上众说纷纭。我的建议是不要盲目追求工业级标准组合而是根据自己的实际规模来。存储计算层的主流选项大致有三类第一类是Hive数仓体系。Hive本身是构建在Hadoop之上的数据仓库工具它把SQL翻译成MapReduce或者Spark任务执行。优点是能处理PB级数据生态非常成熟缺点是查询延迟高不适合交互式分析。如果你的起步阶段没有海量数据直接用Hive全家桶会有一种杀鸡用牛刀的感觉。第二类是MPP数据库比如Doris、ClickHouse、StarRocks。这类系统天然支持列式存储和分布式查询性能比Hive强很多部署也轻量一个普通服务器集群就能跑起来。对入门和中小规模场景我现在的首选基本是Doris或ClickHouse它们既能存明细数据也能承担聚合查询一鱼两吃。第三类是云上的数仓服务。如果你所在的公司已经在用云厂商直接采购云数仓服务也是不错的选择。免运维、自动扩缩容入门成本最低但需要注意数据导出和迁移绑定的问题。数据接入层的选择取决于数据源类型。业务库的数据同步可以用DataX、Sqoop或者Flink CDC日志类的数据则走Flume、Logstash或者直接写入消息队列再消费。入门场景下我个人最推荐DataX因为它配置简单、单机部署、支持几乎所有主流数据源学习曲线非常平缓。调度层如果不追求复杂DAG只用Crontab或者系统的定时任务其实也行。但表数量多起来之后没有任务依赖管理和失败重试机制的调度会让人崩溃。DolphinScheduler和Airflow是当前用得较多的两个开源调度系统前者中文资料多、部署方便后者生态更强但上手门槛稍高。2.3 老生常谈的数仓分层数仓分层这件事所有文章都会讲但很多新手不理解我明明可以直接从业务库同步数据到报表层为什么还要分ODS、DWD、DWS、ADS这些层。这里说一个真实的场景。某天业务方要统计最近30天下单且有支付成功记录的用户数如果没有中间层每个分析师可能自己对原始数据写过滤条件。有人用支付表里存在记录来判断有人用订单表里支付状态字段来判断统计口径不一致出来的结果对不上开会互相扯皮。分层真正的价值不只是性能优化更是把公共的数据加工逻辑沉淀下来让所有人使用同一套经过校验的数据。ODS层操作数据存储是最原始的数据落地层和业务库保持同构基本不做清洗。DWD层数据明细层做清洗、去重、维度退化、统一口径把ODS层的数据加工成干净的明细事实。DWS层数据汇总层按主题做轻度汇总比如用户每日汇总、商品每日汇总。ADS层应用数据层面向具体报表需求通常是高度聚合的结果表。有了这四层一次分析需求出现时分析师直接从DWS或ADS层取数即可绝大多数情况下不再碰ODS层。数据血缘也是顺着这个链路逐层往下追哪层出问题就定位哪层排查效率高得多。2.4 元数据和数据血缘元数据经常被忽略但它决定了数仓的天花板。元数据包括表名、字段注释、分区说明、负责人、更新时间、上游依赖表等。很多团队的表建得很随意字段名用拼音缩写连注释都不写三个月后连写表的人都不知道这个字段什么意思这是非常危险的。个人经验是建表时务必写清楚comment统一命名规范至少要有一个地方能够查到这张表是做什么的、数据从哪里来、更新频率是多少。血缘关系则记录了每张表的数据流向当出现数据异常时顺着血缘才能快速定位是哪个上游环节出了问题。开源工具里Apache Atlas可以自动采集血缘但对小团队来说初期用文档维护也可以关键是必须有这个意识。3. 数据建模维度建模法3.1 事实表和维度表的区分建模是整个数仓建设的灵魂也是拉开普通搬运工和资深工程师差距的地方。目前实践最广泛的还是维度建模它的核心是区分事实表和维度表。事实表记录的是业务过程产生的度量值比如订单金额、销售量、点击次数它们通常是数值型的而且会不断累积增长。维度表则是描述业务过程的上下文环境比如用户维度、商品维度、时间维度它们提供谁、什么、何时、何地这类背景信息。举一个直白的例子一条订单记录是小明在2024年11月10日购买了3件红色T恤共支付119.7元。订单事实表存的是3件、119.7元这些可加总的数值用户、商品、日期则由维度表来描述。分析时要看红色T恤上周卖了多少本质就是把事实表按商品维度表的颜色属性做筛选后求和。事实表的设计要点是粒度要明确。一张事实表里的每行数据代表什么粒度的某个事件必须清晰定义。订单明细事实表的粒度就是订单中的一个商品行订单事实表的粒度是一个订单。混用粒度是建模中最容易犯的错比如同一张表里有的行是按订单聚合的有的行是按明细行存的聚合结果会对不上。3.2 星型模型还是雪花模型了解了事实表和维度表下一步就是确定它们的连接形态。星型模型下维度表直接与事实表关联一条路径就能找到维度属性。雪花模型则把维度表做进一步规范化拆分比如地区维度拆出省市县多张表维度之间形成层级。星型模型查询时关联次数少性能好理解和维护成本低。雪花模型存储上可能节省一点空间但查询要多做几次join在分布式系统中这意味着更多的shuffle和网络开销。现在的存储成本已经很低几乎没必要为了省那点空间去向雪花模型妥协。我的建议很直接默认用星型模型只有当某个维度的规范化能明显减少维护成本时才考虑雪花结构。维度退化是另一个常用技巧。所谓维度退化是指把一些本来需要关联才能拿到的维度属性直接冗余到事实表中。订单的渠道字段、所在城市字段这些信息如果都通过关联维度表获取每个查询都要多几次join。直接在事实表里加上这几个字段查询速度和易用性都会有明显提升代价是存储增加但这点成本完全值得。3.3 三种事实表类型维度建模中事实表还分为三类事务事实表、周期快照事实表、累积快照事实表。很多入门者只知道事务事实表对后两种了解不够结果设计完发现有些分析做不出来。事务事实表记录每个业务事件产生的一行数据比如每笔支付记录、每次点击。它的特点是能统计发生次数支持按任意维度切分但也意味着数据量最大。周期快照事实表以固定时间周期比如每天记录累积状态典型场景是账户余额快照表每天存一条全量余额信息。为什么要这么做如果你只存支付流水想算某天账户还有多少钱就必须把历史所有流水累加一遍数据量大时极慢而快照表把当天余额直接存下来查询时取那天的记录即可。累积快照事实表则用于跟踪一个流程从开始到结束的各个状态比如订单从创建、支付、发货到完成的全过程每条订单一行记录各个阶段的时间戳。三者的选择依据是业务需求。想知道今天卖了多少单用事务事实表想知道今天有多少库存、多少余额用周期快照想看平均发货时长用累积快照。设计数仓时不能只盯着某一个要按业务过程匹配。3.4 缓慢变化维维度表里的数据并不是一成不变的用户会改手机号商品会改类目这就是缓慢变化维SCD问题。处理策略主要有三种。策略一是覆盖直接更新维度表原记录不保留历史。实现最简单但历史事实关联到的最新维度属性会被改写历史分析口径会漂移。策略二是新增一行记录给旧记录标记过期时间新记录作为最新版本。这样能完整保留历史但维度表膨胀较快查询时还需要处理版本条件。策略三是添加新字段来记录变化前后值比如一个用户维度有原手机号和现手机号两个字段适合变化次数非常有限的场景。实际生产中绝大多数维度采用策略一就足够。比如用户收货地址变化如果分析不关注历史地址直接覆盖即可。只有那些对口径有严格要求的维度比如组织架构、产品类目才需要考虑策略二。建模不是越复杂越好过度设计会给自己挖坑我见过不少团队把每个维度都做成SCD2最后查询时忘加版本过滤条件数据翻倍之后才发现问题这类教训很常见。4. 实操搭建一个最小可用的数据仓库4.1 场景设定理论讲再多不动手总是虚的。我以一个典型的电商订单场景为例带你走一遍从建表到ETL的实现过程。场景假设如下业务库是MySQL包含订单表、订单明细表、商品表、用户表四张核心表目标是构建一个离线数仓产出每日商品销售汇总报表。这里我用Hive来演示数仓的ODS、DWD、DWS三层实现因为Hive的SQL语法最通用读者即使在Doris或Spark上也能平滑迁移。如果你的起步环境是Doris、ClickHouse把存储引擎和分桶字段改成对应语法即可建模逻辑完全通用。4.2 建表ODS层和DWD层的表结构设计先建ODS层贴源同步结构尽量和业务库保持一致。以订单明细表为例CREATE DATABASE IF NOT EXISTS dwd; CREATE TABLE IF NOT EXISTS ods_order_detail ( order_id BIGINT COMMENT 订单ID, detail_id BIGINT COMMENT 明细ID, user_id BIGINT COMMENT 用户ID, product_id BIGINT COMMENT 商品ID, product_name STRING COMMENT 商品名称, category_id BIGINT COMMENT 类目ID, price DECIMAL(10,2) COMMENT 成交单价, quantity INT COMMENT 购买数量, amount DECIMAL(10,2) COMMENT 成交金额, order_status STRING COMMENT 订单状态, create_time STRING COMMENT 下单时间, pay_time STRING COMMENT 支付时间 ) COMMENT 订单明细ODS层 PARTITIONED BY (dt STRING COMMENT 分区字段按天) STORED AS ORC;注意几点一是分区字段dt按天存储这是离线数仓最常用的策略便于按天增量更新、按天删除和回溯。二是明细表没有做维度拆分商品名称、类目ID等维度属性直接冗余进来这就是维度退化。查询时不用关联商品表就能看到商品名称效率大幅提升。三是ORC列式存储对分析场景友好压缩率高、扫描快。再建DWD层表。ODS层数据基本是原样拷贝真正加工发生在ODS到DWD这一步。DWD订单明细事实表大致如下CREATE TABLE IF NOT EXISTS dwd_order_detail ( order_id BIGINT COMMENT 订单ID, detail_id BIGINT COMMENT 明细ID, user_id BIGINT COMMENT 用户ID, product_id BIGINT COMMENT 商品ID, product_name STRING COMMENT 商品名称, category_id BIGINT COMMENT 类目ID, price DECIMAL(10,2) COMMENT 成交单价, quantity INT COMMENT 购买数量, amount DECIMAL(10,2) COMMENT 成交金额, order_status STRING COMMENT 订单状态, create_time STRING COMMENT 下单时间, pay_time STRING COMMENT 支付时间, order_date STRING COMMENT 下单日期 ) COMMENT 订单明细DWD层 PARTITIONED BY (dt STRING) STORED AS ORC;相比ODSDWD层会做这些事统一字段类型和格式比如日期字段统一成YYYY-MM-DD格式清洗明显异常的数据比如过滤掉金额为负的测试订单补充一些便于后续分析的冗余字段比如把下单时间截取成下单日期。这个层面的加工逻辑要沉淀下来所有下游都要引用DWD而不是各自去ODS层加工。4.3 ETL实现从ODS到DWDODS到DWD的ETL用一条INSERT OVERWRITE语句实现加上适当的清洗逻辑INSERT OVERWRITE TABLE dwd_order_detail PARTITION (dt ${bizdate}) SELECT order_id, detail_id, user_id, product_id, product_name, category_id, price, quantity, amount, order_status, create_time, pay_time, SUBSTR(create_time, 1, 10) AS order_date FROM ods_order_detail WHERE dt ${bizdate} AND order_id IS NOT NULL AND detail_id IS NOT NULL AND amount 0;${bizdate}是调度系统传入的日期参数每次都处理当天分区这就是增量处理模式。注意这里的幂等性即使同一个任务被重复执行多次结果也是一样的这是数仓任务最重要的特性之一后面的常见问题部分我会详细说。实际场景中ODS到DWD还会做去重。比如Kafka重放或者同步组件重复推送导致ODS出现重复记录就要在DWD的加工中按主键去重保留最新的那条。4.4 从DWD到DWS汇总层加工DWS层面向主题做轻度汇总。以每日商品销售汇总为例CREATE TABLE IF NOT EXISTS dws_product_sale_daily ( product_id BIGINT COMMENT 商品ID, product_name STRING COMMENT 商品名称, category_id BIGINT COMMENT 类目ID, order_cnt BIGINT COMMENT 订单数, item_cnt BIGINT COMMENT 销售件数, gmv DECIMAL(14,2) COMMENT 销售额 ) COMMENT 商品每日销售汇总 PARTITIONED BY (dt STRING) STORED AS ORC;加工逻辑是INSERT OVERWRITE TABLE dws_product_sale_daily PARTITION (dt ${bizdate}) SELECT product_id, MAX(product_name) AS product_name, MAX(category_id) AS category_id, COUNT(DISTINCT order_id) AS order_cnt, SUM(quantity) AS item_cnt, SUM(amount) AS gmv FROM dwd_order_detail WHERE dt ${bizdate} AND order_status NOT IN (CANCELLED, CLOSED) GROUP BY product_id;这里有一个业务口径的判断计算销售GMV时取消和关闭的订单要排除掉。这种口径一定要提前和业务方确认并在注释里写清楚否则后面发现数仓的数怎么跟业务部门对不上时又要回到这一层来查。数仓里很多差异问题根源不是SQL写错而是业务口径没有对齐。DWS层的数据已经能够满足大多数报表需求。更细粒度的ADS层往往是针对某一个具体应用做的定制加工这里不再展开。4.5 调度与任务依赖上面这几条SQL本身不难真正的工程难点在于如何让它们按正确的顺序每天自动运行。你要保证ODS层同步完成之后DWD层任务才能开始DWD完成之后DWS层任务才能开始。最简单的入门方案是DolphinScheduler。你可以在它的Web界面上创建一个工作流包含三个节点ods_sync数据同步、dwd_process加工DWD、dws_process加工DWS节点之间配置依赖关系再设置每天凌晨2点定时启动。这里有几个实操经验值得分享。一是调度时间和业务高峰期错开如果业务方每天8点就要看到昨天报表你至少要留出报表生成的时间余量二是每个任务都要配置失败重试和告警通知失败后默认重试2到3次间隔5到10分钟三是大表和小表的依赖关系要处理好上游表同步完成并不意味着切分完整更稳妥的做法是让同步任务结束前做一个数据量校验发现为空或异常则标记失败防止下游在错误数据上继续加工。5. 常见问题与排查技巧实录5.1 数据少了几条怎么办增量同步的边界问题增量同步实践中最隐蔽也最烦人的问题是边界数据缺失。假设你在凌晨1点同步昨天的业务数据如果同步工具按某个时间字段抽取而该字段的值恰好跨越了抽取时间戳就可能出现数据漏抽。这类问题的排查思路是先对比ODS层和业务库的记录数找到差异出现的业务表然后观察漏掉的数据分布往往能看出规律。根治方案通常有两种一是改用基于主键或自增ID的增量方式二是把同步时间点再往后延给晚到数据留出冗余时间。更彻底的做法是引入Flink CDC这类实时采集工具把数据先打进消息队列数仓再从队列消费但这是后话入门阶段先理解同步边界的风险所在。5.2 数据倾斜聚合任务卡了半小时数据倾斜是离线数仓里最常见也最头疼的问题我到现在都记得第一次被一个group by任务卡住半小时的感受。它的典型表现是大部分任务很快就跑完了但某个阶段长时间卡住。原因多是某个键的值分布极不均匀比如电商场景里一个大V主播的订单量比其他所有商品加起来还多或者null值被分到同一个reduce里。定位倾斜位置的常规做法是看任务进度卡住的阶段通常就是聚合或join的地方。缓解方案有几个空值处理上把null改成随机字符串分散到不同reduce大键拆分上把极大值筛选出来单独聚合再跟整体结果合并join倾斜则可以考虑将小表做成broadcast join避免大量shuffle。定期排查数据分布如果发现某些维度键增长异常及时在大促或者热点事件前做预处理这是经验堆积出来的感觉。5.3 小文件过多的隐患Hive体系里小文件过多是一个慢性病。每次ETL按天分区落地如果同步的数据源表众多且分区大小不稳就会产生大量远小于默认块大小的小文件。它们会让NameNode内存压力增大、查询时的文件扫描开销骤增、计算引擎的task数量爆炸。解决思路无非两条写入阶段合并或者事后合并。写入阶段可以开启Hive的合并输出参数让最终落地的文件数量受控。事后合并则是定期将分区内的小文件用INSERT OVERWRITE重写为大文件。我建议在数仓建设初期就同步制定文件治理规范比如Hive开启hive.merge.mapfiles和hive.merge.mapredfiles并设置合并阈值Doris、ClickHouse这类系统也有对应的compaction机制相对省心一些。5.4 字段变更怎么兼容业务侧不会等数仓稳定后再改字段上线三个月后产品经理告诉你订单表要加一个优惠券ID字段这是常态。同步组件对新增字段的处理能力参差不齐最常见的情况是源表加了字段同步任务没感知数仓ODS层缺少对应列下游加工任务直接报错或产出空值。应对措施是在接入层设计阶段就兼容字段变更。用DataX或Flink CDC时尽量让同步配置支持自动识别表结构变化同时要有数据质量监控对ODS层的核心表做字段级监控发现新增字段或字段类型变化就及时告警。实操中还有个土办法很有效同步时把源表的所有字段按名称映射不依赖序号这样即便源表中间加了字段也不会把数据串列。5.5 任务重跑与幂等性数仓任务难免会有跑挂的时候失败后修好数据需要重跑。如果任务不幂等重跑一次就会产生重复数据这种问题在DWS层表现特别明显晚到的调度天天给报表注入重复值。最保险的实践是所有写目标表的ETL都采用INSERT OVERWRITE而不是INSERT INTO。INSERT OVERWRITE会先清空对应分区再写入天然保证重跑的结果可预期。同时建议在任务中加入数据量波动校验比如今天的记录数相比上周同一天波动超过50%就自动触发告警防止因为上游数据漏跑导致产出假数据。把这种校验做进任务的收尾阶段能帮你在用户发现问题之前先发现问题。我个人在实际操作中的体会是数仓的建设最难的部分从来不是工具或者SQL而是业务理解。同一张订单表财务关心实付金额运营关心下单金额市场关心含优惠券的金额你的DWD层怎么统一这个口径出报表时怎么向各业务方解释这些每天都在考验人。建好数仓的关键不只是把ETL跑通而是把是不是你算的和为什么你这么算这两个问题回答清楚。入门阶段可以从一张表、一个场景开始先打通链路再逐步完善规范。数据仓库这条路没有终点它跟着业务一起生长这也正是它有意思的地方。
返回列表