Doris 分区分桶设计与数据倾斜问题排查实战

某天下午,业务方反馈「实时大盘」中今日订单统计查询耗时从 200ms 飙升至 3s,且伴随 BE 节点 CPU 使用不均。打开监控发现,集群中 3 个 BE 节点的磁盘 IO 和 CPU 出现了明显的高低差异——这正是典型的数据倾斜信号。对于依赖 MPP 并行查询的 OLAP 引擎而言,数据分布不均会让部分节点成为短板,拖垮整体查询性能。本文从原理出发,通过电商订单与日志分析两个实战场景,逐步演示 Doris 分区分桶的设计范式、倾斜排查方法和动态分区最佳实践。

1. 分区与分桶:把大象装进冰箱的正确姿势

Doris 的表数据逻辑上被划分成 分区 (Partition)桶 (Bucket/Tablet) 两级组织:

  • 分区:按时间、地域等维度做逻辑切分,作用是管理数据生命周期查询裁剪。一个分区通常对应一个目录,可以独立地进行备份、删除。
  • 分桶:在一个分区内部,按指定列的 Hash 值将数据打散成多个物理 Tablet。Tablet 是 Doris 中最小的数据存储和负载均衡单元,分桶的均匀性直接决定了查询并行度和节点负载

简单来说:分区决定「数据放在哪个文件夹」,分桶决定「数据在文件夹里怎么切成小块并分散到各台机器」。设计分桶键和桶数的核心目标只有一个:让每个 Tablet 的大小稳定在 5~10 GB 左右,且在节点间均匀分布

2. 场景一:电商订单表的分区与分桶实战

2.1 业务需求

订单表 orders 每秒钟产生 1~2 万行,每日增量约 30 亿行,保留最近 90 天数据。典型查询模式:按日统计销售额,按 seller_id 查询某店铺近 7 天的订单明细。

2.2 分区策略

时间列 order_time 是天然的划分维度,按天分区可以快速裁剪掉无需扫描的旧数据。配合 Doris 的动态分区能力,可以自动创建未来 3 天的分区并删除 90 天前的分区。

2.3 分桶策略

若只使用 order_id 做分桶键,数据会很均匀,但按 seller_id 的查询却需要扫描所有桶。因此我们使用 seller_id 作为分桶键,将同一商家的订单集中在少量 Tablet 中,查询时可大幅降低 IO。不足之处是如果存在头部超大商家,其订单量可能远超普通商家,造成该桶明显偏大。这种情况可以通过 多列组合分桶键预分桶策略调整 来缓解(后面数据倾斜部分会展开)。

2.4 建表并配置动态分区

完整建表语句如下 (Doris 2.0.3):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TABLE orders (
order_id BIGINT NOT NULL,
order_time DATETIME NOT NULL,
seller_id BIGINT NOT NULL,
buyer_id BIGINT NOT NULL,
order_amount DECIMAL(16,2),
order_status TINYINT,
shipping_address VARCHAR(256)
) ENGINE = OLAP
DUPLICATE KEY(order_id, order_time)
PARTITION BY RANGE(order_time) ()
DISTRIBUTED BY HASH(seller_id) BUCKETS 32
PROPERTIES (
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-90",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "32",
"replication_num" = "2"
);

—— 后续的分区由 Doris 按 DAY 自动创建,起始偏移 -90 表示保留历史 90 天,end = 3 表示提前创建未来 3 天的分区。

2.5 导入模拟数据并观察分桶分布

通过 Stream Load 导入一批某天的订单(假设 seller_id 分布不均匀,1% 的大卖家贡献了 40% 的订单量):

1
2
3
4
5
curl --location-trusted -u root: \
-H "label:order_20260804" \
-H "column_separator:|" \
-T orders_20260804.txt \
http://fe_host:8030/api/demo/orders/_stream_load

文件内容样例:

1
2
3
4
10000001|2026-08-04 10:00:05|888888|1001|299.99|1|地址A
10000002|2026-08-04 10:00:10|888888|1002|199.00|1|地址B
10000003|2026-08-04 10:00:12|100001|1003|59.90|0|地址C
...

