订阅更新

从0设计大数据平台——在架构层面把倾斜风险降到最低

数据倾斜手册 第 20 题 约 14 分钟读完

本文目录
  1. 先说结论
  2. 第一层:数据采集层——统一编码、类型、格式
  3. 第二层:数据存储层——分区分桶设计
  4. 第三层:数据清洗层——NULL处理和数据标准化
  5. 第四层:计算引擎层——参数调优
  6. 第五层:数仓分层——减少直接查询明细数据
  7. 第六、七层:查询优化和监控预警
  8. 面试高频追问
  9. 实战:我的数据平台建设最佳实践
  10. 总结

先说结论

预防胜于治疗:在架构设计阶段把倾斜风险消灭,代价比运行时处理低100倍。一个表的分桶key设计,建表时多想5分钟,能省掉运行时几年的调优成本。

七层防倾斜框架:数据采集层(统一编码+类型)→ 数据存储层(合理分区分桶)→ 数据清洗层(NULL处理+数据标准化)→ 计算引擎层(参数调优+AQE)→ 数仓分层(DWD/DWS减少明细查询)→ 查询优化层(索引+物化视图)→ 监控预警层(全链路倾斜监控)。

没有银弹:再好的架构设计也无法完全消灭倾斜,业务增长带来的新热点、数据分布的变化都会引入新的倾斜。架构设计的目标是「降低倾斜发生的频率和处理的代价」,而不是「永远不发生倾斜」。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

这是数据倾斜系列的最后一篇,也是最综合的一篇。前19篇讲的是「如何处理倾斜」,这篇讲「如何设计让倾斜尽量不发生」。面试里有一类高阶题:「如果让你从0设计一个数据平台,你会怎么做?」能把倾斜的预防融入到整体架构设计里,是高级工程师和资深工程师最明显的区别。我结合自己在四家公司的数据平台建设经历,总结出了一套「倾斜预防七层框架」,覆盖从数据采集到查询的完整链路。

第一层:数据采集层——统一编码、类型、格式

倾斜的很多根源在数据入口就种下了。采集层做好以下几点,能消除大量下游倾斜风险:

# 采集层防倾斜的设计规范:
 
# 1. 统一字符编码:所有数据源统一UTF-8
# 明确要求上游系统输出UTF-8,对于历史系统提供转换脚本
# Kafka Producer配置:
# props.put('key.serializer', 'StringSerializer');  -- UTF-8序列化
 
# 2. 统一数字类型:避免String存数字
# 规范:所有ID类字段统一BIGINT,金额类统一DECIMAL
# 禁止:把BigInt的ID存成String(后续JOIN时有类型转换风险)
 
# 3. 统一NULL规范:明确NULL和空字符串的含义
# 规范:缺失值用NULL,空业务值用约定的默认值(如'UNKNOWN')
# 禁止:用''(空字符串)代替NULL(空字符串的哈希值固定,会导致集中)
 
# 4. 统一时间时区:所有时间戳统一UTC存储
# 展示时在前端做时区转换,不在数据层做
# 跨时区的数据倾斜(某个时区的用户活跃时间集中)通过UTC均摊到各时间段

第二层:数据存储层——分区分桶设计

建表时的分区和分桶设计,是防倾斜最重要的一层:

-- 防倾斜的建表设计模板(以事实表为例)
 
CREATE TABLE fact_ad_exposure (
  exposure_id  BIGINT NOT NULL,   -- 高基数自增ID,唯一标识
  ad_id        BIGINT NOT NULL,   -- 广告ID,统一BIGINT类型
  user_id      BIGINT,            -- 用户ID,可为NULL(未登录用户)
  channel_id   INT NOT NULL,      -- 渠道ID
  country_code STRING NOT NULL,   -- 国家代码
  exposure_ts  TIMESTAMP NOT NULL, -- 曝光时间(UTC)
  revenue      DECIMAL(18, 6)     -- 收入(统一DECIMAL,避免精度问题)
)
USING DELTA
PARTITIONED BY (dt STRING)          -- 按日期分区(粗粒度,控制文件数)
CLUSTERED BY (exposure_id) INTO 256 BUCKETS  -- 按高基数ID分桶(均匀)
TBLPROPERTIES (
  'delta.autoOptimize.optimizeWrite' = 'true',  -- 自动合并小文件
  'delta.autoOptimize.autoCompact' = 'true'     -- 自动Compaction
);
 
