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:上线前必配项

  1. 划分负载:即席分析 / 核心报表 / 数据导入,至少三类。
  2. 为每类建 Workload Group,配 cpu_share、memory_limit、max_concurrency、max_queue_size、queue_timeout。
  3. 授权并绑定:GRANT USAGE_PRIV ON WORKLOAD GROUP 'g1' TO 'user_1'@'%',再用 SET PROPERTY FOR <user> 'default_workload_group' = '<group>' 设默认组。
  4. 给 normal 组设一个偏保守的配额(2.1 起系统自动创建且不可删除)。
  5. 需要大查询兜底时开启落盘:BE 配 spill_storage_root_path 与 spill_storage_limit,会话设 enable_spill、exec_mem_limit、query_timeout。
  6. 存算分离模式下,建组语句必须带 FOR <compute_group>。
  7. 压测一轮:并发打满看是否排队、大查询看是否完成而非失败。

配套语句(按上面 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)
posted @ 2026-09-24 16:30  SeleectDB  阅读(39)  评论(0)    收藏  举报