Doris Compaction 原理与查询性能优化实战

凌晨两点,监控群里突然弹出告警:user_behavior 表的查询 P99 延迟从 45ms 飙到 800ms。第一反应是“谁又在上线?”但检查后发现是夜间高频导入任务在持续写入。深入排查 BE 节点后,发现 12 张 Tablet 的 CumulativeCompactionScore 全部超过 500,磁盘上堆积了上千个小 Rowset。

这篇文章不会从“什么是 Doris”开始,我会直接复盘这次 Compaction 积压的排查和优化过程,并给出可运行、可验证的实验步骤。

1. 问题表现:查询为什么突然变慢

Doris 的写入路径很清晰:

1
2
3
写入 -> MemTable -> 刷盘 -> Rowset(小文件)
\-> Rowset
\-> Rowset ...

每一批导入都会在磁盘上生成一个或多个 Rowset。如果导入频率高、批次小,就会产生大量“小 Rowset”。查询时需要扫描所有这些文件,并在内存中按主键合并、聚合,扫描成本随文件数量线性增加。这就是我们夜里看到的现象:导入任务没有报错,但查询延迟突然恶化。

在开始调优前,你需要先确认是 Compaction 积压。否则盲目改参数可能越改越糟。

2. Doris Compaction 核心机制

Doris 的 Compaction 本质是后台合并 Rowset 的任务。它把多个小 Rowset 合并成一个更大的 Rowset,减少扫描文件数,同时清理过期版本和重复主键。

2.1 主要 Compaction 类型

类型 作用 触发条件 影响
Cumulative Compaction 合并多个 Cumulative 层 Rowset 为一个更大的 Cumulative Rowset 积攒一定数量小 Rowset 减少小文件,写放大可控
Base Compaction 将 Cumulative 层合并到 Base 层 Rowset Cumulative 层过大或版本时间超过阈值 减少层数,但写放大较大
Vertical Compaction 按列切分合并,降低内存峰值 参数开启且 Rowset 列数多、数据量大 降低内存,但增加临时文件
Segment Compaction 在 Segment 内部合并,减少 Segment 内文件数 高频小批量导入时自动触发 对小导入很有效

2.2 写放大与查询延迟的权衡

Compaction 不是免费的。频繁 Compaction 会消耗磁盘 IO 和 CPU,带来“写放大”——一个字节可能被多次读写成多个 Rowset。但如果 Compaction 积压,查询延迟又会升高。我们的目标就是找到一个平衡点:

1
2
3
4
5
6
写放大 ↑  <--->  查询延迟 ↓
\ /
\ /
\ /
\ /
平衡区间

在 UNIQUE KEY 模型且开启 Merge-on-Write 后,Cumulative Compaction 可以快速合并重复键,减少查询时 Merge 的开销。这也是我们这次案例中关键的一步。

3. 实验环境准备

下面所有示例运行在以下环境:

  • Doris 2.1.3 FE + BE,3 个 BE 节点,replication_num = 1
  • Linux x86_64,每个 BE 挂载一块独立数据盘
  • 所有 SQL 通过 mysql -h FE_HOST -P 9030 -u root 执行

3.1 建表并模拟高频写入

先创建一个常用的用户行为表,使用 UNIQUE KEY 模型,开启 Merge-on-Write:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE DATABASE IF NOT EXISTS compaction_demo;
USE compaction_demo;

CREATE TABLE IF NOT EXISTS user_behavior (
user_id INT,
event_type VARCHAR(20),
event_time DATETIME,
cnt BIGINT SUM
)
UNIQUE KEY(user_id, event_type, event_time)
DISTRIBUTED BY HASH(user_id) BUCKETS 10
PROPERTIES (
"replication_num" = "1",
"enable_unique_key_merge_on_write" = "true",
"compaction_policy" = "size_based"
);

enable_unique_key_merge_on_write = true 会让重复主键在写入时直接合并,减小 Compaction 的压力。compaction_policy = size_based 让 Compaction 优先合并大小合适的 Rowset。

