Day 23|工作负载治理:Workload Group、资源隔离、并发、队列、Spill 与熔断

Day 22 解决了 FE、BE、副本、扩缩容、升级、备份和恢复等生命周期问题。生产集群具备高可用拓扑以后,还会面临另一类长期风险:系统整体没有节点故障,业务体验却持续恶化。经营大屏突然变慢,临时分析占满 CPU,历史回算挤压磁盘带宽,某条大 Join 消耗大量内存,几十个相同报表同时到达后把查询线程和连接池堆满。此时 FE、BE 都可能保持 Alive,真正失控的是共享资源的分配秩序。
Doris 的工作负载治理围绕三件事展开:先把不同业务划入清晰的资源合同,再用并发与队列吸收流量峰值,最后用 Spill、SQL Block Rule 和 Workload Policy 处理超出合同的异常任务。今天的目标,是让在线报表、Ad-hoc、ETL、导入和物化视图刷新在同一平台中保持可控共存。
完成本篇后,你应当能够:
- 区分 Resource Group、Workload Group 与 Compute Group 的隔离边界;
- 创建 Workload Group,并完成权限授予、用户默认绑定和会话临时路由;
- 使用 CPU、内存、IO、并发和队列参数表达业务优先级;
- 解释 BE Process、Workload Group、Query 三级内存约束;
- 判断一次查询应继续使用内存、进入 Spill、排队、熔断或取消;
- 使用 SQL Block Rule 阻断无分区条件、超大扫描和危险 SQL 模式;
- 使用 Workload Policy 根据真实执行时间、扫描量和内存消耗止损;
- 通过系统表、Audit Log、Query Profile 和监控指标形成资源诊断证据链;
- 输出一份能够进入生产评审的《多租户资源隔离策略》。
零、先校准 Doris 4.x 的术语和能力边界
工作负载治理领域存在一些历史名称。当前 4.x 官方文档将资源控制分成物理隔离与逻辑隔离两层:存算一体模式可以使用 Resource Group 按 BE 节点划分资源池;存算分离模式使用 Compute Group 划分独立计算池;Workload Group 在单个 BE 进程内部继续细分 CPU、内存和 IO,并附带并发与队列能力。[1]
0.1 Workload Group 是进程内隔离
一个 BE 进程中可以同时存在多个 Workload Group。查询线程、内存 Tracker 和扫描吞吐会按组进行计量与约束。多个组仍然共享 BE 进程、部分 Cache、RPC 线程池和后台组件。某个 BE 进程发生崩溃或被操作系统 OOM Killer 终止时,该 BE 上的全部组都会中断。[2]
因此,Workload Group 适合细粒度资源分配、突发流量控制和低成本多租户。核心业务需要进程级故障隔离时,应使用 Resource Group 或 Compute Group,把关键负载放到独立 BE 或独立计算组。
0.2 4.0 统一了软限制和硬限制的表达
当前推荐的 CPU 参数是 min_cpu_percent 与 max_cpu_percent,内存参数是 min_memory_percent 与 max_memory_percent。前者表达竞争时的最低保障,后者表达任何时候都不能突破的最高上限。系统表和部分兼容接口中仍可能出现 cpu_share、cpu_hard_limit、memory_limit 等历史字段,生产配置应以目标 Patch 版本的 SHOW WORKLOAD GROUPS 和官方 SQL 参考为准。[2]
0.3 Workload Group 无法完整约束所有后台资源
当前 Workload Group 可以管理查询的 CPU、内存、内部表和外部表读取吞吐。高吞吐导入可能触发 Compaction,Compaction 当前没有完整纳入 Workload Group CPU 限制;共享 Cache、部分 RPC 和系统线程同样位于组外。在线与离线混合压测需要包含真实导入和 Compaction,单纯运行 SELECT 难以覆盖生产争用形态。[2]
0.4 Spill 默认关闭
当前支持 Spill 的算子包括 Hash Join、Aggregation、Sort 和 CTE。Spill 会将中间状态写入本地磁盘,帮助内存受限的查询继续完成,同时显著增加磁盘 IO 和执行时间。enable_spill 默认值为 false,启用时应配置独立 Spill 路径、容量限制和更合理的 query_timeout。[3]
0.5 分区强制规则存在 Patch 版本边界
SQL Block Rule 的 require_partition_filter 在 4.0 系列从 4.0.7 起支持,在 4.1 系列从 4.1.2 起支持。当前 Stable 4.0.8 与 Latest 4.1.3 均覆盖该能力。[4]
截至 2026 年 8 月 17 日,Apache Doris 下载页将 4.1.3 标记为 Latest、4.0.8 标记为 Stable。本文的命令和限制以当前 4.x 文档为基线,上线前仍需在计划部署的 Patch 版本重新执行语法与行为验证。[5]
一、混合工作负载为何容易失控

