OLAP引擎里也有倾斜——Doris、ClickHouse的倾斜和Spark有什么不同?
先说结论
MPP倾斜的本质:MPP引擎(Doris/StarRocks/ClickHouse)把数据分片存在多个节点,查询时各节点并行计算。如果数据分片不均匀(某个分片数据远多于其他),承担大分片的节点成为瓶颈,拖慢整个查询,这就是MPP倾斜。
和Spark倾斜的区别:Spark倾斜发生在Shuffle阶段(运行时动态分配),MPP倾斜发生在数据存储阶段(建表时分片策略决定)。Spark倾斜是动态的,每次查询可能不同;MPP倾斜是静态的,数据写入后就固定了,每次查询都一样慢。
处理时机不同:Spark倾斜靠查询时优化(加盐、Broadcast、AQE);MPP倾斜靠建表时优化(选对分桶key和分桶数),一旦数据写入,很难在查询时纠正,只能重建表。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
很多人谈数据倾斜只想到Spark和Hive,但OLAP引擎(Doris、ClickHouse、StarRocks、Hologres)同样存在倾斜问题,而且因为它们是MPP架构,倾斜的表现和处理方式和Spark有明显不同。我在某元宇宙社交应用用Doris,在某短剧出海公司用Hologres,都遇到过OLAP层面的倾斜。这篇把OLAP引擎的倾斜原理和处理方案梳理清楚,这是很多面试者的知识盲区,能说清楚的候选人凤毛麟角。
Doris的数据分布和倾斜
Doris用哈希分桶(Hash Bucketing)把数据分布到各BE节点。建表时指定DISTRIBUTED BY HASH(key)和BUCKETS N,同一个key的所有数据进同一个桶。如果key分布不均匀,就产生数据倾斜。
-- Doris建表时的分桶设计(关键决策点)
CREATE TABLE orders (
order_id BIGINT NOT NULL,
user_id BIGINT,
amount DECIMAL(10,2),
create_time DATETIME
)
DUPLICATE KEY(order_id) -- 数据模型:明细模型
DISTRIBUTED BY HASH(user_id) BUCKETS 32; -- 按user_id分32个桶
-- 如果user_id分布不均匀(如某些用户有海量订单),就会产生倾斜
-- 检查桶内数据分布是否均匀:
SHOW TABLET FROM orders;
-- 查看各Tablet的RowCount,如果差异超过5倍,说明有倾斜
-- 查看数据在BE节点的分布:
SELECT
BackendId,
COUNT(*) AS tablet_cnt,
SUM(RowCount) AS total_rows
FROM information_schema.tablets
WHERE TABLE_NAME = 'orders'
GROUP BY BackendId;
-- 各BE的total_rows应该接近,差异大 = BE级倾斜
Doris的倾斜有两个层次:Tablet级倾斜(某些Tablet数据特别多)和BE级倾斜(某些BE节点存储的数据特别多)。Tablet级倾斜影响查询性能;BE级倾斜既影响查询也影响存储节点的负载均衡。
Doris倾斜的处理:选对分桶key
Doris倾斜的根本解法是建表时选对分桶key,让数据尽量均匀分布:
| 分桶key选择 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 高基数列(如order_id) | 分布均匀,几乎不倾斜 | 按user_id查询时需全桶扫描 | 明细数据,随机访问多 |
| 业务JOIN/GROUP BY的key(如user_id) | JOIN/GROUP BY时Colocation Join可能生效 | 如有热点user,倾斜明显 | 用户维度分析为主 |
| 复合key(如user_id+dt) | 平衡均匀性和查询效率 | 桶数要足够多 | 既要均匀又要按用户查询 |
| 随机分桶(RANDOM) | 绝对均匀,无倾斜 | 无法利用Colocation Join | 数据量大且无明显查询pattern |
-- Doris 2.0+支持随机分桶(Auto Bucket),彻底避免分桶key选择的困境
CREATE TABLE orders_random (
order_id BIGINT NOT NULL,
user_id BIGINT,
amount DECIMAL(10,2)
)
DUPLICATE KEY(order_id)
DISTRIBUTED BY RANDOM BUCKETS AUTO; -- 随机分桶,自动决定桶数
-- 优点:数据绝对均匀,无倾斜
-- 缺点:Colocation Join无法生效(因为相同user_id可能在不同桶)
-- 什么是Colocation Join?
-- 两张表按相同key分相同桶数,相同key的数据在同一个BE节点
-- Doris查询时可以本地Join,不需要网络传输,极快
-- 但前提是两张表的分桶key和桶数完全相同
ClickHouse的分片倾斜
ClickHouse是列式存储数据库,分布式场景下把数据分片(Shard)存在不同节点,查询时各Shard并行计算,最后汇总。分片倾斜和Doris的分桶倾斜原理相同,但配置方式不同。
-- ClickHouse分布式表建表时指定分片key
-- 本地表:
CREATE TABLE orders_local (
order_id UInt64,
user_id UInt64,
amount Float64
) ENGINE = MergeTree()
ORDER BY (user_id, order_id);
-- 分布式表:用user_id做分片key(如果user_id有热点,会产生倾斜)
CREATE TABLE orders_distributed AS orders_local
ENGINE = Distributed(
'my_cluster', -- 集群名
'default', -- 数据库名
'orders_local', -- 本地表名
user_id -- 分片key:按user_id的哈希值分片
);
-- 检查各Shard的数据量:
SELECT
hostName() AS host,
COUNT(*) AS row_count
FROM orders_distributed
GROUP BY host;
-- 各host的row_count差异大 = 倾斜
-- 更均匀的分片key选择:用高基数列或使用rand()随机分片
-- rand()分片:绝对均匀但无法做Shard本地聚合
ENGINE = Distributed('my_cluster', 'default', 'orders_local', rand())
MPP倾斜 vs Spark倾斜的核心区别
| 维度 | Spark倾斜 | MPP(Doris/CH)倾斜 |
|---|---|---|
| 发生时机 | 查询运行时(Shuffle时) | 数据写入时(分桶/分片策略) |
| 可见性 | Spark UI的Task分布 | SHOW TABLET或系统表查询 |
| 影响范围 | 单次查询变慢 | 所有查询都慢(固定的) |
| 处理时机 | 查询时加盐/Broadcast/AQE | 建表时选对key,或重建表 |
| 紧急程度 | 可以先跑完再优化 | 数据写入后改动代价大 |
| 动态调整 | AQE可运行时调整 | 基本不能运行时调整 |
这个区别在面试里很容易被考到:「Doris的数据倾斜和Spark的倾斜有什么不同?」能说出「一个是存储层的静态倾斜,一个是计算层的动态倾斜」,就展示了对两类系统本质区别的理解。
面试高频追问
追问一:Doris的Colocation Join是什么?和倾斜有什么关系?
Colocation Join是Doris的一个重要优化:如果两张表的分桶key相同、桶数相同,且数据分布在同一组BE节点上(Colocation Group),那么这两张表的Join可以在BE本地完成,不需要网络数据传输,性能极好。但Colocation Join和倾斜是一对矛盾:为了Colocation,分桶key要选业务JOIN的key(如user_id);但user_id可能有热点,导致倾斜。实际中要根据业务特点权衡:如果user_id分布相对均匀,用user_id分桶同时享受Colocation;如果user_id极度不均匀,用order_id分桶避免倾斜但放弃Colocation。
追问二:Hologres(阿里云)的倾斜怎么处理?
Hologres是阿里云的实时数仓产品,底层也是MPP架构。我在某短剧出海公司大量使用Hologres。Hologres的分布列(Distribution Key)相当于Doris的分桶key,选择原则相同:高基数、查询常用的JOIN/GROUP BY key、避免NULL多的列。Hologres有一个特殊功能:PARTITION BY(分区)+DISTRIBUTE BY(分布)可以组合使用,先按时间分区减少扫描数据量,再按业务key分布保证均匀。Hologres还支持CLUSTERING KEY(聚簇索引),按聚簇key排序存储,范围查询效率极高。倾斜排查可以通过Hologres的系统视图hologres.hg_table_info查看各Shard的数据分布。
追问三:如果已经发现Doris建表时分桶key选错了,数据已经写进去了,怎么办?
这是最棘手的情况,有几个方案:1)创建新表(选对分桶key)→ INSERT INTO SELECT把数据从旧表迁到新表 → 切换查询入口。代价是写一遍全量数据,对大表可能要几小时甚至几天;2)使用Doris的ROLLUP(物化视图):在原表上创建一个按不同key聚合的Rollup,查询时Doris自动路由到Rollup,绕过原表的倾斜问题(但只适合聚合查询,不适合明细查询);3)短期应急:增加BE节点、调整查询并行度,让倾斜的节点有更多资源处理更多数据,但这只是缓解,不能根治。长期还是要重建表。
实战踩坑
我在某元宇宙社交应用使用Doris做实时数仓时,有一张「用户礼物流水表」,建表时用user_id做分桶key(当时考虑的是按用户查询方便)。上线3个月后,有一个按直播间统计礼物收益的查询越来越慢——从秒级到了30秒级。
-- 排查倾斜:查各Tablet的数据量
SELECT
TabletId,
RowCount,
DataSize
FROM information_schema.tablets
WHERE TABLE_NAME = 'gift_flow'
ORDER BY RowCount DESC
LIMIT 20;
-- 发现:TOP 3的Tablet行数是其他Tablet的800倍!
-- 原因:某头部主播的粉丝给他送礼,user_id集中在这几个粉丝账号
-- 按user_id分桶,这些粉丝的所有礼物数据都在同一个桶
-- 解决方案:创建新表,改用gift_id(高基数)分桶
CREATE TABLE gift_flow_v2 (
gift_id BIGINT NOT NULL, -- 礼物流水ID,高基数,均匀分布
user_id BIGINT,
live_id BIGINT,
gift_amount DECIMAL(10,2),
gift_time DATETIME
)
DUPLICATE KEY(gift_id)
DISTRIBUTED BY HASH(gift_id) BUCKETS 64; -- 改用gift_id分桶
-- 全量数据迁移(利用业务低峰期)
INSERT INTO gift_flow_v2
SELECT * FROM gift_flow;
-- 迁移后验证各Tablet数据均匀,查询从30秒降到1.2秒
教训:建表时分桶key的选择决定了数据的物理分布,一旦数据写入就很难改,代价极高。建表前要做充分的数据分析:目标key的基数是多少、分布是否均匀、有没有热点值。宁可在设计阶段多花1小时分析,也不要在运行3个月后重建表花3天。
总结
OLAP引擎的倾斜是存储层的静态倾斜,发生在数据写入阶段,影响所有查询且难以事后纠正。Doris靠分桶key、ClickHouse靠分片key控制数据分布,选对高基数且分布均匀的列是关键。和Spark的动态倾斜(可以AQE兜底)不同,MPP倾斜必须在建表设计阶段就想清楚。面试时能区分「存储层静态倾斜」和「计算层动态倾斜」,能讲出Doris分桶key的选择原则,就展示了OLAP引擎的工程经验。
下一篇聊一个非常实用的面试题:数据倾斜和小文件问题哪个更难搞?两者有没有关联?如何同时解决?
你们用过Doris或StarRocks吗?分桶key是怎么选的?有没有遇到过不得不重建表的情况?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长