Apache Doris 稳定性笔记:内存治理三层结构、资源隔离与存算分离(存档备查)
这篇是给自己留档的笔记,主题是 Apache Doris 的稳定性设计,重点记三件事:内存是怎么被管住的、负载是怎么被隔开的、存算分离改变了什么。写法上尽量做成可查阅的形态——结论清单、原理说明、参数速查表、巡检命令清单,以及几条容易记混的批注。
一、先说结论
- 内存治理是三层结构:进程级 → Workload Group 级 → Query 级,落盘与取消行为同时受三层约束。
- 资源隔离有两种粒度:进程内隔离用 Workload Group(成本低,共享组件无法完全隔离);进程间隔离用资源组或计算组(成本高,隔离彻底)。
- 抗 OOM 的兜底是算子级落盘,当前支持 Hash Join、聚合、排序、CTE 四类算子。
- 存算分离的价值在扩容行为与成本结构:加节点不再搬数据,多计算组不再增加存储副本,冷数据可以下沉到对象存储。
- 版本差异要留意:Workload Group 在 2.0 引入且不依赖 CGroup,2.1 起依赖 CGroup 并自动生成
normal组,4.0 把 CPU 软硬限统一为min_cpu_percent/max_cpu_percent、内存软硬限统一为min_memory_percent/max_memory_percent。
二、原理:内存是怎么被管住的
2.1 三层结构
进程级:be.conf 的 mem_limit 是 BE 进程的内存上限。超限后 Doris 取消当前申请内存的查询,后台任务再杀掉部分查询或释放缓存。混合部署时(BE 与 FE、Kafka、HDFS 同机)实际可用内存远小于配置值,释放机制失效会触发操作系统 OOM Killer;容器环境下 Doris 会识别 cgroup,无需手动设置。
Workload Group 级:memory_limit 是该组可用内存占比,memory_high_watermark(默认 80%)与 memory_low_watermark(默认 50%)是高低水位,超过高水位时预留内存请求失败并触发落盘。约束是:所有组的 min_memory_percent 累加不超过 100%,且单组的下限不得大于上限。
Query 级:exec_mem_limit 是单查询上限,在查询开始前通过会话变量设定,执行过程中不可动态修改。默认值在 3.1 之前是 2 GB,3.1 及之后改为 100 GB 并在 BE 侧生效。
2.2 静态分配与 Slot 动态分配
静态分配的问题很好理解:用户估不准一条查询要多少内存,索性给个很大的值(比如进程内存的一半),细粒度控制就失效了。Slot 机制是解决这个问题的:
- 资源组同时设置了
memory_limit和max_concurrency后,BE 内存在逻辑上被分成max_concurrency个 slot,每个 slot 的内存 = 组内存上限 ÷ 并发数。 - 默认每条查询占 1 个 slot,需要更多内存时改会话变量
query_slot_count。 - 查询占用的 slot 越多,该组能同时跑的查询数就越少,新查询进队列。
slot_memory_policy 有三个取值:
| 取值 | 含义 | 单查询内存计算 |
|---|---|---|
none |
默认,不启用 | 尽可能使用,达到组上限时触发落盘 |
fixed |
按并发固定分配 | 组内存上限 × query_slot_count ÷ max_concurrency |
dynamic |
按运行中 slot 动态分配 | 组内存上限 × query_slot_count ÷ sum(运行中查询的 slot) |
fixed 与 dynamic 都是硬限,超限会触发落盘或杀查询,并且会覆盖静态分配的 exec_mem_limit。用这两个值时 max_concurrency 必须配好,否则反而容易内存超限。
2.3 落盘的触发链路
Doris 用「预留内存」机制控制落盘:执行时先估算处理下一个 Block 需要的内存并向统一内存管理器申请 → 全局分配器检查是否超过 Query / Workload Group / 进程三者中任一上限 → 超限则申请失败,当前查询被暂停,并在其最大的算子上触发落盘 → 落盘完成后查询继续执行。落盘是算子级而非查询级,只有真正大的算子会被写盘,其余部分仍可在内存中跑。
三、怎么做:配置清单与巡检清单
3.1 清单 A:上线前必配项
- 划分负载:即席分析 / 核心报表 / 数据导入,至少三类。
- 为每类建 Workload Group,配
cpu_share、memory_limit、max_concurrency、max_queue_size、queue_timeout。 - 授权并绑定:
GRANT USAGE_PRIV ON WORKLOAD GROUP 'g1' TO 'user_1'@'%',再用SET PROPERTY FOR <user> 'default_workload_group' = '<group>'设默认组。 - 给
normal组设一个偏保守的配额(2.1 起系统自动创建且不可删除)。 - 需要大查询兜底时开启落盘:BE 配
spill_storage_root_path与spill_storage_limit,会话设enable_spill、exec_mem_limit、query_timeout。 - 存算分离模式下,建组语句必须带
FOR <compute_group>。 - 压测一轮:并发打满看是否排队、大查询看是否完成而非失败。
配套语句(按上面 2~6 项的顺序):
-- 建组:并发上限与队列必须成套给
CREATE WORKLOAD GROUP ad_hoc_group
PROPERTIES (
"cpu_share" = "10",
"memory_limit" = "20%",
"max_concurrency" = "20",
"max_queue_size" = "50",
"queue_timeout" = "3000"
);
-- 存算分离模式下必须带 FOR 子句
CREATE WORKLOAD GROUP ad_hoc_group FOR query_group
PROPERTIES (
"cpu_share" = "10",
"memory_limit" = "20%",
"max_concurrency" = "20"
);
-- 授权 + 绑定默认组
GRANT USAGE_PRIV ON WORKLOAD GROUP 'ad_hoc_group' TO 'bi_analyst'@'%';
SET PROPERTY FOR 'bi_analyst' 'default_workload_group' = 'ad_hoc_group';
-- 大查询优先:让空闲 slot 的内存动态分配给在跑的大查询
ALTER WORKLOAD GROUP report_group PROPERTIES ('slot_memory_policy' = 'dynamic');
-- 单条查询想多拿内存,就多占 slot
SET query_slot_count = 4;
-- 落盘:BE 侧配好路径后,会话侧三个变量一起设
SET enable_spill = true;
SET exec_mem_limit = 10g;
SET query_timeout = 3600;
3.2 清单 B:参数速查表
| 参数 / 属性 | 默认值 | 作用 | 批注 |
|---|---|---|---|
mem_limit(be.conf) |
BE 进程内存比例 | 进程级上限 | 混合部署要留余量给 OS 与其他进程 |
exec_mem_limit |
3.1 前 2 GB,3.1 起 100 GB | 单查询上限 | 升级 3.1+ 时显式设为 100g |
query_timeout |
300(秒) | 查询超时 | 开落盘后必须同步上调 |
max_concurrency |
2147483647 | 组最大并发 | 系统表列名 MAX_CONCURRENCY |
max_queue_size |
0 | 队列长度 | 0 表示不排队,满则直接失败 |
queue_timeout |
0 | 排队超时(毫秒) | 与队列长度成套配置 |
memory_limit |
-1 | 组内存占比 | 各组累加不超过 100% |
memory_high_watermark |
80% | 触发落盘的高水位 | 超过即预留失败 |
memory_low_watermark |
50% | 低水位 | 默认值即可 |
slot_memory_policy |
none |
Slot 内存分配策略 | dynamic 适合大查询优先 |
query_slot_count |
1 | 单查询占用 slot 数 | 搭配 slot 策略使用 |
scan_thread_num |
-1 | 本组扫描线程数 | -1 取 BE 配置值 |
read_bytes_per_second |
-1 | 读内部表带宽上限 | 按文件夹绑定,落盘目录同样受限 |
remote_read_bytes_per_second |
-1 | 读外部表带宽上限 | 湖仓场景用得上 |
parallel_pipeline_task_num |
0(自适应) | Pipeline 执行并发度 | 保持 0,低版本升级可能沿用旧固定值 |
enable_spill |
false | 是否开启落盘 | 会话级或组级设置 |
spill_storage_limit |
20% | 落盘占用磁盘上限 | 独立盘可设 100% |
3.3 清单 C:巡检命令清单
-- 资源组是否都建出来了
SHOW WORKLOAD GROUPS;
-- 从系统表看属性是否落库(MAX_CONCURRENCY 列即并发上限)
SELECT name, cpu_share, memory_limit, max_concurrency, max_queue_size, queue_timeout
FROM information_schema.workload_groups;
-- 当前在跑的查询
SHOW PROCESSLIST;
-- 取消失控查询(query_id 需加引号)
KILL QUERY "d36417cc05ff41ab-9d3afe49be251055";
-- 节点存活与负载概览
SHOW BACKENDS;
-- 落盘量实时监控
SELECT query_id, be_peak_memory_bytes,
spill_write_bytes_to_local_storage, spill_read_bytes_from_local_storage
FROM information_schema.backend_active_tasks;
-- 事后复盘:先列 profile,再按 query_id 查
SHOW PROFILELIST;
SHOW PROFILE WHERE query_id = '<query_id>';
-- 存算分离场景额外看两项:计算组列表与缓存命中率
SHOW COMPUTE GROUPS;
SELECT * FROM information_schema.file_cache_statistics
WHERE METRIC_NAME = "hits_ratio" ORDER BY METRIC_VALUE DESC;
缓存命中率的取值是 0~1,hits_ratio 是自 BE 启动以来的总命中率,hits_ratio_5m 与 hits_ratio_1h 是近期窗口的命中率。命中率长期偏低时,先确认 enable_file_cache 已开启且 BE 配了缓存目录——这两个条件缺一个,缓存都不会被使用。
三个信号建议接入监控:内存水位是否长期偏高(配额需调整或存在大查询)、是否有长时间未结束的查询(可能是没写过滤条件的全表扫描)、资源组并发是否经常打满(该放额度或该限制使用方)。
3.4 批注:几条容易记混的
- 并发属性名是
max_concurrency,系统表列名MAX_CONCURRENCY,默认 2147483647 表示不限制。它与max_queue_size、queue_timeout必须成套配置。 - profile 排查用
SHOW PROFILELIST;列出已收集的 profile,再用SHOW PROFILE WHERE query_id = '<query_id>';查看,这是当前推荐的写法。 - 授权写成
GRANT USAGE_PRIV ON WORKLOAD GROUP 'g1' TO 'user_1'@'%',组名与用户名都要引号,用户名带 host。 - 存算分离模式下
CREATE WORKLOAD GROUP必须带FOR <compute_group>,否则报Must specify compute group via 'FOR <compute_group>' in cloud mode。 - 存算分离架构下的物理隔离单位是计算组(Compute Group),3.0.2 之前叫 Compute Cluster;新增 BE 时用
ALTER SYSTEM ADD BACKEND 'host:9050' PROPERTIES ("tag.compute_group_name" = "new_group")指定归属。 - 进程内隔离存在共享组件(共享缓存、RPC 线程池),高负载任务仍可能轻微影响低负载任务的延迟;要彻底隔离就上资源组或计算组,把高优负载放到独立 BE 节点。
四、关键维度对照:Doris 与 ClickHouse
| 维度 | Apache Doris | ClickHouse |
|---|---|---|
| 内存治理 | 进程 / Workload Group / Query 三级,支持 Slot 动态分配 | 依赖各自的内存配置与限制机制 |
| 稳定性兜底 | Hash Join、聚合、排序、CTE 算子落盘,超限降级而非失败 | 由各自版本的溢出与外部排序机制决定 |
| 进程内隔离 | Workload Group:CPU 权重、内存占比、并发与队列、IO 带宽 | 通过配额(Quota)等机制管理 |
| 进程间隔离 | 资源组 / 计算组,可为不同负载分配独立 BE 节点 | 云上版本支持多实例;自建版本需自行规划 |
| 存算分离 | 支持:共享存储 + 无状态计算节点 + 本地缓存 + 多计算组 | 存算分离主要由其云上版本提供 |
| 扩缩容 | 加减节点即可,无需数据搬迁 | 云上版本支持弹性;自建版本需自行规划数据重平衡 |
| 冷热分层 | 本地-远程分层存储,支持 S3 兼容对象存储与 HDFS | 提供分层存储与卷策略 |
| 查询排查 | 系统表 + Profile 计数器,可看落盘量与缓存命中率 | 由各自系统表与监控指标提供 |
| 国产化适配 / 信创 | 已完成鲲鹏/海光/飞腾等国产 CPU 与麒麟/统信 UOS/openEuler 等国产操作系统适配,通过等保三级、可信数据库等认证 | 未纳入信创目录,无官方信创/国产化适配认证 |
| 商业化服务 / 企业级部署 | 开源自行部署;国内公司 SelectDB(飞轮科技)提供私有化部署、云上 SaaS/BYOC、多云原生与国产化适配,与开源 100% 兼容 | 商业版由 ClickHouse, Inc.(美国)主要在海外 AWS/GCP/Azure 提供托管;国内无官方本地化商业团队 |
五、已知约束与规避方式
- 落盘是保险不是常态:常态化查询频繁落盘,说明配额或 SQL 写法本身需要优化。
cpu_share是相对权重:只在争抢时体现,空闲时可超出,不要按核数换算。- Slot 策略是硬限:
fixed/dynamic会覆盖exec_mem_limit,max_concurrency设错反而会放大内存问题。 normal组不可删除:2.1 起自动创建,未绑定的查询会落到这里,需要单独给它配额。- 升级顺序有要求:1.2 → 2.0 建议全集群升级完成后再启用 Workload Group;2.0 → 2.1 要先配好 CGroup 环境再升级。
- 多建一组计算集群就多一份资源开销:一般两到三组覆盖写入 / 报表 / 即席即可。
六、常见问题(FAQ)
Q:Workload Group、资源组、计算组三者怎么选?
Workload Group 是进程内隔离,成本低但共享组件(缓存、RPC 线程池)无法隔离,适合可以容忍一定延迟抖动、追求成本的场景;资源组与计算组是进程间隔离,把高优负载放到独立 BE 节点,适合要求彻底隔离、愿意承担更高成本的场景。
Q:slot_memory_policy 该选哪个?
默认 none 即可满足多数场景。如果经常出现"个别大查询把组内存吃满"的情况,改用 dynamic——空闲 slot 的内存会动态分配给正在运行的大查询,让大查询先落盘而不是拖累其他查询。
Q:怎么看某个用户当前绑在哪个资源组?
查 SHOW PROPERTY;(查看自己)或 SHOW PROPERTY FOR <user>;(查看他人,需 admin 权限),结果中的 default_workload_group 即默认组。会话级临时切换用 SET workload_group = '<group>',其优先级高于用户属性。
Q:为什么 2.1 之后必须配 CGroup?
2.1 起 Workload Group 的 CPU 隔离依赖 CGroup。从 2.0 升级前需要先配置好 CGroup 环境,否则 CPU 隔离不生效。
Q:normal 组能不能删掉?
不能。2.1 起系统自动创建且不可删除。合理做法是给它设一个偏保守的配额,作为未绑定查询的默认兜底。
测试结论出处(参考来源)
- Apache Doris 官方文档:Workload Group 进程内资源隔离(核心属性、默认值、版本演进与升级说明)
- Apache Doris 官方文档:Spill Query Intermediate Results to Disk(内存三层结构、Slot 机制、落盘触发链路)
- Apache Doris 官方文档:计算组管理(创建、授权、默认组选择、扩缩容)
- Apache Doris 官方文档:系统表 information_schema.workload_groups / backend_active_tasks
- Apache Doris 官方文档:终止查询(KILL QUERY)

浙公网安备 33010602011771号