数据库资源争用经常从一条看起来合理的 SQL 开始。某个分析师查询过去一年的订单明细,SQL 使用了有效 Join 和聚合,单独运行也能在几十秒内完成。问题出现在它与其他任务同时执行时:经营大屏每十秒刷新,ETL 正在重算昨日分区,Routine Load 持续写入,后台 Compaction 合并小版本,另一个团队又发起了宽表导出。
这几类负载的资源曲线不同:
| 负载 | 典型目标 | 主要资源 | 常见风险 |
|---|---|---|---|
| 核心在线报表 | 低延迟、稳定 p95/p99 | CPU、线程、缓存 | 被长查询挤压后尾延迟上升 |
| Ad-hoc 探索 | 允许较长时间,结果灵活 | 内存、Shuffle、Spill | SQL 不可预测,容易产生大中间结果 |
| ETL / 回算 | 吞吐和完成时间 | CPU、IO、网络、磁盘 | 长时间占用资源,污染缓存 |
| 实时导入 | 持续稳定写入 | MemTable、磁盘、Compaction | 小批过多导致版本与合并压力 |
| MV 刷新 | 按时完成,数据新鲜 | Scan、Join、Aggregate | 与报表争抢相同数据和计算资源 |
一条大扫描首先增加本地或远程读取;扫描数据进入 Join 和聚合后继续消耗 CPU 与内存;内存不足会触发 Spill;Spill 又占用本地磁盘和线程;同一时间 Compaction 也在使用磁盘;RPC 和 Pipeline 调度得到的 CPU 变少,短查询开始排队。最终表现可能是查询超时、导入延迟、FE 入口堆积和 BE 尾延迟共同升高。
1.1 query_timeout 只能约束时间
单条 query_timeout 可以终止运行时间过长的 SQL,却无法保证它在超时前使用多少 CPU、扫描多少数据、占用多少内存,也无法控制几十条查询同时进入系统。一个设置为 300 秒超时的查询,可能在第 20 秒已经让 BE 内存紧张;五十条 5 秒查询也可能在短时间内把并发和线程池推到极限。
稳定治理需要五个维度共同工作:
- 资源分组:明确谁与谁共享资源;
- 资源上限:限制 CPU、内存和 IO;
- 入口节流:限制并发并建立等待队列;
- 执行降级:允许大查询 Spill,降低 OOM 风险;
- 异常熔断:在规划或执行阶段阻断超出边界的任务。
二、三种隔离方式怎样选择