接下来模拟线上高频小批量导入:每 30 秒插入 1000 行。你可以用 shell 循环执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
# 模拟高频导入,产生大量小 Rowset
for i in $(seq 1 50); do
mysql -h FE_HOST -P 9030 -u root <<EOF
USE compaction_demo;
INSERT INTO user_behavior
SELECT
(number % 10000) as user_id,
CASE WHEN number % 2 = 0 THEN 'click' ELSE 'view' END as event_type,
DATE_ADD('2026-08-01 00:00:00', INTERVAL number SECOND) as event_time,
1 as cnt
FROM numbers("number" = "1000");
EOF
sleep 30
done

执行一段时间后,Doris 会积攒大量只包含几十 KB 数据的 Rowset。

3.2 查看 Compaction 积压状态

先找到这张表对应的 Tablet:

1
SHOW TABLETS FROM user_behavior\G

输出类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
*************************** 1. row ***************************
TabletId: 10001
ReplicaId: 20001
BackendId: 30001
Version: 120
VersionHash: 0
LstSuccessVersion: 120
LstFailedVersion: -1
LstFailedTime: NULL
SchemaHash: 12345678
DataSize: 2.03 MB
RowCount: 49600
State: NORMAL
LstConsistencyCheckTime: 2026-08-16 00:00:00
CheckVersion: 120
CheckVersionHash: 0
VersionCount: 350
PathHash: 892813
MetaUrl: http://BE_HOST:8040/api/meta/header?tablet_id=10001
CompactionStatus: http://BE_HOST:8040/api/compaction/show?tablet_id=10001

VersionCount: 350 说明这个 Tablet 上有 350 个 Rowset 版本。正常情况下这个值应该小于 50。

再通过 BE 的 HTTP API 查看 Compaction 详细队列:

1
curl -s "http://BE_HOST:8040/api/compaction/show?tablet_id=10001"

输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"cumulative_compaction": {
"task": {
"tablet_id": 10001,
"compaction_type": "cumulative",
"cumulative_score": 530,
"base_score": 12,
"input_rowsets": [ "10001_001_001", "10001_002_002", ... ],
"output_version": 121,
"status": "NO_TASK"
}
},
"base_compaction": {
"task": {
"tablet_id": 10001,
"compaction_type": "base",
"base_score": 12,
"status": "NO_TASK"
}
}
}

cumulative_score 高达 530,远超触发阈值(通常默认 10)。这意味着后台 Compaction 已经严重跟不上写入速度。

4. 参数调优实战

4.1 理解关键参数

Doris 的 Compaction 行为由 BE 配置控制。下面是我们这次用到的几个核心参数:

参数 默认值 作用
cumulative_compaction_num_singleton_deltas 5 累积多少个小 Rowset 后触发一次 Cumulative Compaction
max_cumulative_compaction_num_singleton_deltas 10 单次 Cumulative Compaction 最多合并多少个小 Rowset
compaction_task_num_per_disk 1 每个磁盘同时运行的 Compaction 任务数
enable_vertical_compaction false 是否启用 Vertical Compaction,降低内存峰值
cumulative_size_based_compaction_promotion_min_size 64MB Cumulative 层 Rowset 小于该值时才会继续合并,超过则提升到 Base 层

对于高频小批量导入,cumulative_compaction_num_singleton_deltas 过小会导致合并过于频繁,过大又会积压。我们当时的配置是默认值,但磁盘 IO 充足,可以调大 compaction_task_num_per_disk 并适当降低触发阈值。

4.2 修改 BE 配置并滚动重启

先备份原配置:

1
2
cd /path/to/doris-be/conf
cp be.conf be.conf.bak.$(date +%Y%m%d)

编辑 be.conf,修改或新增以下项:

1
2
3
4
5
6
7
8
9
10
11
# 每 5 个小 Rowset 就触发一次 Cumulative Compaction
cumulative_compaction_num_singleton_deltas = 5

# 单次最多合并 20 个小 Rowset,减少合并次数
max_cumulative_compaction_num_singleton_deltas = 20

# 每个磁盘允许 4 个并发的 Compaction 任务(前提是磁盘 IO 和 CPU 充足)
compaction_task_num_per_disk = 4

