先讲一个我印象特别深的夜晚。监控大屏上,OLAP集群的CPU使用率像心跳骤停前的心电图一样,一条直线直接冲到100%。紧接着,运营后台的报表接口全部超时,数据产品那边的同事在群里连发好几个问号,领导在电话里问“集群是不是挂了”。我们冲到机器上看,发现是一个分析师跑了一条没有加分区条件的SQL,一次性扫描了几百GB数据,吃掉了几乎所有CPU核心。这不是第一次发生,但那次之后我终于想明白了一个问题:OLAP集群缺的不是机器,不是优化器,而是资源隔离和调度策略。
这篇文章就围绕“大数据OLAP中的资源隔离与调度策略”来聊,讲清楚为什么要做隔离、从哪些维度做、调度策略怎么设计、落到配置上怎么配、怎么验证效果,以及我在实际运维中踩过的坑。适合大数据平台工程师、OLAP引擎的运维人员,以及正在设计数据中台底座的技术负责人参考。
1. 事故复盘:为什么一条SQL就能拖垮整个集群
1.1 一条“坏邻居”查询引发的雪崩现场
那次事故的根因并不复杂。集群里跑着一套OLAP引擎,所有查询共享同一份计算资源。白天高峰时段,在线报表查询、即席分析、定时数仓任务全都挤在同一个资源池里。正常情况下,单个查询也就用几十毫秒到几秒,大家相安无事。可一旦有人发起一个大查询,比如把整张事实表拉出来做聚合、做大表Join,整个集群的CPU就会瞬间被打满。
OLAP引擎为追求性能,默认会让一条查询尽可能利用多核并行,把数据切碎后分给几十个线程同时处理。如果一条查询扫描的数据量是百GB级,它会毫不犹豫地占满几十个核心。这时候,其他查询不是排队等待,而是连CPU时间片都抢不到,延迟从几十毫秒暴涨到几十秒。更糟的是,连接池也被占满,新进来的查询直接超时失败,监控里看到的就是一块块的红色告警。
这就是典型的“坏邻居问题”:一个查询把共享资源耗尽,所有查询一起遭殃。问题出现后,我们第一时间杀掉那条慢SQL,集群在几分钟内恢复,但业务侧的损失已经造成了。
1.2 资源隔离要解决的本质问题:在共享与稳定之间找平衡
你可能会说,那就禁止大查询呗。不行,OLAP的价值恰恰在于能对海量数据进行灵活分析,大查询是刚需。问题不在于查询本身,而在于没有给不同查询划定边界。
资源隔离要解决的本质问题,一句话概括:在资源高度共享的集群里,让不同业务、不同优先级的工作负载互相不影响。它不追求让每条查询都跑得最快,而是保证高优业务的延迟稳定、低优任务不至于饿死、集群整体利用率维持在一个健康水位。
拿办公楼用水来类比:整栋楼共用一根主水管,有人拧大水龙头,其他人水就变小甚至没水。资源隔离就像给每层楼装上限流阀、加装蓄水箱,或者干脆把用水额度提前分配好。你想多用水可以,但不能影响邻居的正常使用。
1.3 OLAP与OLTP在资源控制上的本质差异
做资源隔离之前,先要理解OLAP和OLTP的资源特征完全不同。OLTP事务短小、高频,每次操作涉及的数据量很小,主要靠行锁、事务隔离级别来控制并发,资源隔离的重点在数据库内部的锁和日志机制,单个事务几乎不可能占满整台机器。
OLAP恰恰相反。一条查询动辄扫描几亿行、几十GB数据,运行时间从几秒到几十分钟都很常见,内存用于哈希表、排序缓冲区、Shuffle中间结果,CPU并行度可以拉到几十核。这种“重量级查询”更接近批处理作业,需要的隔离手段是CPU配额、内存上限、并发插槽、队列权重、IO限速,而不是行锁和事务隔离。
所以,做OLAP的资源治理,必须换一套思路:不要指望“数据库自己控制好”,而是要从工作负载管理的角度,主动给查询分门别类、划定边界。
2. 资源隔离的四个关键维度:CPU、内存、IO与连接数
2.1 CPU隔离:线程池配额、核心绑定与用量上限
CPU是OLAP查询最核心的资源,也是最先被打满的资源。CPU隔离的常见手段有三种。
第一种是线程池按资源组分配。很多OLAP引擎支持把不同队列的查询路由到不同的线程池,或者给线程池设置并发上限。这样即使低优查询再暴力,也最多占用线程池里固定的几个执行线程,不会影响到高优查询所在的线程池。
第二种是CPU时间片配额,最典型的实现是Linux cgroup的cpu子系统。可以给某个资源组设置cpu.cfs_quota_us和cpu.cfs_period_us,限制该组内所有进程每100毫秒最多使用多少CPU时间。例如period设为100000,quota设为50000,含义是该组最多使用一个CPU核心的50%时间片。如果要限制使用4个整核,quota设为400000。这里的重点是,限额不等于核数,cgroup控制的是时间片比例,好处是低峰期配额空闲时可以临时借用,坏处是如果配置不合理,会出现“CPU明明没占满但查询变慢”的假象。
第三种是NUMA绑定和CPU pinning。把某个资源组的查询固定绑定到特定的物理核心上,避免跨NUMA访问内存带来的性能抖动。这种方式最硬核,但在虚拟化环境和云上实例里不一定生效,需要先确认底层CPU拓扑。
我的建议是:线上环境优先用线程池加cgroup配额组合,绑定CPU核数作为极端隔离场景的补充。
2.2 内存隔离:硬限制、软限制与溢出路径
内存是比CPU更危险的资源。CPU打满顶多查询变慢,内存超限直接OOM,进程被杀,甚至引发集群雪崩。OLAP查询的内存消耗大头有三个:聚合和Join的哈希表、排序操作的临时缓冲区、查询执行框架的并行中间结果。
内存隔离分三层做。第一层是查询级别限制,大多数引擎都支持设置单条查询的最大内存,比如ClickHouse的max_memory_usage、Doris/StarRocks的query_mem_limit。超过就报错终止,这是最硬的兜底。第二层是用户或资源组级别限制,比如某个低优队列的内存总配额是64GB,这个队列里所有查询共享这份额度,谁先申请谁先用。第三层是进程或者容器级别的内存上限,用cgroup的内存子系统或者K8s的limit来做,防止该组查询把整台机器拖垮。
这里有个很重要的概念:硬限制和软限制。硬限制是一旦超过直接终止或者拒绝查询,保护效果最强,但用户体验很差——大查询可能跑了一个小时最后报错,浪费了之前的计算。软限制则是一旦接近阈值,引擎会主动降速、降低并行度、或者触发部分数据落盘,尽量让查询继续跑完。
配套必须做的是溢出路径。内存受限的查询应该能把临时数据写到磁盘,而不是直接报错。但这会引入新的问题:落盘文件的读写会占用磁盘IO,如果同一个资源组里同时有多个查询在落盘,IO就变成了新的瓶颈。所以,配置内存隔离的同时,一定要考虑IO隔离。
2.3 IO隔离:磁盘带宽与网络流量的控制难点
IO隔离是四个维度里最容易被忽略、也最难做好的。OLAP查询的典型IO行为有两个:扫描阶段的大量顺序读,以及Shuffle/落盘阶段的顺序写加随机读。如果一群大查询同时扫描不同的分区,磁盘带宽会被吃满,即使是高优查询,所有数据读取都会在磁盘那里排队。
磁盘IO隔离的常用手段有:用ionice给不同进程设置IO优先级;用cgroup的blkio子系统限制某组进程的读写带宽和IOPS;在引擎层面控制扫描并发和扫描量,比如通过分区裁剪、强制采样、限制读取列数来减少不必要的IO。
实际运维中,我遇到过更隐蔽的问题是“慢盘效应”。存储节点上如果有一两块磁盘性能下降,查询一旦落到这些节点上,整个查询的耗时就会被拖长。这个问题靠资源隔离解决不了,只能靠监控及时发现坏盘并摘除。
网络IO同样值得关注。大查询的Shuffle阶段会产生大量网络传输,尤其是Presto/Trino这类MPP引擎,几十GB的中间结果在节点之间搬来搬去,网卡打满后所有查询都会受影响。做法通常是给不同资源组设置不同的Shuffle并发度,或者在网络层做流量整形,但后者在普通物理机环境里配置成本较高,很多时候依赖引擎自己的调度避免“全员大Shuffle”。
2.4 连接数与并发隔离:从源头限制查询数量
一个很容易被忽略的维度是并发量。很多资源问题不是单条查询吃太多资源,而是同一时间有太多查询叠在一起。引擎的线程池和连接池处理能力是有限的,一旦并发数超过阈值,即使CPU和内存都没到瓶颈,新查询也会因为等待线程或连接而超时。
连接数隔离可以在引擎侧配置,给每个资源组设置最大并发查询数,比如高优队列允许同时运行50条查询,低优队列只允许同时运行10条。超过部分直接排队,排队的查询可以设置最大等待时间,超时后返回“队列繁忙”的提示,而不是放在后台无限期挂着。
并发隔离还有一个好处:排队比慢死更容易让人接受。用户看到的是“前面还有3个任务在排队,预计等待20秒”,比请求发出去后一直转圈、最后超时失败,体验上要好得多。我把这点看得很重,因为资源隔离不只是技术问题,也是产品体验问题。
3. 调度策略的设计思路:从FIFO到多级队列的演进
3.1 为什么简单的FIFO不够用
资源隔离解决的是“谁能用多少”的问题,调度策略解决的是“谁先谁后、怎么排队”的问题。最早期的OLAP调度策略就是简单的FIFO,谁先到谁先跑。这种方案在小团伙、单业务的场景里没毛病,一旦业务多元,问题立刻暴露。
FIFO最大的毛病是“队头阻塞”。一个需要跑30分钟的大查询排在队列最前面,后面如果有上千个只需要跑1秒的小查询,全部得等那30分钟。这和大水漫灌没什么区别,高优业务的SLA根本保不住。
另外,FIFO没有任何“公平”的概念。同一个队列里有几个部门的任务,A部门的查询量是B部门的几十倍,B部门的查询就会被不断挤到后面,虽然它们先提交。所谓公平,在这里其实变成了“谁量大谁有理”。
3.2 多级队列与权重调度:核心思想与参数设计
现代OLAP引擎和调度框架,普遍采用多级队列加权重的设计方案。核心思想是:先把整个集群的资源切分成多个队列,每个队列有自己的最小资源保证和资源上限,然后把用户或查询绑定到某个队列,引擎在队列之间按权重分配剩余资源。
举一个典型的配置思路:
- 根队列下有3个子队列:高优生产队列、常规分析队列、低优批量队列。
- 高优队列最小保证40% CPU资源,上限60%;常规队列最小30%,上限50%;低优队列最小10%,上限30%。
- 三个队列的总上限超过100%,这是允许的,因为低峰期有队列空闲时,其他队列可以临时借用资源,这就是所谓“弹性超用”。
多级队列设计的关键在于:最小保证是硬承诺,资源上限是安全阀,权重决定空闲资源的分配比例。高优队列即使在集群繁忙时,至少也能拿到四成资源;而低优队列在资源充裕时也可以借用别人不要的算力,避免资源浪费。
调度策略不是越复杂越好。队列太多,每个队列的资源碎片化,集群整体利用率反而下降;队列太少,隔离粒度又不够。我见过比较合理的划分是5到10个队列之间,按业务线、按优先级、按计算任务类型三个维度来切。
3.3 优先级与抢占:谁有资格打断谁
队列模型解决的是资源分配比例,优先级解决的是“同一个队列里谁先跑”以及“资源不够时谁让路”。
OLAP场景里的优先级,一般分两种。一种是静态优先级,由提交任务的用户身份决定,比如平台管理员提交的运维任务优先级最高,临时分析用户的查询优先级最低。另一种是动态优先级,根据队列当前负载自动调整,比如队列排队人数多了就临时提高自己的优先级,防止饥饿。
比优先级更难设计的是抢占。抢占的意思是:高优任务进入队列时,如果系统资源不足,强制杀掉或者暂停低优任务来腾出资源。这在历史悠久的大数据调度框架(比如YARN)里很常见,但在OLAP场景里要小心谨慎。
为什么谨慎?因为OLAP查询的中间状态大多在内存和临时文件里,强杀会导致这部分计算成果作废,如果查询已经跑了几十分钟,直接杀是对计算资源的巨大浪费。我在实践中更倾向于“先杀慢的、先杀大的”:当需要抢资源时,选择那些耗时最长、占用内存最多、且来自低优队列的查询,而不是杀刚刚提交的新查询。另一个折中方案是“禁止新提交”:队列资源满了之后直接拒绝低优查询进来,而不是启动后杀掉。
3.4 安全超卖:要利用率还是稳定性
资源利用率与稳定性之间永远存在张力。为了省钱,我们希望集群跑得越满越好;为了稳定,我们希望资源冗余越多越好。调度策略里有一个关键设计就是超卖比例。
我在实际运维中总结的经验是:CPU可以超卖,内存绝对不超卖,IO能不超卖就不超卖。CPU配额本质是时间片,短暂的高负载会表现为延迟上升,但不会直接崩溃;而内存是物理实体,超卖意味着一旦同时申请就可能OOM。IO超卖会导致磁盘延迟飙升,对集群整体影响也很明显。
具体到数值,我个人习惯把CPU分配总和控制在物理核数的1.5到2倍之间,内存分配总和控制在物理内存的80%以内,剩余的内存留给系统缓存和引擎自身的开销。这个比例不是死的,要结合具体业务的峰值特征动态调整。
4. 一套可落地的隔离调度方案:配置、指标与观测
4.1 先定义场景目标,再动手改配置
不少同学一上来就想把引擎的各类资源限制参数全部配上,结果配了一周还不知道有没有效果。我的建议是先明确场景和目标,再决定配置项。
假设这样一个典型场景:一个中等规模的OLAP集群,承担两类业务。一类是BI报表和在线数据产品查询,特点是并发高、单查询数据量小、延迟要求高,这是高优业务。另一类是数据挖掘和离线分析,特点是查询量大、跑得久、不要求秒级响应,这是低优业务。当前的问题是,第二类业务偶尔会跑一个巨型查询拖垮整个集群,导致第一类业务掉链子。
这个场景下的目标就很清晰了:高优查询的P99延迟控制在1秒以内,低优大查询允许跑30分钟以上但不许影响高优查询的正常响应;整体集群资源利用率保持在50%到70%之间。有了目标,下面每一条配置都要围绕这个目标来验证。
4.2 资源组与队列的具体配置思路
以一套通用配置来说明。先建两个资源组,一组叫bi_high,绑定BI报表业务账号;另一组叫ad_hoc_low,绑定临时分析账号。
bi_high组的关键配置项:
- 最大并发查询数:50。
- 单查询内存上限:8GB。
- 组内存总上限:集群总内存的40%。
- CPU权重:高,或者在cgroup里分配整机50%以上的时间片配额。
- 队列优先级:最高,进入即执行,不参与抢占。
ad_hoc_low组的关键配置项:
- 最大并发查询数:10。
- 单查询内存上限:32GB。
- 组内存总上限:集群总内存的30%。
- CPU权重:低,时间片配额不超过30%。
- 队列优先级:低,大查询限时,超时自动降级或终止。
这套配置的核心逻辑是:高优组用“低内存上限+高并发数”来服务大量短查询,保证吞吐和延迟;低优组用“高内存上限+低并发数”来服务少量大查询,允许它们吃内存但不能同时开太多。
配置生效后,最需要留意的参数是max_concurrent_queries和内存总上限之间的配合。如果并发设得高但内存总上限小,一有查询突发就会触发内存拒绝,业务方会觉得“集群是不是坏了”;如果并发设得低,可能高优组的查询根本跑不满,白白浪费资源。
4.3 慢查询熔断与审计:治理不能只靠隔离
资源隔离和调度策略是防御机制,但总有配置cover不到的角落。我的经验是,必须配套做慢查询熔断和审计。
慢查询的阈值怎么定?先收集一周的查询日志,统计每个查询的执行时间分布,找到P95和P99对应的耗时。把P95的三倍作为“可疑慢查询”的阈值,把P99的五倍作为“必须熔断”的阈值。举例来说,如果P95是5秒,那么超过15秒的查询需要被标记并通知申请者,超过25秒的查询直接终止并告警。
熔断策略不能一刀切。对于低优队列的查询,可以设置30分钟硬超时;对于高优队列的查询,一般不设运行时超时,而是靠资源隔离让它们始终有资源可用,避免出现误杀。另外,熔断的同时要自动记录查询ID、来源账号、SQL摘要、扫描行数、消耗内存,这些数据既是跟业务方沟通的证据,也是后续调优的依据。
4.4 监控哪些指标,才能证明隔离生效
配置做完之后,监控比配置本身更重要,因为只有监控能告诉你隔离有没有真正工作。需要盯的核心指标有这么几类。
第一类是资源使用率按队列拆分。不能只看整机的CPU和内存,要把每个资源组的CPU使用率、内存水位、并发查询数单独拆出来画曲线。如果高优组的CPU已经到80%,但低优组只有10%,说明权重分配可能不合理;如果低优组长期贴着上限跑,说明业务方需要扩容或调整配额。
第二类是查询延迟分位数。高优查询的P50、P95、P99要分开监控。隔离生效的标志是:即使低优队列里有十几个大查询在跑,高优查询的P99也只允许有轻微波动,而不是数量级暴涨。
第三类是排队和拒绝情况。每个队列的排队长度、平均等待时间、被拒绝的查询数量,这些指标直观反映调度是否健康。排队过长说明并发限制太紧,被拒绝太多说明内存或连接配额不够。
监控告警建议设置两级:一级是队列资源水位超过85%,持续5分钟,通知值班工程师;二级是高优查询P99超过目标阈值2倍,持续10分钟,需要立即介入。
5. 验证与压测:怎么证明隔离策略真的有效
5.1 构造可控的“坏邻居”场景
配置改完之后,最怕的是“感觉好像没问题了”,但没有量化数据支撑。正确的做法是专门设计压测场景来验证资源隔离的效果。
找一个业务低峰期,准备两类查询。坏邻居查询选一个会扫描全表的大查询,或者用脚本并发提交20个大查询,目标是把CPU和内存都压到接近上限。守护者查询选一个典型的高优查询,比如线上报表常用的一条聚合SQL,统计它的单次执行耗时。
压测分三步走。第一步,只跑高优查询,记录基线数据,比如P99延迟是500毫秒。第二步,启动坏邻居大查询,让集群资源迅速被打满。第三步,在坏邻居运行期间持续提交高优查询,记录P99延迟。如果隔离生效,P99最多从500毫秒涨到800毫秒到1秒左右,而不是涨到10秒以上。
这个验证过程最好能自动化,写成一个脚本定期在低峰期自动执行,防止每次调整配置后都要人工跑一遍。
5.2 从压测结果反推阈值修正
压测结果如果不符合预期,不能盲目调大配额,要定位瓶颈在哪个资源维度。
如果高优查询的P99明显变差,先看CPU使用率:高优组所在线程池是否被打满?如果线程池满了,需要提高高优组的CPU权重或者线程池大小。再看内存:高优查询是否发生了落盘或者被拒绝?如果频繁落盘,说明内存配额太小,查询被逼到磁盘上,性能当然会劣化。如果内存充足、CPU也没打满,但高优查询还是慢,那就得怀疑IO了——大查询的扫描和Shuffle把磁盘带宽吃干净了,此时要么限制低优查询的扫描并发,要么从存储层做限速。
修正阈值的原则是:一次只改一个参数,改完重新压测。不要同时调整CPU配额、内存上限和并发数,否则出了问题根本定位不到是哪个环节引起的。
5.3 隔离配置的常见误区和自查清单
根据自己的经验,整理一个配置自检清单,分享给团队后很受用。
- 是否只配了CPU配额,没配内存上限和并发数?如果CPU限住了但内存不限,大查询依然可能把内存打爆,这是最常见的配置缺失。
- 是否只配了队列,没做用户绑定?如果用户没有归属到任何资源组,就会进入默认组,默认组通常不限资源,隔离形同虚设。
- 是否把低优队列的配额设成0?这样低优查询高峰期完全没资源,一旦业务需要跑离线分析,又反过来投诉“集群不让跑任务”。低优并不等于禁止,要留一个低水位保底。
- 是否配置了超时熔断但没设置掉落后的路径?熔断的查询如果直接报错,用户会反复重试,起不到保护作用。更好的做法是提示“查询太复杂,请缩小时间范围或添加分区条件”。
- 是否监控了排队长度但没监控排队等待时间?排队长度长不代表有问题,只要等待时间短就没关系;排队长度短但等待时间很长,说明队列里有一条查询卡住了。
6. 踩坑实录与一些掏心窝的建议
6.1 我踩过的几个高频坑
第一个坑是cgroup版本导致CPU限制不生效。有段时间发现低优队列的CPU配额形同虚设,查了很久才发现是系统用的cgroup v2和引擎默认配置文件不兼容,配额写进配置但内核没真正应用。后来统一排查了集群的操作系统版本,把cgroup相关配置全部对齐才解决。
第二个坑是忘了做用户绑定。配置完所有资源组之后,测试时发现大查询还是到处跑,结果发现新接入的业务账号掉进了默认组,默认组没有任何资源限制。从那之后,我要求所有账号创建时就必须指定资源组,宁可先给一个保守的临时配置,也不让任何查询落在默认组里。
第三个坑是线程池参数和并发参数叠加导致的“低并发”假象。有个引擎的线程池大小和资源组并发数是分开配置的,我以为把资源组并发调到100就够用了,结果线程池默认只有20个执行线程,高优查询大量排队。参数之间不是独立生效的,配置前一定要把引擎的线程模型彻底搞清楚。
第四个坑是熔断阈值设得太激进。刚开始为了保高优业务,把低优大查询的超时时间设成10分钟,结果很多正常的分析任务跑到一半被杀了,业务方意见很大。后来把阈值改成基于查询历史的动态阈值,并且熔断前先通知,给了业务方一个缓冲期,才算平息。
6.2 资源治理不只是DBA的事,需要协作机制
做了一段时间资源治理,我最大的感受是:这不能只靠一两个DBA在配置中心里玩命调参。资源治理是平台能力,需要多方协作才能长期运转。
在平台侧,需要提供资源组申请和管理入口,让各个业务线能看到自己队列的配额和使用量。在业务侧,需要把查询按“生产任务”和“临时分析”分开,不同用途走不同队列。在开发侧,核心报表查询要主动设置合理的时间范围和分区裁剪条件,避免无谓的全表扫描。
我们还做了一个“查询账单”机制,每周给各业务方发一份本队列的资源消耗报告,包含总查询数、总扫描数据量、CPU和内存消耗占比。目的不是为了找茬,而是让业务方看到自己的资源消耗,倒逼他们优化SQL和模型设计。效果比想象中好,很多之前没人在意的全表扫描,业务方自己就主动修掉了。
6.3 派得上用场的长期演进方向
隔离和调度策略不是一次性工程,随着业务发展要持续演进。我现在比较关注三个方向。
第一个是工作负载感知调度。传统隔离策略根据用户和业务做静态分组,但不会判断查询本身的特点。未来可以结合查询历史数据,自动识别“这个查询是扫描密集型还是Shuffle密集型的”,然后动态调整它在队列里的资源配置,让隔离粒度细化到工作负载层面。
第二个是自动弹性配额。固定队列配额的好处是稳定,坏处是资源利用率有上限。如果能够根据实时负载,在保证最小资源的前提下,自动把闲置配额临时分配出去,既能保SLA又能提升利用率。现在很多云原生数仓已经在做类似的能力。
第三个是跨集群的调度协同。公司如果有多个OLAP集群,比如一个跑常规报表、一个跑实时分析,可以考虑把查询按优先级和资源需求自动路由到合适的集群,实现更大范围的资源调配,而不是每个集群各自为战。
做了几年OLAP资源治理,我最大的体会是,隔离和调度本质上是在用确定性的规则对抗不确定性的流量。宁可让查询多排一会儿队,也不要让它把整个集群拖下水。集群的稳定靠的不是单点的高配机器,而是每一条查询都知道自己的位置在哪里、能碰多少资源、超了会付出什么代价。每次看到监控大屏上高优查询的P99长时间保持一条平稳的曲线,低优任务也在自己的队列里安静地跑着,我就觉得这套体系建得值。最后再分享一个小技巧:业务模型变化之后,原有的配额配置往往会在两个月内失灵,所以每年至少做一次全量压测和配额复盘,比平时反复微调管用得多。