Doris 当前的三种隔离方式可以理解成两道边界:先决定是否拆分 BE 进程或计算池,再决定是否在进程内进一步分配资源。
2.1 Resource Group:存算一体下的节点级物理隔离
Resource Group 通过 BE Tag 划分节点,并让表副本按照 Tag 放置。绑定到某个组的用户只在对应 BE 节点上查询。在线组与离线组使用不同 BE 进程,离线查询造成 BE OOM 时,在线组的 BE 不会一起退出。
这种方式的代价主要在副本和容量。每个需要读取某张表的 Resource Group 都要拥有可用副本,组越多,存储开销和副本调度复杂度越高。跨组查询受到副本放置约束,资源借用能力也较弱。
2.2 Workload Group:BE 进程内的逻辑隔离
Workload Group 不增加节点和副本,可以为不同用户设置 CPU 最低保障、CPU 最高上限、内存上下限、IO 吞吐、扫描线程、并发、队列与 Spill 策略。系统空闲时,软限制允许业务借用更多资源;竞争出现后,最低保障和最高上限开始发挥作用。
这类隔离成本较低,也保留了共享集群的资源利用率。它无法隔离 BE 进程级故障,系统 Cache 和后台任务也可能形成残余影响。
2.3 Compute Group:存算分离下的计算池硬隔离
存算分离模式下,Compute Group 使用独立计算节点承担查询。在线、ETL、AI 检索可以分别进入不同 Compute Group,共享远端存储和元数据。工作负载需要更细粒度控制时,可以在每个 Compute Group 内再创建 Workload Group。[6]
2.4 一条实用选择路径
可以按照以下顺序判断:
- 业务能否接受同一 BE 进程故障时共同中断;
- 是否愿意承担额外节点、副本或计算池成本;
- 是否需要共享数据、共享缓存与弹性借用资源;
- 主要诉求是稳住资源比例,还是隔离进程级故障;
- 部署形态是存算一体还是存算分离。
核心支付报表、监管查询和全量回算之间需要强隔离时,节点级资源池通常更稳妥。多个普通业务共享同一集群,希望控制突发和公平性时,Workload Group 提供更高的资源利用率。
三、Workload Group 的对象、权限与路由

Workload Group 的落地包含四个对象:组定义、使用权限、用户默认绑定和 Session 临时绑定。只创建一个组并不会自动把业务流量送进去。
3.1 创建在线报表组
CREATE WORKLOAD GROUP IF NOT EXISTS wg_bi
PROPERTIES (
"min_cpu_percent" = "30%",
"max_cpu_percent" = "60%",
"min_memory_percent" = "25%",
"max_memory_percent" = "45%",
"max_concurrency" = "12",
"max_queue_size" = "30",
"queue_timeout" = "3000",
"slot_memory_policy" = "dynamic"
);
参数值只是实验起点。生产比例需要结合 BE 核数、mem_limit、实际并发、Query Profile 和目标 SLA 校准。
3.2 授权和绑定
GRANT USAGE_PRIV ON WORKLOAD GROUP 'wg_bi' TO 'bi_user'@'%';
用户能够看到并使用目标组后,可以设置持久默认组:
SET PROPERTY 'default_workload_group' = 'wg_bi';
当前 Session 需要临时切换时:
SET workload_group = 'wg_bi';
Session 变量优先级高于用户默认属性。连接池中执行临时切换时,需要在借出连接后设置,在归还连接前恢复,避免下一个请求继承错误的组。
3.3 normal 组的作用
Doris 2.1 起会自动创建不可删除的 normal 组。没有匹配到其他组的查询通常进入该组。生产治理中应避免把所有默认流量长期留在 normal:未知应用、临时账号和遗漏绑定会继续共享一个大池,最终绕过业务优先级设计。
3.4 存算分离需要绑定 Compute Group
存算分离模式创建 Workload Group 时需要显式使用 FOR <compute_group>。Workload Policy 绑定组时也需要使用 <compute_group>.<workload_group> 全限定名。组一旦绑定到某个 Compute Group,当前实现不支持直接迁移到其他 Compute Group,需要重新创建并切换流量。[6]
四、CPU 治理:软保障、硬上限和系统余量