-- 设计决策说明:
-- 分区:按dt(日期),每天一个分区,查询时分区裁剪减少扫描量
-- 分桶:按exposure_id(高基数),确保每桶数据量均匀
-- 不按user_id分桶:user_id可能有热点(机器人账号等),倾斜风险高
-- 不按ad_id分桶:爆款广告的ad_id会热点,倾斜风险高

第三层:数据清洗层——NULL处理和数据标准化

DWD层是消除数据质量问题(包括倾斜隐患)的关键层:

-- DWD层的防倾斜清洗规范
 
INSERT INTO dwd_ad_exposure PARTITION(dt='2024-08-01')
SELECT
  exposure_id,
  ad_id,
  -- NULL处理:user_id为NULL的记录保留,但标记为-1(匿名用户标识)
  COALESCE(user_id, -1) AS user_id,  -- -1作为「匿名用户」的ID
  -- 数据标准化:统一大小写,去除空格
  UPPER(TRIM(country_code)) AS country_code,
  -- 异常值处理:收入为负数或超过阈值的记录标记并过滤
  CASE
    WHEN revenue < 0 OR revenue > 100000 THEN NULL  -- 异常值置NULL
    ELSE revenue
  END AS revenue,
  exposure_ts
FROM ods_ad_exposure
WHERE dt = '2024-08-01'
  AND exposure_id IS NOT NULL  -- 必须有唯一ID
  AND ad_id IS NOT NULL;       -- 必须有广告ID
 
-- 为什么把user_id=NULL改成-1?
-- 因为-1作为一个固定的「非NULL」值,哈希后会均匀分布到各分区
-- 而NULL的哈希值固定,所有NULL集中到一个分区(倾斜)
-- 注意:-1只是一个标记,不是真实用户ID,后续查询要区分处理

第四层:计算引擎层——参数调优

集群级别的参数配置,影响所有作业的倾斜处理能力:

# Spark集群全局参数配置(防倾斜相关)
 
# === AQE相关 ===
spark.sql.adaptive.enabled = true                        # 必须开
spark.sql.adaptive.coalescePartitions.enabled = true     # 小分区合并
spark.sql.adaptive.skewJoin.enabled = true               # 倾斜Join处理
spark.sql.adaptive.skewJoin.skewedPartitionFactor = 3   # 阈值调低,更积极
spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes = 134217728  # 128MB
 
# === Broadcast相关 ===
spark.sql.autoBroadcastJoinThreshold = 209715200  # 200MB
# 根据Executor内存调整,不要超过Executor内存的10%
 
# === Shuffle相关 ===
spark.sql.shuffle.partitions = 2000  # 大于默认200,减少单分区数据量
# 随数据量增大而增大,原则:每分区数据量在128MB-512MB之间
 
# === 内存相关(防止倾斜Task OOM) ===
spark.executor.memory = 16g  # 根据实际节点内存设置
spark.executor.memoryFraction = 0.6  # 60%用于执行
spark.memory.offHeap.enabled = true   # 开启堆外内存,缓解GC压力
spark.memory.offHeap.size = 4g

第五层:数仓分层——减少直接查询明细数据

分层架构是防倾斜的最根本手段:让高频查询尽量在预聚合的DWS层完成,不直接碰DWD明细:

层次职责倾斜风险设计原则
ODS原始数据,按来源原样存储高(原始数据可能有各种倾斜)只存,不聚合,不做任何清洗
DWD明细数据,清洗+标准化中(清洗后减少部分倾斜隐患)统一类型/编码,处理NULL,过滤无效数据
DWS汇总数据,预聚合结果低(数据量已大幅压缩)按高频查询维度预聚合,一张DWS表对应一类分析需求
ADS应用数据,最终结果极低(行数很少)直接从DWS取数,不写复杂聚合逻辑
-- DWS层的设计原则:按查询模式预聚合
-- 高频查询:按天+渠道+国家统计广告指标
-- → 建对应的DWS表:dws_ad_channel_country_daily
 
INSERT INTO dws_ad_channel_country_daily PARTITION(dt='2024-08-01')
SELECT
  ad_id,
  channel_id,
  country_code,
  SUM(revenue)  AS total_revenue,
  COUNT(*)      AS exposure_cnt,
  -- UV用Bitmap精确存储(支持后续跨天合并)
  bitmap_union(to_bitmap(user_id)) AS user_bitmap
FROM dwd_ad_exposure
WHERE dt = '2024-08-01'
  AND user_id != -1  -- 排除匿名用户(如业务需要)
GROUP BY ad_id, channel_id, country_code;
-- DWD:100亿行 → DWS:约500万行(压缩2000倍)
-- ADS层查询DWS:无倾斜,秒级完成

第六、七层:查询优化和监控预警

最后两层是运行时的兜底和持续治理:

-- 第六层:查询优化层
-- Doris物化视图(自动维护,透明路由)
CREATE MATERIALIZED VIEW mv_channel_daily
AS SELECT channel_id, SUM(total_revenue) AS revenue,
          SUM(exposure_cnt) AS exposure
FROM dws_ad_channel_country_daily
GROUP BY channel_id;
-- 查询dws时Doris自动路由到mv,进一步加速
 
-- Spark的索引(Delta Lake Z-Ordering)
OPTIMIZE delta.`/data/dws_ad_daily`
ZORDER BY (ad_id, channel_id);
-- 按ad_id和channel_id的Z-Ordering聚簇,
-- 查询时能跳过大量不相关的数据块
# 第七层:监控预警层(全链路)
# 覆盖前六层的核心指标:
 
# 采集层:各数据源的数据量趋势(检测异常增长)
# 存储层:各Tablet/Partition的数据量分布(检测写入倾斜)
# 清洗层:NULL率、类型转换错误率、行数丢失率
# 计算层:Spark UI的Task时间比、AQE触发次数
# 分层:DWS/ADS查询时间趋势(发现DWS查询变慢预警)
# 查询层:慢查询日志、全表扫描告警
 
# 告警阈值建议:
# Spark Task时间比 > 5倍:预警
# Spark Task时间比 > 10倍:严重告警
# Kafka Consumer Lag增速 > 1万/分钟:预警
# DWS查询时间 > SLA × 80%:预警
# DWD行数 vs ODS行数差异 > 5%:数据质量告警

面试高频追问

追问一:七层框架里,哪一层对防倾斜的效果最大?

第五层(数仓分层,DWS预聚合)效果最大,但它是结构性的,需要和业务深度对齐。第二层(分区分桶设计)是基础,设计错了后面很难纠正。如果只能做一件事,我会优先做好DWS层的设计——因为它从根源上减少了查询时需要处理的数据量,让倾斜的影响空间大幅缩小。DWS设计到位,很多原本需要复杂调优的查询,就自然消失了。

追问二:如果加入一个历史包袱很重、倾斜很严重的团队,你会怎么推进改善?

历史包袱的治理不能搞「大爆炸」(全部推翻重来),那样风险极高且推进不下去。我的策略:1)先止血——把最严重的倾斜(影响SLA的)用治标方案快速解决(加盐、过滤NULL),让团队能正常运转;2)建台账——梳理所有有倾斜风险的作业,按影响程度排优先级,做成治理Roadmap;3)从新建作业开始规范——新建的表和作业按规范设计,老作业利用版本迭代的机会逐步迁移;4)建监控——先建起来,让团队「看见」倾斜,有了数据才能推动改进;5)分享沉淀——把倾斜处理的经验整理成团队文档,让每个人都能自主处理常见倾斜,减少对少数人的依赖。这个策略我在某出海硬件品牌加入后用过,半年内把核心作业的SLA达成率从82%提升到97%。