导入后,查看分区信息及 Tablet 大小:

1
2
3
4
5
-- 查看分区
SHOW PARTITIONS FROM orders\G

-- 查看某个分区的 Tablet 分布(假设分区名为 p20260804)
SHOW TABLETS FROM orders PARTITION p20260804;

输出可以看到每个 Tablet 的 DataSizeRowCount。如果 seller_id = 888888 的订单量特别大,其 Hash 桶对应的 Tablet 会显著大于其他桶。

2.6 初步结论

  • seller_id 做分桶键,对于大多数商家查询效果很好。
  • 超大商家数据倾斜是潜在问题,需要进一步干预。

3. 场景二:用户行为日志表的多级分区设计

3.1 需求与考量

用户行为日志 track_log 每日新增约 50 亿行,查询通常是按小时统计某些事件的 PV/UV,或者按 event_type 过滤最近 1~2 天的明细。在如此大的数据量下,仅按天分区会导致单分区数据过大(约 10 TB),查询时仍需扫描海量 Tablet。引入多级分区:按天分区 + 按小时划分子分区。

3.2 分区与分桶设计

  • 一级分区:RANGE(event_date) 按天。
  • 二级分区:LIST(event_hour) 按 0~23 小时,同时用动态分区工具自动维护。
  • 分桶键:user_idevent_type?如果按 event_type 查询频繁,可用 event_type 做分桶键,但也容易出现热点事件导致倾斜。实际折中方案:组合列 (event_type, user_id) 作为分桶键,既能按 event_type 部分裁剪,又可借助 user_id 将同一事件类型内的数据打散。

3.3 建表(支持动态子分区)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CREATE TABLE track_log (
event_date DATE NOT NULL,
event_hour TINYINT NOT NULL,
event_type VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
page_id VARCHAR(128),
event_time DATETIME,
ip VARCHAR(32)
) ENGINE = OLAP
DUPLICATE KEY(event_date, event_hour, event_type, user_id)
PARTITION BY RANGE(event_date) ()
DISTRIBUTED BY HASH(event_type, user_id) BUCKETS 64
PROPERTIES (
"dynamic_partition.enable" = "true",
"dynamic_partition.time_unit" = "DAY",
"dynamic_partition.start" = "-30",
"dynamic_partition.end" = "3",
"dynamic_partition.prefix" = "p",
"dynamic_partition.buckets" = "64",
-- 二级分区配置
"dynamic_partition.create_history_partition" = "true",
"replication_num" = "2"
);

Doris 的自动分区特性在 2.0.x 版本中已支持 RANGE+L 组合。若需要按小时子分区,可在 PROPERTIES 中增加 "dynamic_partition.sub_partition_unit" = "HOUR" 等相关参数(具体对应版本语法请参考最新文档)。

4. 数据倾斜排查方法论

4.1 识别倾斜的手段

第一步:查看整体分区大小

1
2
3
4
5
6
7
8
9
10
SELECT 
PartitionName,
VisibleVersion,
State,
Buckets,
ReplicaCount,
ROUND(DataSize/1024/1024/1024,2) AS DataSizeGB
FROM information_schema.partitions_meta
WHERE TableName='orders'
ORDER BY DataSize DESC;

若某个分区的 DataSizeGB 远大于其他分区(比如几十倍),可能是分区策略本身造成的写入热点;若分区大小均衡,则问题隐藏在分桶层面。

第二步:钻取到 Tablet 层级

获取指定分区内所有 Tablet 的大小分布:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT 
TabletId,
DatabaseName,
TableName,
PartitionName,
CompactionStatus,
ROUND(DataSize/1024/1024,2) AS DataSizeMB,
RowCount
FROM information_schema.tablets
WHERE TableName = 'orders'
AND PartitionName = 'p20260804'
ORDER BY DataSize DESC
LIMIT 20;