4.1 min_cpu_percent 表达最低保障
当多个组同时争抢 CPU 时,最低保障部分不能被其他组抢走;系统空闲时,该组可以使用更多 CPU。在线报表具有明显高峰和低谷时,最低保障可以提高高峰期的稳定性,同时保留低峰期的整体利用率。
所有组的 min_cpu_percent 之和不能超过 100%,单个组的最小值也不能大于最大值。实际规划通常还会保留一部分 CPU 给 RPC、Compaction、Cache 回收和系统线程。将所有组的保障值加到 100%,会让内部组件在高负载时缺少余量。
4.2 max_cpu_percent 表达最高上限
最高上限属于硬限制。Ad-hoc 和 ETL 即使在系统空闲时也不能突破该比例。硬限制能够防止某个组把整个 BE CPU 打满,对核心在线业务更有保护价值。代价是离线任务在空闲时也无法充分使用全部核心,完成时间可能延长。
一个 64 核 BE 的起始模板可以是:
| 组 | 最低保障 | 最高上限 | 说明 |
|---|---|---|---|
| 核心报表 | 35% | 60% | 高峰期优先,允许突发 |
| Ad-hoc | 5% | 20% | 低保障、严格封顶 |
| ETL | 5% | 20% | 稳定运行,限制峰值 |
| 系统余量 | — | 约 15% | 留给内部线程与后台任务 |
该表只表达优先级,最终结果需要以目标集群压测为准。
4.3 CGroup 环境
Workload Group 的 CPU 管理依赖 CGroup。物理机需要确认 CGroup V1/V2、创建 Doris 目录、设置权限,并配置 doris_cgroup_cpu_path;机器重启后目录可能被清空,应通过 systemd 固化创建与授权步骤。容器中的 CPU 限制基于容器已经获得的 CPU 继续细分:容器只有 8 核、组上限 50% 时,实际最多使用约 4 核。[2]
容器需要具备操作宿主机 CGroup 的权限。Kubernetes 场景优先使用 Doris Operator,并在安全评审中明确 privileged 或等效权限边界。
4.4 IO 与扫描线程同样属于资源合同
CPU 被限制以后,离线任务仍可能通过大量并行 Scanner 把本地盘或对象存储带宽打满。Workload Group 提供 read_bytes_per_second 和 remote_read_bytes_per_second,分别限制读取 Doris 内部表与外部表的吞吐;scan_thread_num、max_remote_scan_thread_num 和 min_remote_scan_thread_num 用于控制扫描线程池规模。
这些参数需要和磁盘、对象存储、网络及查询并行度一起验证。把线程数压得过低,会让 CPU 空闲但扫描排队;只限制吞吐、保留过多线程,又可能增加上下文切换与小 IO。内部表读取上限按目录生效,Spill 文件位于同一受控目录时也会受影响。在线报表依赖热缓存时,不应通过清空 OS Cache 直接评估 IO 上限,生产 POC 应分别记录冷缓存和热缓存结果。
五、三级内存模型

Doris 的内存治理有三层:BE Process、Workload Group、Query。一次算子申请内存时,需要同时满足这三层约束。[3]
5.1 BE Process 层
be.conf 中的 mem_limit 控制整个 BE 进程可使用的内存上限。这里覆盖查询、导入、Cache、Compaction 和其他内部对象。BE 与 Kafka、HDFS、代理程序混部时,操作系统实际可用内存可能远小于配置值,Doris 内部释放机制还没有完成,系统 OOM Killer 已经开始杀进程。
生产环境应尽量让一台机器只运行一个 BE,容器资源限额也要与 mem_limit 的自动检测结果一致。监控需要同时观察 BE Tracker、进程 RSS、系统 Available Memory 和容器限制。
5.2 Workload Group 层
min_memory_percent:内存紧张时为组保留的最低份额;max_memory_percent:组的内存最高边界,超过后触发 Spill 或 Kill;memory_low_watermark:低水位,用于区分相对宽松的内存区间;memory_high_watermark:高水位,超过后 Reserve Memory 更容易失败并触发 Spill。
当前 Workload Group 指南列出的默认值为 50% 与 80%,而部分当前 SHOW WORKLOAD GROUPS 示例显示 75% 与 85%。这类默认值可能随版本和配置演进,目标集群应以 SHOW WORKLOAD GROUPS 的实际结果为准。
所有组的最低内存保障之和不能超过 100%。最低保障配置过高会让系统缺少弹性,配置过低又可能让在线业务在资源紧张时得不到稳定内存。
5.3 Query 层
exec_mem_limit 在查询开始前确定,执行过程中不能动态修改。官方文档说明,3.1 之前默认值为 2 GB,3.1 及以后调整为 100 GB。升级和迁移环境应执行:
SHOW VARIABLES LIKE 'exec_mem_limit';
单查询上限过高时,一条大查询可以快速占满组内存;上限过低时,正常查询会频繁 Spill 或失败。适合的值来自真实 SQL 的 Peak Memory 分布,而非按机器总内存直接取一个比例。
六、并发与队列:先控制进入执行层的任务数量