追问三:你提到的七层框架,在小团队(5人以下)里能完整落地吗?

不能,也不应该全落地。七层框架是理想状态,小团队要按资源和优先级裁剪:必须做的(第一优先级):统一数据类型和编码(第一层,成本低收益大)、合理的分区设计(第二层,建表时的一次性决策)、Spark参数调优(第四层,一次配置长期受益)。有时间再做(第二优先级):DWD数据清洗标准化(第三层)、DWS预聚合(第五层)、基础的倾斜监控(第七层)。暂时不做:物化视图、Z-Ordering等高级优化(第六层),等数据量到了再说。小团队最重要的是「做对优先级最高的几件事」,而不是追求完整框架。

实战:我的数据平台建设最佳实践

结合四家公司的经历,总结出来的「建平台时最值得投入的几件事」:

# 优先级排序(ROI从高到低):
 
# 1. 建表规范文档(ROI极高,一次投入,长期受益)
#    明确:分区策略、分桶策略、数据类型规范、NULL处理规范
#    格式:Confluence或内部Wiki,新同学入职必读
 
# 2. 数据类型和编码的CI检查(自动化,低维护成本)
#    在代码提交时自动检查:
#    - 是否有String存数字的字段
#    - 是否有BOOLEAN作为JOIN key
#    - 是否有跨类型的JOIN条件
 
# 3. DWS层的核心指标预聚合(高频查询加速)
#    和业务方对齐TOP 20查询需求,为这20个查询建DWS表
#    这20个查询覆盖了80%的业务需求
 
# 4. Spark参数基线配置(一次设置,全局受益)
#    AQE开启、Broadcast阈值、shuffle.partitions
#    写成公司Spark配置模板,所有新作业继承
 
# 5. 作业性能基线监控(发现倾斜的必要条件)
#    记录每个作业的历史执行时间、Task分布
#    自动检测异常(执行时间超过历史均值2倍就告警)
-- 我在某出海硬件品牌建平台时的第一件事:
-- 整理所有现有表的分桶情况,发现问题
 
-- 检查哪些表没有分桶(潜在倾斜风险):
SHOW TABLES;
-- 逐表执行:
DESC FORMATTED table_name;
-- 查看 'Num Buckets' 字段,0或-1表示没有分桶
 
-- 检查分桶key是否合理(基数是否够高):
SELECT
  bucket_key,
  COUNT(DISTINCT bucket_key) AS cardinality,
  COUNT(*) AS total_rows,
  COUNT(*) / COUNT(DISTINCT bucket_key) AS avg_rows_per_key
FROM your_table
GROUP BY bucket_key;
-- avg_rows_per_key越小越好(说明key越分散)
-- 如果avg_rows_per_key > 10万,考虑换分桶key
 
-- 这个检查花了2天时间,发现了5张关键表的分桶设计有问题
-- 重建后,相关作业的执行时间平均降低了45%

总结

防倾斜最好的方式是在架构设计阶段就把它考虑进去。七层框架:采集层统一编码类型、存储层合理分区分桶、清洗层标准化数据、计算层参数调优和AQE、数仓层减少明细查询、查询层物化视图加速、监控层全链路预警。小团队按优先级裁剪,优先做建表规范、DWS预聚合、基础监控。没有银弹,倾斜会随着业务增长不断出现,好的架构设计能降低发生频率和处理代价。面试时能用「七层防倾斜框架」结构化地讲出整体设计思路,展示了从单点调优到体系化设计的跨越,这正是高级工程师和资深工程师的分水岭。

数据倾斜面试通关手册全20篇到此完结!接下来会出「总结篇」:20道题快速查阅表 + 面试备战指南,帮你在面试前快速刷一遍所有核心知识点。关注我,不要错过!

20篇看下来,你觉得哪道题对你帮助最大?还有哪个数据倾斜的场景没有覆盖到?评论区告诉我,我来针对性补充!

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器