干K8s平台运维这么多年,最让我糟心的场景之一就是:业务方天天喊要扩容器,财务盯着多云账单喊降本,而我打开Grafana一看,集群平均CPU利用率常年只有13%上下。不是机器不够,是K8s资源调度这套机制天然就"按声明分配、不按实际用量分配"——每个Pod都往高里报Requests,节点账面满满当当,实际却大半时间在空转。要破这个局,混部技术是目前云原生社区公认最有效的路子:让在线服务和离线任务在同一批节点上错峰共生,把预留却闲置的算力重新利用起来。这篇文章我会从资源浪费的根因讲起,说清楚混部的核心原理、调度器和节点Agent的落地细节、资源隔离方案,再晒一组真实改造数据和踩坑经历,适合正在做K8s成本优化、资源治理的SRE和平台工程师。
1. 资源利用率困局:集群平均负载撑不过15%的真相
要谈混部,得先想明白一件事:集群算力到底浪费在哪了。很多人一开始以为要优化的是"机器不够用",真正上了监控平台才发现,问题恰恰相反,是"账面不够用,实际很空闲"。
1.1 从一台物理机看资源浪费:Requests背后的"虚高账单"
K8s调度器的调度依据,从来不是Pod实际用多少CPU和内存,而是Pod声明要多少(Requests)。Pod能占多大资源(Limits)只是配额上限,调度时看的只是Requests。这个机制本身没错,但集群规模大了以后,它就会形成一种典型的"账面挤压"效应。
举一个我经常用来跟业务方解释的例子。一台4核8G的节点,上面跑了一个requests=4C8G、limits=4C8G的业务Pod。实际运行一周后你看监控,它的CPU平均使用率只有0.3核,内存峰值也只有600M。因为账面上这个Pod已经把整台节点的资源都"包圆"了,其他任何Pod都没法再调度上来——哪怕实际上这台机器有九成算力在闲着。
这种情况非常普遍。业务方为了给流量高峰留缓冲,通常会把Requests按峰值负载的1.5到2倍去报。再加上多个团队各自拿着业务指标酌情加缓冲,集群层面的requests总量就被层层放大。算下来,集群平均CPU利用率能到20%都算健康,多数生产集群常年趴在10%到15%。我们内部做过统计,线上80个Deployment的集群,按requests计算的"虚拟利用率"能达到75%,按实际用量算的真实利用率只有13%。
这就是混部技术要撬动的最大空间:requests与真实用量之间的差值。资源利用率在K8s里并没有一个现成的仪表盘,通常我们用Prometheus和Kubelet的容器指标来算,公式很简单:节点CPU实际使用量除以节点CPU总量,再做集群聚合平均。算出来的数字越难看,说明可优化空间越大。
1.2 在线服务的潮汐效应与缓冲策略
在线服务的负载天然是潮汐式的。电商平台白天流量高,凌晨流量低;直播业务在开播和活动节点是高峰;支付系统在整点和小周期内有明显脉冲。但Requests声明是静态的,不会跟着潮汐起落。于是出现一个很矛盾的现象:夜间在线服务基本闲置,但整个集群的资源被requests锁死,Spark批处理任务等不到节点。
这并非某个团队的配置失误,而是保障SLO的系统性代价。对在线服务来说,P99延迟一旦被击穿就是事故。业务方宁可把requests拍到波峰值的两倍,也不愿接受任何"看起来有风险"的调度。结果是:每个团队都在自己的Pod上留了缓冲,多个团队的缓冲叠在一起,集群层面形成了一座巨大的"虚拟资源山"。这座山上没有任何实际负载,但调度器看不见,新Pod就是上不去。
要解决这个问题,光靠宣导"大家把requests填准一点"没有用。业务方不可能为了省成本放弃自身稳定性。正确的思路是:在线服务保留它的缓冲声明,但在调度和资源管理层面,能够识别这些缓冲其实是闲置的,并允许离线任务在运行中借用这部分闲置算力。这正是混部技术的出发点。
2. 混部技术的核心逻辑:在线和离线任务如何共处一室
2.1 混部到底在混什么:利用错峰与超卖回收"闲置算力"
混部(Colocation),简单说就是把在线服务和离线任务放在同一个K8s集群、同一批节点上运行。在线服务包括Web应用、微服务API、事务型数据库等,特点是延迟敏感,要求P99稳定;离线任务包括Spark批处理、Flink作业、AI模型训练、数据回填等,特点是计算密集且对延迟不敏感,晚跑几分钟甚至几小时都能接受。
这两类负载放一起,就能实现错峰共生:在线服务波谷期,离线任务把闲置的CPU和内存用起来;在线服务波峰期,离线任务让出资源或主动压制自己,保证在线任务SLO不受影响。绕开requests账面的限制、利用"闲置算力"跑批处理,本质上是给K8s资源调度引入了一层动态超卖逻辑。
打个比方。一台服务器就像一套房子,在线服务是常驻住户,它的房间(requests)需要有保证,但多数时间其实空着。混部相当于给这套房子装了智能门禁:住户不在房间时,允许临时租客(离线任务)进去用空间;住户一回来,门禁必须立刻把房间腾出来。关键是"立刻归还"这步,如果做不到,整个混部就是灾难。
2.2 为什么K8s原生调度做不了这件事:从Requests分配到真实供需博弈
K8s原生调度器解决不了混部,有以下几条硬伤。
第一,调度器只看静态声明。它把节点可分配资源(Allocatable)减去所有已调度Pod的Requests之和,来判断节点是否还有余量。它永远不知道实际利用率是多少,自然无法判断哪些Requests是虚高的。
第二,缺少跨QoS的资源压制能力。原生虽然把Pod分为Guaranteed、Burstable、BestEffort,但一旦调度上去,CPU和内存的竞争基本是"谁抢到算谁的"。原生机制允许高优Pod挤压低优Pod,但混部需要的是双向动态调整:在线任务资源用不完时,离线任务可以把资源拿走;在线任务负载上来时,又随时把资源收回。这种"动态借用"关系原生调度器完全hold不住。
第三,缺少回收机制。在线任务释放出来的闲置资源,谁来发现、谁来统计、谁来显式分配给离线任务?kubelet只知道按cgroup约束让Pod不超限,不会主动利用空闲资源。要落地混部,必须在调度器和节点代理两层同时扩展。
所以混部方案通常是一个控制面组件加一个节点Agent的组合:控制面负责把离线任务调度到有闲置资源的节点,节点Agent负责动态感知真实负载并执行压制和回收。Koordinator是阿里开源后捐给CNCF的混部项目,也是目前社区里这套能力最完整的实现,Volcano也能做类似的批调度。后面我会以Koordinator为例讲落地细节,因为它在资源隔离和干扰检测上的设计更贴近混部场景。
3. 落地混部调度:QoS分级、超卖控制与节点Agent
3.1 QoS分层设计:从Guaranteed到Batch的优先级阶梯
先明确一点:K8s原生QoS只有三级,混部场景远远不够用。你没法只靠Guaranteed/Burstable/BestEffort来表达"这个在线服务是核心中的核心,必须永远不被挤压"和"这个在线服务可以容忍偶尔被挤一下"之间的差异。混部组件的第一个核心能力,就是提供更细粒度、支持抢占和压制逻辑的QoS体系。
以Koordinator为例,它把Pod的QoS扩展成了四档:
- System:系统组件,例如kube-system下的关键Pod。
- LSE(Latency Sensitive Exclusive):独享资源的延迟敏感应用,可以独占某些物理核或重资源。
- LS(Latency Sensitive):普通在线服务,允许与其他在线服务共享,但不允许被BE抢占。
- BE(Batch):离线批处理任务,只能使用在线服务闲置出来的资源。
调度器分配资源时,严格按照System > LSE > LS > BE的优先级排序。同一节点上如果BE任务和LS任务争抢CPU,BE必须让路。这个"让路"不是靠BE任务自觉,而是节点Agent强制执行的,后面细说。
实际操作中,给Pod打标很简单,在metadata里加一个label即可:
apiVersion: apps/v1 kind: Deployment metadata: name: order-service labels: koordinator.sh/qosClass: LS --- apiVersion: batch/v1 kind: Job metadata: name: spark-etl-job labels: koordinator.sh/qosClass: BE这里有个经验:不是所有在线服务都适合标LSE。我的建议是,核心API、消息队列、交易链路这部分标LSE,普通内部服务标LS,别一上来全标LSE——LSE太多会把混部空间挤没,因为LSE倾向独享节点资源。我见过有团队把所有Deployment全标成LSE,混部收益直接归零。QoS分层的目的是区分优先级,而不是让所有业务都抢最高档。
3.2 节点资源超卖与弹性限额:如何"安全地多卖"CPU和内存
混部能不能跑起来,关键在超卖模型。这里的"超卖"不是盲目地让节点放很多Pod,而是通过节点Agent动态计算节点"真实可分配资源",让调度器以此为依据,而不是死死盯着requests账面值。
CPU超卖相对安全,因为CPU是可压缩资源,挤了顶多拉长调度队列,最多让任务变慢,不会直接杀掉进程。通常混部会把CPU超卖率控制在1.5倍到2倍之间。Koordinator里的关键参数是超卖系数,节点Agent会根据在线Pod的实际使用量动态算出"可超卖容量",再把结果作为扩展资源上报给调度器。公式大致是:可超卖CPU = 节点Allocatable CPU + 超卖系数 × 在线Pod的Requests CPU - 在线Pod实际使用CPU。这个值不是固定的,每分钟都会刷新。
内存就不一样了,内存是不可压缩资源。一用完就是OOM,轻则杀掉BE Pod,重则把同节点的在线Pod也拖下水。所以混部对内存极其谨慎,默认策略是让BE Pod只能"捡"在线任务实际没用完的内存,超卖率最多1.2倍,还必须配合完整的回收和保护机制。极端情况下宁可淘汰BE任务,也绝不让它挤走在线任务。
建议的初始参数如下表,这是我跑了多轮压测后相对稳妥的组合:
| 资源类型 | 默认超卖系数 | 安全上限 | 关键保护机制 |
|---|---|---|---|
| CPU | 1.5~2.0 | 2.5 | CPU Suppress、CPU Burst |
| 内存 | 1.0~1.2 | 1.3 | memory.high、远程内存回收、OOM优先级调整 |
| 网络带宽 | 不超卖 | - | 带宽限流 |
| 磁盘IO | 不超卖 | - | IO权重控制 |
这里特别提醒一句:磁盘IO和网络带宽经常被忽略,但实际对在线服务干扰最明显的恰恰是这两个。BE任务在大量读写数据时,如果IO权重不限,在线服务的存储延迟会直线上升。混部完整方案里一定要把IO调度纳入进来。
3.3 节点Agent的作用:从静态调度走向动态回收
K8s原生kubelet只管按requests和limits给Pod建cgroup,它没有能力回应"实际剩余多少资源"这种问题。混部的第二个核心组件就是节点Agent(Koordinator对应的是Koordlet),它做三件事。
第一,采集真实用量。Agent会周期性地读取cgroup和系统指标,算出每个在线Pod的实际CPU使用、内存使用、网络和存储IO,整理成节点级的"真实剩余资源"。
第二,上报给调度器。Agent把"可超卖容量"和"动态剩余容量"以扩展资源的形式上报,比如koordinator.sh/batch-cpu、koordinator.sh/batch-memory,调度器调度BE Pod时只看这些扩展资源的余量。
第三,执行压制和回收。当在线任务负载上升、节点压力变大时,Agent会降低BE Pod的CPU配额,或者触发内存回收。这个动作不需要重新调度,秒级生效。
这第三点特别重要——在线任务负载飙升的瞬间,指望调度器把BE Pod迁走是不现实的,太慢。只有节点Agent能快速压制BE任务,给在线任务让出路径。所以混部系统的架构本质是:控制面管"计算",节点Agent管"执行"。K8s资源调度的优化,到这一步,实际上已经演变成"控制面调度 + 节点级动态治理"的协同架构了。
4. 性能保障与稳定性控制:压得住离线,护得住在线
混部改造最怕的不是技术方案选型,而是"上线后一个午高峰,P99延迟直接飙红"。压得住离线任务、护得住在线任务,比任何漂亮的调度策略都重要。这一章我把CPU、内存、干扰检测三块分开讲。
4.1 CPU资源隔离:为什么不能靠cgroup限流了事
很多人觉得限制离线任务CPU很简单:给它设个CPU limits,超出就throttle。真这么做,在线任务的延迟反而会受伤。原因是cgroup的CFS quota限流是"启动/停止"式的:容器用完配额后会被强制挂起,等下一周期再跑。这种反复挂起和换入换出会让CPU调度队列变得拥挤,增加上下文切换。在线任务如果和BE任务在同一核上竞争,P99延迟会显著恶化。
混部方案里更有效的CPU隔离是组合拳:
- 绑核(CPUSet):把BE Pod绑定到指定物理核上,不与在线任务抢核。绑核适合CPU密集型的BE任务,比如AI训练、大数据计算,效果最直接。
- 优先级权重:cgroup v2的cpu.weight可以设置在线和离线的CPU权重比例,在线任务权重远高于BE,调度器在竞争时优先满足在线任务。
- CPU Burst:允许在线任务在预留配额之外临时借用闲置CPU,弥补CFS周期性的短板。
- CPU Suppress:节点Agent发现CPU整体压力超过阈值时,直接压低BE Pod的CFS配额,把CPU让出来。
举个例子,一个运行电商核心API的Pod,requests=2C,limits=2C,平时可能只用0.5C,高峰期瞬时冲到2.5C。如果没有CPU Burst,2C的limit会直接限住它;有了Burst,它可以临时借用BE任务占用的CPU,节点Agent同时压制BE任务,保障这个API不被卡住。
我踩过的一个坑是绑核策略和NUMA的交互。BE任务如果绑在某个NUMA节点上,同时内存分配也落在同一NUMA,性能很稳定;但如果跨NUMA频繁访问内存,离线任务性能会暴跌,还会拖累同节点的在线任务。所以配置绑核一定要看拓扑,别只盯着CPU核数匹配。
提示:绑核之前务必先检查机器的NUMA拓扑,用
lscpu或numactl --hardware确认物理核分布,避免BE任务绑定到跨NUMA的核组。
4.2 干扰检测与自愈:轻微抖动时的主动规避
即使做了上述所有隔离,BE任务仍然可能通过共享的L3缓存、内存带宽、网络队列等方式干扰在线任务。经典场景:BE任务在Spark shuffle阶段,突然大量读本地磁盘又写远端,网络队列被占满,同节点在线服务的接口延迟从50ms涨到300ms。静态隔离拦不住这种"看不见的干扰"。
因此成熟的混部方案都带干扰检测模块,围绕几个关键指标:
- CPU调度延迟(runqueue latency):在线Pod的线程在可运行队列里等待的时间。
- 缓存缺失率(LLC miss):在线Pod的L3缓存命中率是否异常下降。
- 内存带宽占用:BE任务是否吃掉大量内存带宽。
- 网络和磁盘延迟:在线服务的包传输和存储IO延迟是否抬升。
Koordinator的干扰检测策略可以配置成类似这样的逻辑:如果连续30秒检测到在线Pod的调度延迟超过基线阈值,就判定BE任务构成干扰,自动执行压制或驱逐。实际配置通过slo-config这个ConfigMap管理,里面可以定义针对不同QoS等级的干扰阈值和处置动作。
指标基线怎么定很重要。我的做法是,混部改造前先采集在线服务一两周的指标,算出P99基线和波动区间,再以基线作为干扰检测的参照。如果直接拍脑袋定一个绝对阈值,比如"调度延迟大于10ms就报警",很容易误报,因为在线任务本身偶发业务抖动也正常。
自愈动作的梯度也要设计好,别一步到位直接驱逐。我一般按三级处理:第一级压低BE Pod配额,第二级给BE Pod打上"已被压制"标记并降级,第三级才驱逐重调度。给BE任务留出"慢慢完成任务"的时间,总比在高峰期突然把BE Pod全部驱逐、造成节点资源骤变更可控。
4.3 内存回收与缓存限制:别让离线任务吃光Page Cache
内存这块是混部翻车的重灾区。BE任务,尤其大数据和AI训练,会大量读文件,把Page Cache吃得很厉害。在线任务如果也访问同样的数据集,本来该命中Page Cache,结果因为缓存被BE撑爆,只能重新读盘,延迟瞬间飙升。这个现象非常隐蔽,因为CPU占用率看着不高,但业务已经慢了。
解决办法有几层:
- 设置memory.high作为BE Pod的软内存上限,超过之后内核会积极回收该cgroup的内存,不给硬杀进程的机会。
- 远程内存回收,把BE Pod的匿名内存换出或压缩,把空闲内存还给系统。
- 限制Page Cache占用比例,不让BE Pod无限制膨胀缓存。
- 调整OOM优先级,确保系统内存紧张时,最先被OOM掉的是BE Pod,而不是在线Pod。
Koordinator有专门的内存回收能力,在检测到节点内存压力高时,主动把BE Pod的匿名内存换出或者把Page Cache清掉。K8s原生没有这种机制,这也是混部组件不可替代的原因之一。
我强烈建议对BE任务做短时大内存申请压测,模拟Spark计算中段突然请求10G内存的场景。不压一次,你永远不知道节点Agent的回收策略反应有多快、是否可靠。
5. 从方案到实践:混部改造的路径、收益和风险
前面讲完了原理,最后这部分是真实项目里最值钱的经验:怎么落地、收益怎么算、踩了哪些坑。
5.1 一个真实的混部改造案例:资源利用率从13%提升到38%
我在上一家公司推进过一次混部改造,背景是线上有80多个Deployment,离线有零散的Spark和训练任务,集团要求IDC成本同比降低20%。方案分三步走。
第一步,资源盘点与打标。把在线服务按重要性分成LSE和LS两档,核心交易链路和支付网关标LSE,普通内部服务标LS,离线任务全部标BE。这一步花了大概两周,主要是跟各业务方对齐"你的服务真的需要LSE吗",讨论过程占了大部分时间。
第二步,部署Koordinator控制面和Koordlet节点代理,先在一个16台机器的节点组里试点。这个节点组我特意挑的是业务流量相对稳定的支付后端集群,而不是流量峰谷最大的电商主站,目的是控制试错影响面。试点期间只容纳Spark任务,不开干扰检测的自动驱逐,只记录不处置,让数据先说话。
第三步,验证稳定后全量推广。逐步放开超卖比例、开启干扰检测的自动压制,同时把监控面板共享给业务方,让每个业务都能看到自己的P99有没有波动。推广节奏按节点组滚动进行,每批观察两天。
最终数据:试点节点组CPU利用率从13%提到38%,内存利用率从41%提到69%。集群整体物理机数量从30台压缩到18台,IDC成本年化下降约18%。更难得的是大促流量高峰期间,核心在线服务的P99没有出现过明显抬升。这个数据在行业里不算极致,有些互联网大厂能做到利用率50%以上,但对中型业务来说已经是显著改善了。
5.2 关键收益明细与"回本"计算
混部改造的收益不能只盯着利用率数字,要算综合账。假设一台物理机(以2路32核、256G内存为例)年综合成本是X元,混部省下12台机器,一年就是12X的纯节省。再算投入:Koordinator本身是开源组件,没有软件采购成本;主要投入是人力,需要一个专门的混部运维角色或至少半个SRE的持续投入,加上监控系统扩展干扰指标、每周复盘BE任务的资源画像。这个成本不低,但相比省下的硬件开销,通常一个季度以内就能回本。
什么样的集群最适合上混部,我总结了几条:
- 在线负载有明显波峰波谷,夜间或低峰期有大量空闲窗口。
- 有稳定的离线计算需求,比如定时ETL、数据训练、报表跑批,而不只是临时任务。
- 多个业务线负载高峰不重叠。
- 有足够的监控和告警能力支撑干扰检测。
反过来,如果集群负载已经长期维持在70%以上,或者在线服务属于一点抖动都不能容忍的极实时业务,比如高频交易撮合,混部就别碰了。混部是给"有闲置但账面满"的集群准备的,不是万金油。
5.3 混部最容易被忽视的三个坑
第一个坑:BE任务突发大内存引发节点雪崩。我们踩过一次:一个数据回填任务在计算中段突然一次性申请8G内存,节点Agent回收速度没跟上,直接触发OOM,把同节点的在线Redis实例杀了。事后复盘,根因是BE Pod没有设置内存limits,且节点内存回收阈值配置过保守。解法是给所有BE Pod强制设置内存limits,并把回收触发阈值从90%下调到80%,留出足够缓冲。
第二个坑:压测数据掩盖真实业务波动。试点期间压测跑得很完美,CPU利用率超过30%、P99毫无感知。结果正式流量一上来,午高峰在线P99在连续几天里持续波动。排查发现压测流量是匀速分布的,真实流量有突发性和周期共性,BE任务在高峰期存活过多,在线任务偶发资源需求时来不及腾地方。后来把超卖系数下调了20%,并开启高峰时段BE任务自动降级策略,才稳定下来。
第三个坑:业务方不信任导致方案推不动。技术问题都是小事,组织问题才是大事。有业务方看到自己的节点上多了BE Pod,情绪很大,生怕被影响。我们后来做了两件事:把BE Pod状态、压制事件、干扰检测记录全部透明化,开一个只读监控面板让业务方随时自查;同时明确写了"一键暂停混部"的应急预案。有了看得见的监控和可回退的方案,业务方配合度大幅提升。
5.4 可选的简化路径:Volcano、云厂商托管方案与GPU扩展
如果不想自己维护Koordinator,也有简化路径。Volcano更偏批处理调度增强,在"在线+离线混合"的QoS管控上比Koordinator弱一些,但对任务多、调度复杂的场景依然很实用。云厂商一般也提供托管式的Serverless混部或者节点池级混部方案,适合不想投入太多运维人力的情况。
我的态度是:要么认真上混部组件并持续治理,要么就别用节点池硬隔离。最怕的是"半混部"——只装了调度器,没配节点Agent的压制和回收,那种方案线上跑一周就会出问题。
另外,如果集群里有GPU资源,混部还涉及显存隔离和GPU共享调度,Koordinator也有对应的GPU方案。但通常建议先把CPU+内存的混部跑通,再考虑GPU混部,因为GPU的显存不可压缩,容错空间比内存还小。
如果让我总结混部改造的经验,我想说:这件事的技术难度其实没有传说中那么大,真正的门槛在于团队对资源利用率的理解是否形成了共识。Koordinator这类组件已经把调度、压制、回收、干扰检测都集成好了,剩下的是运维流程和业务对齐。我个人实操中的体会是,先别追求激进的超卖系数,也别一上来全量推广,用一个节点组跑三到四周,把监控基线、告警规则、回收策略调到让业务方完全无感,再逐步放量。另外,确保任何时刻都保留一个"一键全量驱逐BE任务"的开关,这个开关比任何监控都更让业务安心。混部这条路一旦跑通,对成本优化的贡献是立竿见影的,而它背后撬动的K8s资源调度逻辑升级,也值得每位平台工程师深入理解。