6.1 三个核心参数
| 参数 | 默认值 | 作用 |
|---|---|---|
max_concurrency |
最大整数 | 同一组正在运行的查询上限 |
max_queue_size |
0 | 等待队列长度,0 表示不排队 |
queue_timeout |
0 | 最长等待毫秒数,0 表示立即失败 |
当运行数达到 max_concurrency 时,新查询进入队列;队列已满时直接拒绝;等待超过 queue_timeout 后返回失败。排队把突发流量转化成有界等待,帮助 BE 避免在几秒内启动过多 Fragment 和 PipelineTask。
6.2 多 FE 集群的并发口径
当前队列参数按照单 FE 粒度生效。3 个 FE、每个 FE 的 max_concurrency=10 时,均匀分流下的集群并发可能接近 30。业务期望集群同时运行 12 条核心报表,前面有 3 个 FE 时,可以从单 FE 设置 4 开始验证,并考虑入口流量是否均匀。[7]
这项边界也要求负载均衡器具备健康检查和相对均匀的连接分配。某个长连接池长期只连接一台 FE,会让该 FE 的队列提前饱和,其他 FE 仍然空闲。
6.3 队列参数要与调用方一起设计
数据库队列超时为 3 秒,应用请求超时为 2 秒时,客户端会先断开;应用随后自动重试,又在队列中制造一个新请求。合理的时间关系通常是:数据库队列超时短于应用总超时,并给真正执行、网络返回和少量重试保留预算。
运维账号在紧急处置时可能也被队列阻塞,可以在当前会话临时设置:
SET bypass_workload_group = true;
操作完成后应关闭或重新建立连接,避免日常查询绕过治理。
七、Slot 内存:把并发与单查询预算联动

静态 exec_mem_limit 很难同时适应小查询和大查询。Workload Group 同时配置 max_memory_percent 与 max_concurrency 后,可以把组内存逻辑切成多个 Slot:
单查询内存预算
= Workload Group 可用内存
× query_slot_count
÷ max_concurrency
默认每条查询使用 1 个 Slot。需要更多内存的查询可以设置:
SET query_slot_count = 2;
查询占用更多 Slot 后,同组可运行的查询数量会下降,新增任务进入队列。这让“给大查询更多内存”和“降低并发”形成同一个资源合同。
slot_memory_policy 有三种模式:
| 模式 | 行为 | 适用特点 |
|---|---|---|
none |
默认模式,查询尽可能使用内存,到组上限后 Spill | 配置简单,单查询预算不稳定 |
fixed |
按 Slot 计算固定硬上限 | 可预测,空闲资源利用较保守 |
dynamic |
空闲 Slot 可以动态分配给运行中的大查询 | 利用率较高,需要严格设置并发 |
fixed 与 dynamic 会覆盖静态 exec_mem_limit。如果 max_concurrency 设置过高,每个 Slot 的内存很小,查询更容易触发 Spill。设计 Slot 时要把并发、内存和完成率一起验证。
