
干过大数据的同学应该都有这种体会白天业务线正在跑例行报表几条即席分析查询冲进来集群 CPU 瞬间打满内存持续告警紧接着是一串 Executor Lost、Container OOM 的报错。到了晚上离线任务和实时宽表构建挤在一起调度队列堆成山谁先跑谁后跑完全看命。这就是典型的 OLAP 资源隔离和调度策略没有做好的现场。我最早接触这个主题是在维护一套十几台节点的 OLAP 集群时那时候团队里对“资源”的理解还是“机器内存够大就行”。直到一次事故——一个分析师提交了一个没有过滤条件的 join直接把整个节点搞到 Full GC影响了所有在跑的报表任务才逼着我系统性地去研究资源隔离和调度策略。这篇内容我想结合自己在几个主流引擎上的实践把 OLAP 场景下的资源隔离怎么做、调度策略怎么定、有哪些坑必须绕开完整梳理一遍。无论你是在校准备大数据方向面试还是刚接手集群运维都应该能从里面拿到一套可以直接落地的思路。1. 为什么OLAP必须谈资源隔离1.1 我看到的OLAP资源失控现场OLAP 场景和传统离线数仓最大的区别在于查询是临时的、突发的、不可预测的。Flink 跑实时任务资源占用相对固定用多少并行度、多少内存启动时就能算清楚。但 OLAP 不是同样一张宽表按分区查可能几百毫秒返回不带分区条件可能就是几分钟甚至直接卡死。更麻烦的是OLAP 集群往往同时服务多个角色运营看板、财务对账、数据产品 API、分析师即席查询、甚至算法团队的特征抽取。每类角色对稳定性、延迟、吞吐的要求完全不同。运营看板要求 99% 的查询在 3 秒内返回分析师即席查询可以接受 30 秒但要求能跑特别复杂的 join算法抽特征则希望把机器压满。如果所有查询共用一个资源池没人能保证 SLA。我印象很深的一次事故是这样某天下午一个同事跑“某季度全量用户行为分析”SQL 里三个大表直接不加条件做 join产生的中间结果集把 NodeManager 的可用内存全部吃掉随后整个节点开始疯狂 Full GC。当时集群上有二十多个正在运行的报表任务全部卡在等待资源上。由于我们当时用的是 FIFO 调度新任务永远排在后面整个集群的服务能力直接降为零。这个过程从查询提交到恢复用时将近四十分钟影响到的业务方超过十个。这类事故的核心原因只有一个没有隔离。任何一条不可控查询都可能污染整个集群。1.2 不隔离的典型故障链隔离缺失的场景下故障传导路径几乎是固定的单条查询申请大量内存触发 YARN 或容器级别的内存超额使用。操作系统开始 swap节点 IO 被打满。该节点上其他任务的执行速度急剧下降GC 频繁。任务超时或心跳中断触发重试重试进一步放大资源压力。调度器看到资源不足把新任务积压在队列里队列堆长之后触发超时和失败。这条链路里最容易被忽略的是第二步。很多人以为内存隔离只影响内存实际上内存打满之后最先崩溃的是磁盘 IO因为系统在疯狂 swap。这会让所有共享同一块物理磁盘的任务全部降速。你检查监控时如果只看 CPU 和内存大概率看不到根因。所以做 OLAP 资源管理先想清楚一个原则资源上限必须可以“硬限制”不能依赖“自觉”。任何一个跑在集群上的任务都应该在启动时就知道自己最多能用多少内存、多少 CPU、最多保持多少并发。超过上限的要么拒绝、要么排队、要么降级而不是让它去抢别人的资源。2. 资源隔离到底在隔离什么2.1 CPU与并发隔离CPU 隔离是个容易让人误解的概念。容器化环境下cgroup 的 cpu.shares 控制的是 CPU 时间片的权重不是绝对上限。举个例子两个容器权重分别是 1024 和 1024当只有一个容器的任务需要 CPU 时它可以占满所有核只有两个容器同时竞争时才会按权重分配。这在 OLAP 场景里导致了两个经典问题突刺问题某个查询突然发起了 50 个线程虽然容器的 CPU 权重不高但其他任务恰好不需要 CPU 时它可以瞬时吃满所有核导致其他任务的“偶发变慢”。权重误判你以为给项目 A 配了高权重就能高性能但如果项目 B 的任务持续占着 CPU 不放高权重也只能在调度周期内获得更多份额无法硬性阻止 B。实际操作中真正的 CPU 隔离要配合两层机制一是每个查询的并发度限制比如在 Presto 里限制单个 query 的 split 并行度在 Spark 里限制单任务的 executor 数量二是节点的最大并发查询数这和 CPU 核数是强相关的。我们后来遵循的经验值是节点并发查询数不超过 vcore 数的 2 到 3 倍超过的查询排队等待。2.2 内存隔离内存是 OLAP 资源隔离中最核心、最难做也是最先崩的一层。难点在于 OLAP 查询的内存占用是动态变化的。一个 join 操作build 阶段的内存是线性增长的一个 order by内存随输入数据变化一个 group bydistinct 基数决定了内存占用。你无法用静态配置精确预测。所以主流 OLAP 引擎都采用两层内存保护单查询内存上限比如 Presto 里的 query.max-memory-per-nodeDoris 里的 exec_mem_limit超过该限制的查询直接失败而不是继续拖垮节点。节点总内存池上限比如 Presto 的 JVM 堆内/堆外配置、Doris 的 MemTable 限制保证一个节点上所有查询的总内存不会超过物理内存的安全水位。这两层缺一不可。只有单查询限制多个查询并发时总内存仍然可能超只有总池限制单条大查询可能直接淘汰所有小查询。内存隔离还有个隐性成本——预留内存。JVM 堆内、堆外、操作系统页缓存、Spark 的 overhead 都需要预留桶位。我们遇到过“内存看着够一上线就 OOM”的情况后来排查发现是只算了 JVM 堆没算堆外内存和 page cache。最终的预留系数我个人经验是物理内存的 15%~20% 必须留出来不能全部塞给查询引擎。2.3 IO与缓存隔离IO 和缓存隔离是大多数团队忽略的重灾区。OLAP 引擎普遍依赖内存缓存和本地存储加速比如 ClickHouse 的 MergeTree 数据部分、Doris 的 BE 节点数据文件、Presto 的 spill 临时目录。这些 IO 路径如果不做区分容易被某类重 IO 查询拖垮。一个具体的例子ClickHouse 集群里如果分析人员跑一个全表 scan 的大查询同时实时写入任务的 merge 也在进行两者争抢磁盘 IO可能导致写入 backlog 堆积。解决方式通常是给不同账户配置不同的max_execution_time、max_threads和存储卷策略把分析查询限制在低频存储卷写入走高性能盘。缓存隔离也有讲究。很多引擎的缓存是全局共享的比如 Alluxio 和各类本地 cache。我见过的常见做法是按照业务线拆分 cache 目录或缓存集群避免一个业务的查询把另一个业务的热数据挤出缓存。这个在业务方对查询延迟极度敏感的公司尤其重要。2.4 从资源隔离到工作负载隔离隔离做到一定程度你会发现纯粹按资源类型去分并不够。更合理的方式是按工作负载类型做逻辑分组再在每个分组内做资源隔离。工作负载分类的常见维度读多写少 vs 写多读少报表查询与实时摄入负载模式完全不同。长查询 vs 短查询短查询要求低延迟长查询追求高吞吐不能混在一个队列。关键业务 vs 探索分析关键业务需要保障探索分析可以容忍失败和排队。同步查询 vs 异步查询同步查询要快速返回异步查询可以慢慢跑。在 Doris 和 StarRocks 里这个思想对应的是Workload Group在 Presto 里是Resource Group在 Spark 里是调度池 动态资源分配。你可以按业务线、按查询类型、甚至按用户名划分工作负载组每个组再设置资源上限、优先级、并发数。我自己的习惯是任何新业务接入 OLAP 前先回答一个问题——它属于哪类工作负载、允许的最大查询时间是多少、最多占集群多少比例资源。答不上来的业务接入后大概率会出问题。3. 调度策略从防崩溃到提效率3.1 三类主流调度策略先说调度器层面。YARN 的 Capacity Scheduler 和 Fair Scheduler 是 Hadoop 生态最常见的选择理解这两个就够用了。Capacity Scheduler是典型的队列配额制。你把集群总资源按比例分给多个队列每个队列内部再按 FIFO 或优先级执行。好处是配额隔离坏处是空转浪费——某个队列没任务时它的配额别人不能占除非开启“弹性队列”。Fair Scheduler是动态公平调度。它不固定配额而是按任务数动态分配让所有运行中的任务尽量获得均等资源。好处是利用率高坏处是单任务的稳定性差任务数增多时每个任务获得的资源都会下降。传统大数据面试题里喜欢问这两个的区别本质上就是在考隔离性和弹性之间的权衡。但我得说一句在现代 OLAP 引擎中YARN 调度器的作用更多是资源池管理真正的查询级调度是由引擎自身完成的。比如 Presto 的 Resource Group、Doris 的 Workload Group、ClickHouse 的处理线程池。底层的 YARN 只解决“谁可以坐在这张桌子上”引擎才解决“这张桌子上谁先吃哪道菜”。3.2 优先级、配额与公平性调度策略设计的核心是在三个要素之间找平衡优先级、配额、公平性。优先级控制的是“谁先获得资源”。最常见的有三种FIFO先来先服务实现简单但长任务会阻塞短任务。公平调度所有任务按比例摊资源短任务不会被长任务卡死但关键任务无法获得比普通任务更多的资源。加权公平/加权轮询按权重分配资源高权重任务获得更多时间和资源配额这是现代 OLAP 引擎的主流方案。配额控制的是“谁最多能用多少”。优先级决定顺序配额决定上限两者结合才能既保证关键任务优先又防止关键任务过度消耗集群。公平性不只是任务之间的公平还包括用户和团队之间的公平。A 团队一次性提交 100 个查询B 团队只提交 1 个查询如果完全按任务数量平分B 团队会被挤死。合理的设计是先按团队配额分资源团队内部再按优先级和公平性执行。我经历过一个反面案例某团队为了提升任务执行效率把他们的查询优先级全部设为最高。头一周效果很好第二周其他团队集体投诉响应变慢。后来的解决方案是最高优先级每天只有一定的配额用完自动降级。这就说明优先级不能脱离配额存在否则高并发的高优先级任务会挤掉所有其他任务。3.3 动态调度弹性与稳定性之间的平衡动态调度是这几年比较火的方向核心思路是不是每个任务都按固定资源规格提交而是根据任务的实际压力和集群负载动态调整资源分配。在 Spark 上动态资源分配可以做到executor 空闲 60 秒以上自动释放新任务到来时按需重新申请。配合外部 shuffle serviceexecutor 的增减不会影响正在运行的任务。Presto 和 Trino 的动态资源组管理则更细腻可以在运行时调整某个资源组的 CPU 权重和内存上限起到“限流”的效果。比如大促期间把高优业务的并发数放宽同时把非核心查询的内存限制收窄。弹性调度的核心矛盾是弹性越强集群稳定性越差。因为资源释放和申请有延迟频繁扩缩会导致任务启动等待时间变长甚至因为资源突然被抢占而执行变慢。我的建议是先做分级再做弹性。把任务分成核心、稳定、弹性三档。核心任务固定资源稳定任务在一定范围内波动只有弹性档允许大幅扩缩。这样至少能保证核心链路不抖动。4. 主流OLAP引擎的资源隔离实现对比4.1 Presto/Trino资源组与内存限流Presto/Trino 是 OLAP 引擎中资源隔离设计最成熟的。它提供了Resource Group机制支持三个维度的限制并发限制资源组内同时运行的最大查询数。内存限制组内所有查询的累计内存上限超限的新查询排队。调度权重组之间的 CPU 调度优先级权重。实际使用中Resource Group 的配置文件是 JSON 格式可以通过 orchestrator 动态调整而无需重启集群。这是我们当时为什么选 Presto 作为即席查询引擎的关键原因之一。Presto 的一个难点是内存的管理模型。它区分 JVM 堆内和堆外内存query.max-memory-per-node 控制单个查询在单节点上的内存query.max-total-memory-per-node 还包含了堆外内存。后者经常被忽略导致查询触发了堆外淘汰。配置时需要留足系统开销。4.2 Doris/StarRocksWorkload Group与查询排队Doris 和 StarRocks 的Workload Group特性才是真正对标的“资源隔离”功能。一个 Workload Group 可以独立配置CPU 权重和 CPU hard limit开启 enable_workload_group_cpu_hard_limit 后可以硬限制 CPU 使用率。内存百分比上限。查询并发数上限。查询排队和队列长度。在 2.x 版本之后StarRocks 的 Workload Group 还支持了对数据导入和查询的分离。你可以把实时导入放进单独的 group让导入和查询互不干扰。这对典型的实时数仓场景帮助很大。Doris 早期版本更依赖用户的 resource tag 来划分节点资源也就是将不同类型的 BE 节点打标查询按标签路由到不同节点。这种物理隔离最彻底但成本高。现在的 Workload Group 是逻辑隔离结合两者效果更好。4.3 ClickHouse用户配额与线程池限制ClickHouse 的资源隔离策略和传统 MPP 引擎不太一样它更侧重于用户级配额Quota和线程池限制。每个用户配置文件users.xml 中 profile可以设置profiles readonly_queries max_memory_usage10000000000/max_memory_usage max_execution_time60/max_execution_time max_threads16/max_threads max_result_rows10000000/max_result_rows /readonly_queries /profiles注意ClickHouse 默认情况下没有全局的“查询队列”机制。并发过高时查询会在处理线程池里排队等待。设置max_threads太高会使单查询占用过多 CPU太低则浪费硬件。经验值是单机查询并发控制在 vcore 数的 2 倍以内比较安全。ClickHouse 还支持异步插入和 Kafka 表引擎这些写入任务需要单独设置线程池和内存限制。我们曾经因为写入 merge 和查询抢占资源导致查询延迟上升后来通过给 merge 单独配低线程数才解决。4.4 Spark SQL在OLAP场景下的资源分配Spark SQL 做 OLAP通常不像 Presto 那样面向低延迟场景更多是重 ETL 和明细层加工。它的资源隔离主要靠YARN 队列 动态资源分配dynamicAllocation。核心配置spark.dynamicAllocation.enabledtrue spark.dynamicAllocation.initialExecutors5 spark.dynamicAllocation.minExecutors2 spark.dynamicAllocation.maxExecutors50 spark.shuffle.service.enabledtrue其中shuffle service 必须开启否则 executor 动态释放时未完成的 shuffle 数据无处存放。Spark 的 FILO 和 FAIR 调度器也很值钱。FAIR 调度模式下可以按池子配置 maxResources 和权重实现多团队共享一套 Spark 集群。我实际用的方式是把 Spark 集群按业务线划分到多个 YARN 队列然后在队列内开启 dynamicAllocation同时设置相对保守的 maxExecutors 峰值上限避免某个业务把自己跑满而影响其他队列。4.5 引擎特性对比速查引擎资源单元隔离粒度排队策略动态调整Presto/TrinoResource Group查询级按内存上限排队/拒绝支持运行时调整Doris/StarRocksWorkload Group查询/工作负载级并发和内存排队支持调整部分参数需重启ClickHouse用户配额 线程池用户级线程池排队改配置生效用户级Spark SQLYARN 队列 动态分配应用级队列配额动态扩展executor 自动扩缩这张表不是绝对标准每个产品线版本差异很大。但你可以用这个框架去评估自己使用的引擎它隔离的粒度是什么排队机制是什么运行时能否动态调整这三个问题的答案基本决定了一套系统的资源管理思路。5. 落地一套调度与隔离方案的全过程5.1 先定义SLO和业务等级任何资源隔离方案如果脱离业务目标都会变成“为了隔离而隔离”。我的落地路径第一步永远是写 SLO形式很简单P0 业务例如线上看板、核心报表目标 P99 延迟 3 秒月可用性 99.9%不允许多重失败。P1 业务例如分析师常规查询目标 P90 延迟 30 秒不长期阻塞即可。P2 业务例如临时探索、研发联调要求最低允许排队和失败。有了这个分级你才算有资格去设计资源配额。配额比例通常 P0 占 40%~50%P1 占 30%~40%P2 占 10%~20%。不要试图做到物理隔离式的绝对公平那不是 OLAP 场景的目标。SLO 定义中容易犯的错误是只写延迟目标不写吞吐和并发峰值。两个都要有否则大促期间业务方要求“所有查询 100% 成功”你整个集群直接停机保护才能满足。后来我们约定豁免条件单业务并发超过某个阈值时SLO 自动失效这比硬扛靠谱。5.2 配额、优先级与排队的设计设计资源隔离规则时我的原则是从内到外先做引擎级隔离再做底层资源池隔离最后接入层控制。以我们的 Presto 集群为例先建 Resource Group给 P0/P1/P2 各建一个组配置并发上限和内存上限。再在接入层我们用的是统一的 SQL Gateway按用户和业务解析出应该进哪个资源组传递到 Presto 的 session 属性中。最后在 YARN 层面确保 Presto 工作节点本身只使用该集群配额避免和 Flink、Hive 混跑。排队策略我通常选择“低并发 短队列”而非“高并发 长队列”。原因很简单高并发会让每个查询都变慢用户体验更差短队列可以让多余请求快速失败业务方就能第一时间知道系统有压力而不是傻等超时。具体参数需要压测观察不同并发下查询延迟的拐点。基于拐点的 60%~70% 设置并发上限预留一些突发空间。队列长度不超过并发上限的 3 倍超出直接拒绝。5.3 压测与参数调优资源隔离方案上线前必须压测否则你根本不知道参数是不是合理的。压测的核心是复现真实负载而不是用脚本打满 CPU 看集群什么时候跪。我常用的做法从真实查询日志中抽取过去 30 天的查询分布按类型、表、延迟、内存占用保留 1% 的大查询、5% 的中等查询、94% 的小查询。使用开源的压力工具或自研脚本按比例并发回放。逐步加压记录查询延迟、失败率、内存使用率三个指标。找到失败率和延迟突然抬升的“临界并发数”。在临界值基础上放宽 20%~30% 冗余设置为正式并发限制。压测中最容易出问题的是“典型查询”选取不当。如果你选的都是 10 毫秒返回的简单查询压出的并发上限会虚高。真实环境里偶尔会有全表扫描的疯狂查询必须把这类查询也纳入压测样本。调优的时候不要一次性改多个参数。我们改参数的习惯是“一次只改一个变量”改完压测一轮对比延迟和失败率。否则多个变量同时变化你根本定位不到问题出在哪。5.4 监控、告警与复盘资源隔离方案上线后监控是第一优先级。至少要覆盖以下指标引擎层各资源组的查询数、排队数、失败数、平均延迟、内存使用率、CPU 使用率。任务层每个查询的资源组归属、等待时间、执行时间、所耗内存。宿主层节点内存、CPU、IO 等待、磁盘使用率、GC 频率。告警规则不要只设“查询失败率超过 10%”这种一刀切。更精准的做法是分资源组设置不同的告警阈值P0 组延迟超过 3 秒即告警P2 组可以放宽到 60 秒。告警要带上资源组的名字这才能快速定位是哪个业务线出了问题。复盘这一环很多团队直接省略。我强烈建议每周抽时间去翻一遍排队和拒绝的查询日志看看有没有反复被拒的查询、有没有可以优化的慢 SQL。很多资源问题根因不全是资源不够而是有一条极度低效的查询反复提交。把这条 SQL 优化掉比加十台机器还有用。6. 常见问题与排查技巧6.1 配了内存限制却仍然OOM这是最常见的问题。原因是“内存限制”通常只作用于查询引擎的内存计算但操作系统层、JVM 堆外、网络缓冲、排序溢出文件都可能产生额外内存。排查方向查看容器监控中的 RSS 内存是否持续超过配置值检查是否开了太多的 concurrent 查询导致叠加超出确认引擎的 spill 机制是否开启比如 Presto 的 task-level spill、Doris 的 spill 目录配置。解决方案通常是三层同时调整降低单查询内存上限、增加引擎外的预留内存比例、把同一节点的最大并发数降下来。6.2 查询排队时间长但响应依旧慢资源组里排队数少的时候查询提交后仍然慢这时候大概率不是资源隔离问题而是某个大查询占用了较多资源导致其他查询被压制。我用过非常有效的定位方法在查询管理界面中找到所有正在运行的大查询逐个看它们所属的资源组和内存使用。假如是 P2 组的全表扫描查询把节点 IO 打满那 P1 组的查询不可能不慢。治理手段是给 P2 组设置更严格的 CPU 和内存上限必要时直接按行数限制查询结果集。但也要注意如果所有组都慢很可能是物理资源真的不够了。判断标准是看节点 CPU 在非查询高峰期的负载如果 idle 时间低于 20%建议扩容而不是调参。6.3 紧急任务抢不到资源业务方经常要“紧急捞数”如果资源组的优先级和配额设置太刚性紧急任务就永远抢不到资源。这种情况不能靠实时调参因为人的反应速度赶不上查询提交速度。我们的解决方案是建一个“紧急通道”资源组这个组的资源配额平时很低甚至为零但允许通过提交方凭证临时借调资源。每次借调有熔断机制紧急组的总内存不能超过集群容量的 10%超过则拒绝部分任务。这样可以保证紧急任务能启动又不会失控。另外一个经验紧急任务发起人必须报备系统记录提交者、业务、查询影响范围。这样出了事能追溯也能倒逼业务方思考是真的紧急还是平时舍不得排优先级。6.4 集群资源利用率飘忽不定资源利用率低很多人的第一反应是调度策略还不够激进于是把并发调大结果利用率上来了延迟也上来了。这就是没有把 SLO 和调度参数绑定导致的。利用率波动大时先看时间维度。OLAP 的业务高峰和低谷通常有明显规律一天内早上十点和下午两点是高峰凌晨是低谷。合理做法是配置不同时间段的调度策略。比如 Doris 的 Workload Group 支持通过外部计划任务动态修改配置白天限制分析查询并发晚上放开给 ETL 任务。如果一天内的利用率都低那问题往往出在工作负载本身不够而不是调度器不行。这种情况下优先考虑合并小任务、减少冷数据查询次数甚至考虑缩减集群规模别为了利用率而强行造负载。7. 资源隔离与调度之外的思考资源隔离和调度策略做到最后你会发现它不只是技术问题更是组织管理问题。技术方案再先进如果业务方没有合理的资源使用规范依然会出问题。我分享几个非技术但非常关键的经验。第一资源使用要“可视化”。给业务方提供一个自助查询工具能看到自己的查询用了多少资源、排队多久、有哪些慢查询。当业务方自己能查的时候很多“为什么我这么慢”的反馈会减少一大半因为他们自己就能定位问题。第二预算化管理。每个季度和业务团队对齐预算量化“我们这个季度预计消耗多少查询资源、允许失败率多少”。预算制的效果是业务方在提交大查询之前会主动评估必要性而不是无脑全量跑。第三定期做资源治理。轮询所有注册到 OLAP 引擎的业务方下线长时间不用的查询任务和用户权限。旧业务下线后遗留的定时任务是资源黑洞的常见来源。我们有一次清理掉了二十多个无人认领的定时报表直接释放了大约 15% 的集群资源。第四培养“先看计划再提交查询”的团队习惯。让使用方在上线查询前先看执行计划确认分区过滤、join 顺序、扫描行数都是合理的。很多资源浪费在第一次提交就注定了事后怎么调调度策略都是亡羊补牢。还有一个我自己摸索出来的小技巧给查询打上版本号或标签。所有接入 OLAP 的查询在 SQL 前加上描述性注释比如业务方、目的、期望返回时间。排查问题、做资源审计时这些注释能帮你迅速定位到责任人而不是去猜这个查询是谁提交的。技术层面的资源隔离和调度本质上是在一个共享系统里为不同需求划定边界并为这些边界建立合理的仲裁规则。没有一套参数可以通吃所有集群你必须基于自己的业务特征、团队协作模式、甚至业务方的技术能力来定制方案。但底层的思考框架是稳定可复用的先定 SLO再定优先级和配额再设计排队和拒绝最后用监控和复盘把方案打磨到稳定。只要这套框架立住了无论你用的是 Presto、Doris、ClickHouse 还是自研引擎都能设计出一套适合自己的资源治理方案。回到开头那个事故。后来我们重建了资源隔离体系把报表、即席、探索三类查询彻底分池限制了大查询的内存上限也把调度策略改成了加权公平。再遇到类似的全表扫描查询它只会把自己所在的资源组拖垮核心报表可以稳定不受影响。我到现在都记得那段时间每天盯着监控看 P0 组的延迟曲线一点点回归稳定时的感觉——那种从“天天救火”到“终于能睡个整觉”的转变就是资源隔离和调度策略带来的最直观价值。