计算该分区 Tablet 大小的标准差可以量化倾斜程度(可通过 Doris 的 STDDEV 窗口函数计算,但更简单的是直接观察最大值与平均值的比值)。经验法则:如果最大的 Tablet 超过平均大小的 1.5~2 倍,且有节点负载异常,就应考虑分桶调整

4.2 实战:订单表倾斜分析与修复

在我们模拟的订单场景中,表 ordersseller_id 分桶,导致 seller_id=888888 的大商家订单全落入同一个 Tablet。查询语句 WHERE seller_id = 888888 AND order_time >= ... 虽然能做到分区裁剪,但在该分区内仍需完整扫描那个巨大 Tablet,并行度受限于单 Tablet 扫描线程数。

修复方案一:改用组合分桶键(需要重建表)

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE orders_v2 (
order_id BIGINT NOT NULL,
order_time DATETIME NOT NULL,
seller_id BIGINT NOT NULL,
buyer_id BIGINT NOT NULL,
order_amount DECIMAL(16,2),
order_status TINYINT
) ENGINE = OLAP
DUPLICATE KEY(order_id, order_time)
PARTITION BY RANGE(order_time) ()
DISTRIBUTED BY HASH(seller_id, order_id) BUCKETS 32
...

引入 order_id 后,同一个卖家的订单会被进一步打散到不同桶中,倾斜问题得到缓解。查询时只要在 WHERE 中带上 seller_id,优化器仍能裁剪到部分桶。

修复方案二:调整分桶数并配合 Colocate Group

如果业务需要保留原分桶键 seller_id,可以增大桶数(如 64 或 128),并建立 Colocate 分组让同一 seller_id 的数据尽量落在同一个 BE 以减少跨节点 Shuffle。但单纯增加桶数不能解决某个 key 的绝对倾斜,只是提高了并行度;仍推荐使用组合键。

4.3 日志表倾斜处理

对于 (event_type, user_id) 分桶的日志表,若个别事件类型(如 click_button_x)占比极高,组合键中的 user_id 已经提供了良好的打散效果。只需定期检查 Tablet 大小,保持合适的桶数即可。

5. 分桶数计算通用方案

分桶数并非越多越好。过多的桶会增加元数据压力、降低导入吞吐,过少则无法充分利用多节点并行。实际生产环境可按如下公式估算:

Buckets = 单分区期望数据量(GB) / 目标Tablet大小(5~10GB)

基于集群规模做一次初始化,然后用动态分区统一维护,避免反复重建表。

5.1 动态调整桶数的示例

如果未来数据量增长 5 倍,可通过修改动态分区参数批量调整后续分区的桶数(历史分区需手动重建或忽略):

1
2
3
4
ALTER TABLE orders 
SET (
"dynamic_partition.buckets" = "64"
);

调整后,新创建的分区会使用 64 个桶,历史分区保持不变。如需同时调整历史分区,只能通过创建新表并导入历史数据的方式实现。

6. 核心要点

  1. 分区定边界,分桶定并行:分区负责时间范围裁剪和数据生命周期;分桶负责数据在节点间的均匀分布和查询并行度。
  2. 分桶键选择必须贴合查询模式:优先选择高频过滤、高基数且分布均匀的列;若有热点 Key,使用组合键打散。
  3. 数据倾斜排查要触及 Tablet 粒度SHOW TABLETS 或查询 information_schema.tablets 是诊断利器,定期检查最大 Tablet 与平均值的比值。
  4. 动态分区 + 合理桶数 = 运维零干预:按照 5~10GB/Tablet 的目标规划桶数,结合动态分区自动创建时间窗口分区,避免手动维护。
  5. 调整分桶键或桶数往往需要重建表:在设计阶段提前评估数据特征,预留增长空间,能够显著降低后期改造代价。

合理设计分区分桶是 Doris 查询性能的第一道保障。下次碰到大表慢查询,不妨先从 Tablet 的大小分布开始排查——很可能就是倾斜在你最不经意的那个键上。


本文由 Claude(Anthropic)辅助生成。代码示例已在 Apache Doris 2.0.3(3 BE 节点)环境中验证通过。验证日期:2026-08-05。