# 开启 Vertical Compaction,降低内存峰值
enable_vertical_compaction = true

然后逐个重启 BE 节点。注意不要同时重启所有 BE,否则会导致查询失败:

1
2
3
4
# 在需要重启的 BE 节点上执行
bin/stop_be.sh
bin/start_be.sh --daemon
tail -f log/be.INFO # 确认启动成功

等待所有 BE 滚动重启完成后,观察 Compaction 状态。

4.3 验证调优效果

再次查看同一个 Tablet 的 Compaction 状态:

1
curl -s "http://BE_HOST:8040/api/compaction/show?tablet_id=10001"

此时 cumulative_score 应该已经降到 20 以下,VersionCount 也会明显下降。你可以持续观察几分钟,看积压是否被消化。

查询性能的验证更直接:

1
2
3
4
5
6
SELECT user_id, event_type, COUNT(*) 
FROM user_behavior
WHERE event_time BETWEEN '2026-08-01 00:00:00' AND '2026-08-01 01:00:00'
GROUP BY user_id, event_type
ORDER BY COUNT(*) DESC
LIMIT 10;

在生产环境中,该查询的 P99 从 800ms 回落到 65ms。

5. 案例复盘:一次完整的积压处理流程

下面是当天夜里的时间线:

1
2
3
4
5
6
7
8
9
10
11
02:10  告警:查询 P99 800ms
02:15 SHOW TABLETS 发现 VersionCount 过高
02:20 curl Compaction API 确认 cumulative_score > 500
02:30 确认磁盘 IO 使用率仅 25%,CPU 空闲
02:35 修改 be.conf:compaction_task_num_per_disk=4,
cumulative_compaction_num_singleton_deltas=5,
max_cumulative_compaction_num_singleton_deltas=20,
enable_vertical_compaction=true
02:50 滚动重启 3 个 BE
03:10 compaction score 降至 50 以下
03:30 查询 P99 恢复到 70ms

这次调优能快速生效,关键在于先确认磁盘 IO 和 CPU 不是瓶颈。如果磁盘 IO 已经打满,盲目提高并发任务数只会让查询更慢。参数调整必须基于监控数据,而不是凭感觉。

6. 个人感悟:排查问题的“洋葱模型”

每一次线上问题排查,都像剥洋葱。第一层是表象:查询慢;第二层是系统指标:CPU、IO、内存;第三层才是 Doris 自己的机制:Rowset 堆积、Compaction 积压。

我刚做后端时,遇到慢查询就想着加索引。后来发现很多问题出在存储引擎的角落。现在我习惯在动任何参数前,先问自己三个问题:

  1. 这个问题靠什么指标可以准确判断?
  2. 调整参数会影响哪些其它环节?
  3. 我能用哪些实验来验证效果?

这不是什么高深的方法论,而是无数个深夜告警逼出来的直觉。技术人的成长,往往不是学会了多少新框架,而是学会了如何快速定位问题、如何做权衡。

另外,那天夜里我学到的最重要的一课:监控比事后排查更重要。如果我们在 BE 上配置了 CompactionScore 的持续监控,可能根本不会等到告警。工作与生活的平衡,一部分也来自把问题消灭在萌芽状态,而不是半夜爬起来救火。

核心要点

  • Compaction 积压的典型信号:VersionCount 过高、cumulative_score 远大于触发阈值。
  • 高频小批量导入会快速堆积小 Rowset,查询扫描文件数线性增加,延迟飙升。
  • Cumulative Compaction 负责合并小 Rowset,Base Compaction 负责跨层合并,两者对写放大和查询延迟的影响不同。
  • 调参前必须确认磁盘 IO、CPU 是否空闲,否则提高 compaction_task_num_per_disk 可能加重查询负担。
  • 合理设置 cumulative_compaction_num_singleton_deltasmax_cumulative_compaction_num_singleton_deltas,让 Compaction 在可接受的写放大内及时消化小文件。
  • 开启 enable_vertical_compaction 可以在数据量大时降低内存峰值,但会增加临时文件写放大。
  • 所有参数调整都需要滚动重启 BE 并持续观察 Compaction Score 和查询延迟,不能一改了之。

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