
干了这么多年K8s集群运维我见过太多资源利用率表上写着CPU平均使用率不到20%的集群了。今天想认真聊聊混部技术——就是把在线业务和离线任务塞到同一批物理节点上用资源调度优化手段把整体资源利用率拉上去的做法。这篇文章会从原理讲到实操正好适合正在为集群成本发愁或者准备在K8s上做容量治理的运维、平台工程师参考。我会把自己踩过的坑、验证过的参数一起写出来方便你直接拿去落地时对照。1. 混部技术到底在解决什么问题1.1 为什么K8s集群里资源利用率普遍低我做过不少集群的容量摸底发现大部分K8s集群的CPU平均使用率长期在20%~30%之间内存也好不到哪去。这不是某个公司的问题而是K8s默认调度机制和业务流量规律共同作用的结果。业务为了扛住高峰通常按照峰值流量来申请资源K8s调度器只认requests不认Pod当前实际用了多少CPU。也就是说你为高峰预留出来的资源在低谷时段全都空转着。我见过一个在线业务Deployment每个副本申请8个核实际日常只有2个核的用量这种配置在K8s集群里非常常见。除了需求峰谷差节点碎片化也是一个重要原因。多个Pod各自带着不同的request分散在不同节点上调度器只会把Pod放到有足够“账面资源”的节点不会去考虑节点当前是不是真的空闲。结果就是有些节点已经被一堆超卖Pod占满了账目配额但实际负载很低新Pod又调度不进去。你从node维度看每个节点的allocated resources都接近100%但Usage却低得可怜这就是典型的纸面饱和、实际浪费。K8s的资源调度优化说到底就是要在“保证在线服务质量”的前提下把浪费掉的那部分资源利用起来。混部技术就是目前业界用得比较多的一套组合拳思路不是让单个Pod申请更少资源而是把不同类型的负载放到同一批机器上让他们错峰互补。这样资源利用率才有机会从20%级别拉到50%以上。1.2 混部技术的基本逻辑让不同特性的负载互补混部的核心是把在线服务和离线任务放在同一个Kubernetes节点上运行。在线服务大家都很熟特点是长驻、延迟敏感、流量有峰谷离线任务则相反比如大数据批处理、日志清洗、模型训练、CI流水线这类任务通常体量大、可被中断、对延迟不敏感。如果把他们分开跑在两批机器上就会同时出现两块浪费在线机器在低谷闲置离线机器在跑任务时CPU打满但大部分时间也在闲置。我不太喜欢把混部讲得太玄你可以把它理解成合租。在线服务是白天出门上班的室友离线任务就是晚上回来用客厅的室友。客厅只有一间白天给在线服务用晚上给离线任务用。问题在于两个人打照面的时候怎么分配客厅需要一套规则。K8s里的规则就是调度、优先级、驱逐和资源配额。这里要纠正一个常见误区混部不是把离线任务塞到在线节点上就完事了也不是单纯把离线Pod的requests调低就行。混部技术的重心在于“在线优先”的资源保障机制。在线服务高峰期离线任务必须能快速让出资源在线服务空闲时离线任务又能把资源用足。这样既不影响在线SLA又能最大化集群利用率。1.3 哪些场景适合做混部哪些不适合混部不是万能药我通常会先帮团队做一轮场景评估。适合混部的场景有几个明显特征第一在线业务有明显的流量峰谷比如白天用户访问高、凌晨基本无人访问或者工作日高、周末低第二团队有大量非实时的离线任务比如报表计算、日志处理、定时数据同步第三集群成本是公司很敏感的话题需要靠提高利用率来节省机器。不适合混部的场景也很清晰。如果业务有强合规隔离要求比如金融支付核心链路、医疗健康数据、涉及用户隐私的数据处理不建议混部因为隔离风险带来的代价远大于省下的机器成本。另外如果在线服务本身对抖动极度敏感比如量化交易系统、实时音视频推流也不太适合一开始就做大规模混部至少应该先做小流量验证。值得一提的是GPU资源混部在现在的AI团队里也很热门。GPU卡很贵闲置率却很高。但GPU混部的复杂度和CPU混部完全不是一个量级除了算力还要解决显存隔离、驱动抢占、MIG切分等问题。如果你的目标是先把资源利用率做起来建议先从CPU和内存的混部开始验证GPU混部放到后面当进阶课题。2. 混部设计的四个关键点2.1 资源超卖的前提把Request和Limit的真实关系搞清楚很多刚接触混部的人上来就调低Pod requests以为这样就能把资源利用率提上去。这样做很危险。在K8s里requests是调度器做决策的依据limits是kubelet对容器实际用量的限制。调度器看节点上的已分配资源是累加所有Pod的requests而CPU真正竞争时是按limits和cgroup权重来限制的。如果在线Pod的requests设置过低调度器会把大量Pod堆到同一节点但实际压力来临时CPU会被limits限制服务自然会延迟飙高。相反如果离线Pod的requests设置得和在线服务一样高它就会占用很多账面配额又轮不到在线服务使用这就失去了混部的意义。我的实践方法是在线Pod维持合理的requests最好根据压测数据来定让它真实反映业务峰值用量离线Pod把requests压到很低比如CPU 10m~100m内存留一个基础值但把limits放开让它在在线服务不用资源时可以冲到高位。这样离线任务就像一块“弹性海绵”在线需求上来时自动被压缩在线空闲时又能充分吸水。需要特别注意limits不能无限放大。CPU limit放太大会导致离线任务把节点CPU全部打满因为没有强制CPU亲和或优先级抢占的话cgroup的CPU配额遇到Burstable QoS时是按权重竞争的。后面我会详细说这个问题。2.2 绕不开的优先级PriorityClass决定谁抢占谁K8s原本没有为混部设计独立机制靠的是优先级体系。PriorityClass是集群级别的资源对象通过在Pod上指定priorityClassName可以给Pod一个整数优先级值。数值越大优先级越高。调度器在节点资源不足触发抢占时高优先级Pod会把低优先级Pod挤掉给新Pod腾出位置。因此混部方案里在线服务必须绑定高优先级离线任务绑定低优先级。比如我会创建online-priority和offline-priority两个PriorityClass。这里有几个细节值得注意优先级数值不能乱拍。K8s本身有一些系统组件的优先级像kube-system里的关键Pod通常有很高的priority。如果在线业务的priority设置成比系统组件还高CNI、kube-proxy这些基础组件在资源紧张时可能被挤掉整个节点直接出问题。我的经验是在线业务priority设置在100万左右离线任务设置在100~1000这个量级既保证了在线相对离线的优势也不至于影响系统组件。还要明确一点PriorityClass解决的是节点资源不足时的调度抢占和节点驱逐顺序但它不能解决运行过程中的CPU竞争。上游Pod已经跑起来了实时动态让他们让路更多要依赖混部组件比如Koordinator的CPU Suppress、Crane的动态资源超卖。所以PriorityClass只是混部的第一道防线不是全部。2.3 资源隔离和干扰检测防止离线任务拖垮在线业务混部最大的风险不是调度失败而是离线任务在运行时把在线服务“打爆”。这种干扰通常来自几个维度CPU竞争导致在线线程等待变长内存占用过高触发回收甚至Swap磁盘和网络IO被离线任务占满导致在线请求超时。CPU方面的隔离可以通过cpuset把在线Pod和离线Pod绑到不同的CPU核上但K8s默认调度器不会自动做这个事你需要借助节点级配置或者容器运行时的CPUManagerPolicy。内存方面更复杂K8s本身对节点内存使用是overcommit的一旦离线任务吃光了节点内存kubelet会触发节点级驱逐。所以混部必须配置好cgroup级别的内存限制把离线任务限制在一个可控范围内。干扰检测也极其关键。我推荐在混部环境里至少采集这几类指标节点CPU使用率、容器CPU Throttling时间、CPU Steal值、内存PSI指标、网络延迟统计。其中CPU Steal如果持续偏高说明节点超卖严重PSI里的some和full指标可以直接反映资源竞争造成的任务停顿。Prometheus配好这些指标后混部的每一步调整才有数据支撑不然出了问题只能靠猜。2.4 动态调节让离线任务在在线业务高峰时自动“让路”静态配置只能让混部“能跑”要想让混部“跑得稳”还得有动态调节手段。在线业务是有时效性的白天高峰、凌晨低谷。理想状态下离线任务应该晚上跑得欢白天被压得很低。目前社区里常用的做法是把Prometheus的指标接入混部控制器。比如在某个时间窗口内如果在线命名空间的CPU用量高于节点总量的60%控制器就降低离线命名空间的CPU limit或者批量驱逐一部分离线Pod如果在线用量降到30%以下再放开离线任务。这种动态抑制逻辑Koordinator和Crane都提供了现成能力。我在自己环境里验证过动态调节比固定配置安全得多。固定配置下离线任务一旦按低峰期的资源量放开到了白天高峰如果不及时收在线延迟会很难看。而动态调节至少能保证在线高负载时离线任务以秒级甚至分钟级速度退让。如果你还没有引入混部组件也可以先靠CronJob定时调整离线Deployment的副本数作为过渡方案。3. 实操在K8s里落地一套混部验证环境3.1 场景设定和环境准备先说一个我最近做的验证场景。有一套若依微服务环境原本跑在旧物理机上后来整体迁到了阿里云ECS。迁移完成后要做的一件事就是用JMeter脚本做高并发压测验证云上环境能不能扛住业务流量。我顺手在这个压测窗口期把混部验证也一起做了。这样压测在线业务的同时跑一批离线任务既能看到在线服务的表现又能评估混部对整体资源利用率的影响。环境准备不复杂。我用了三台4核16G的ECS装了最新的稳定版K8s。由于这套环境前后要用不同配置我建议先把基础监控搭起来Prometheus Grafana node-exporter kube-state-metrics。总有人问“k8s集群怎么搭prometheus”其实无非就是helm install或者直接用manifests部署关键是要把node-exporter和kube-state-metrics的指标采集全。Grafana里可以提前导入Node Exporter Full面板和Kubernetes Cluster面板后面观察数据会方便很多。压测工具我用的JMeter。压测脚本针对于若依的几个核心接口比如login、用户查询等线程数按业务预估的QPS来设定加上ramp-up时间避免一上来全部线程就压垮服务。这里不需要造太大压力能稳定打出一部分流量就够了因为我们更关注混部前后在线延迟和节点利用率的变化。3.2 在线服务怎么部署Requests设置不能拍脑袋若依是一套典型的前后端分离微服务包含网关、认证、系统管理等多个服务。混部验证时需要把这几个在线服务放在独立的namespace里比如online。然后给它们统一设置合理的资源配额。这里最大的坑就是requests拍脑袋。我是先做了一轮无限制压测用JMeter以固定线程数持续压测10分钟同时用Prometheus看每个Pod的实际CPU和内存曲线。根据稳定运行时的资源用量我再反推出每个服务的requests。比如ruoyi-auth这个服务压测时CPU稳定在300m~450m内存稳定在400Mi左右那么requests就设成500m和512Milimits可以设置到2核和2Gi。不要让requests等于实际峰值要给调度留一点缓冲也别设太大否则又回到利用率低的局面。在线服务的Deployment配置大致如下apiVersion: apps/v1 kind: Deployment metadata: name: ruoyi-auth namespace: online spec: replicas: 2 selector: matchLabels: app: ruoyi-auth template: metadata: labels: app: ruoyi-auth spec: priorityClassName: online-priority containers: - name: ruoyi-auth image: ruoyi/auth:latest ports: - containerPort: 8080 resources: requests: cpu: 500m memory: 512Mi limits: cpu: 2 memory: 2Gi这个例子可以看到requests代表调度时的账面占用limit代表真正能爆发的上限。在线服务requests不能省因为你的服务必须保证在高峰期有足够的CPU份额。limits反而可以比实际用量放宽一些让Pod在超过预期流量时可以借用节点的空闲资源。3.3 离线任务怎么部署低优先级弹性配额离线任务我用一个模拟批处理来验证镜像直接用polinux/stress让它在容器内跑固定时间的CPU压力。生产环境当然不会用stress但验证混部机制时它非常方便可以精确控制要占几个核、跑多长时间。离线任务放在独立的offline命名空间并绑定offline-priority这个低优先级类。requests写得特别低比如CPU只用10m内存64Mi这样调度器在满节点上也总能找到“账面空间”把它放进去但limits会放开一些比如CPU最多用4核内存最多4Gi这样在线业务资源空闲时它就能多占资源。apiVersion: apps/v1 kind: Deployment metadata: name: offline-batch namespace: offline spec: replicas: 3 selector: matchLabels: app: offline-batch template: metadata: labels: app: offline-batch spec: priorityClassName: offline-priority containers: - name: stress image: polinux/stress command: [stress, --cpu, 2, --timeout, 600s] resources: requests: cpu: 10m memory: 64Mi limits: cpu: 4 memory: 4Gi三个副本同时跑每个打满2个核理论上可以吃掉6个核。但因为有limits限制再加上在线Pod有高优先级当节点出现CPU竞争时离线任务的使用份额会被压制。这个设计就是混部的基本形态离线任务是“填缝剂”不跟在线业务抢主路。3.4 混部核心参数PriorityClass、Quota与LimitRange的组合上面用到了PriorityClass但还没展示定义。我建议在一个集群里至少建两个PriorityClass一个给在线一个给离线。这里有示例apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: online-priority value: 1000000 globalDefault: false description: 在线业务高优先级 --- apiVersion: scheduling.k8s.io/v1 kind: PriorityClass metadata: name: offline-priority value: 100 globalDefault: false description: 离线任务低优先级PriorityClass是集群级对象创建之后在任意命名空间都能引用。要注意的是value是整数不能重复太多否则抢占顺序就不明显。除了PriorityClass我还习惯给离线命名空间加ResourceQuota和LimitRange。ResourceQuota用来限制离线任务在这个命名空间里总的资源用量防止离线任务无限扩容把集群全占满。LimitRange则是给命名空间里的每个Pod设置默认的requests和limits防止有人忘写resources。apiVersion: v1 kind: ResourceQuota metadata: name: offline-quota namespace: offline spec: hard: requests.cpu: 2 requests.memory: 4Gi limits.cpu: 8 limits.memory: 16Gi --- apiVersion: v1 kind: LimitRange metadata: name: offline-limitrange namespace: offline spec: limits: - default: cpu: 2 memory: 2Gi defaultRequest: cpu: 10m memory: 64Mi type: Container这里要解释一下为什么ResourceQuota的limits.cpu和requests.cpu差别这么大。离线任务requests很小是为了让调度器不按它占用的实际资源来分配节点而limits很大是为了让它在资源空闲时可以真正用起来。Quota的limits部分控制的是所有离线Pod的limit总和所以如果把limits.cpu设成8即使离线任务limit为4三个副本也用不满全部需要合理估算。3.5 用Prometheus和JMeter验证混部收益环境配好之后验证流程分四步。第一步先只跑在线业务用JMeter压测10到15分钟记录在线P99延迟和集群整体CPU使用率。假设此时P99是150ms节点CPU平均使用率只有18%这就是混部前的基线。第二步把离线任务扩容到3个副本再跑同样压力的JMeter压测。此时关注在线P99有没有明显恶化。如果P99从150ms涨到300ms甚至更高说明混部策略还没有完全做到位需要检查离线Pod的CPU limit、优先级和动态抑制策略。第三步在Grafana里看节点CPU整体利用率。正常情况会看到节点CPU使用率从18%提到50%~60%。第四步压测结束后把离线任务缩容到0再观察在线P99是否回到基线确认没有留残余影响。Prometheus里我比较常用的几条查询如下# 节点CPU使用率 100 - avg by (instance) (rate(node_cpu_seconds_total{modeidle}[5m])) * 100 # 在线命名空间CPU实际使用量 sum(rate(container_cpu_usage_seconds_total{namespaceonline}[5m])) by (pod) # 节点CPU Steal值 rate(node_cpu_seconds_total{modesteal}[5m]) # 离线命名空间的CPU limit配额使用情况 sum(container_spec_cpu_quota{namespaceoffline}) by (pod)从这些指标里可以清楚看到在线Pod的延迟和CPU竞争有没有关系。如果P99上涨但CPU Steal不高可能问题出在应用自身或者网络层如果P99上涨同时CPU Steal和Throttling明显上升基本可以断定是CPU竞争导致的。JMeter侧需要配置好聚合报告和响应时间百分位。我的习惯是把P99、P95、平均响应时间、错误率这四个数值作为混部是否成功的门槛。在线服务如果在混部后P99增幅控制在5%以内错误率保持为0那这个混部状态就算健康。4. 混部一定会踩的坑与排查实录4.1 坑一离线任务把CPU吃满在线P99直接飙到十倍我最早做混部验证的时候离线任务一放上去在线接口延迟立刻从150ms飙到1.5秒几乎不可用。当时我的第一反应是去调PriorityClass数值但改了数值之后运行中的Pod并不会因为优先级变化而被立刻处理它只是影响后续调度和抢占。真正的问题在于在线Pod和离线Pod的CPU份额在相同QoS等级下是“平权”竞争的。后来我做了三件事解决。第一给离线Pod设置了相对较小的CPU limit比如不要让它一个Pod就占满4个核第二把在线Pod从Burstable提升为Guaranteed QoS也就是让requests等于limits这样CPU权重会更高抢CPU的能力更强第三引入节点CPU亲和策略用cpuset把在线和离线绑到不同的CPU核上。实际生产环境中更推荐直接用Koordinator这类组件来实现CPU Suppress它会基于在线Pod的实际使用量动态压制离线任务的CPU份额。注意默认K8s只靠limits限制CPU不解决同一核上的争抢问题。真正要防干扰还得从内核、运行时或混部组件层面下手。排查这个坑的命令也很简单。用kubectl top pod -n offline看离线Pod CPU使用量再用mpstat -P ALL 1看每核使用率如果所有核都接近100%证明压力已经大到在线服务没有空闲核可用了。4.2 坑二离线任务内存超卖把节点打到MemoryPressureCPU竞争虽然醒目但内存问题往往更致命。离线任务通常处理大量数据内存占用容易失控。我遇到过离线任务limit设了4Gi但它内部分片处理时突然涨到接近limit多个副本叠加后整个节点的内存被吃掉了大半。此时kubelet检测到节点内存压力会开始驱逐Pod而驱逐顺序不是按照Pod创建时间而是按照QoS等级和优先级排序。如果在线Pod是Burstable且优先级不够高可能会和离线Pod一起被驱逐业务直接断流。解决办法我当时是分了三层来做。第一层在ResourceQuota里设置离线命名空间的requests.memory和limits.memory上限从总量上卡死离线任务能用的内存第二层给在线和离线Pod都设置合适的PriorityClass这个必须提前做不能等到节点出问题才补第三层调优kubelet的驱逐阈值比如把--eviction-hardmemory.available500Mi配好让节点在内存告急时更早触发驱逐给在线服务留出喘息空间。如果内核支持cgroup v2我强烈建议把PSI指标接入监控。PSI的some和full值能提前反映内存回收对容器造成的停顿比看OOM事件更有预警价值。4.3 坑三只看调度不看实际节点负载任务堆积到热点节点还有一个很容易忽略的问题。离线Pod requests设得低调度器会觉得它们“很轻”于是可能把很多离线Pod调度到同一个节点上完全忽略这个节点的实际负载已经很高。这会导致另一个方向的问题某些节点空闲某些节点被塞满资源利用率分布极度不均匀。我在验证时就碰到过三个离线Pod全被调度到同一个节点另两个节点几乎空闲。原因很简单K8s默认调度器只看requests不看节点当前的真实CPU使用率。解决办法可以在离线Deployment里加podAntiAffinity让相同app的多个副本尽量分散到不同节点或者在节点上打标签结合nodeAffinity把离线任务均匀分布到指定节点组。更彻底的做法是引入负载感知调度比如Koordinator或者Crane里的descheduler。它们能根据Prometheus采集到节点实时load把Pod从热点节点重新调度到空闲节点。不过这类组件有一定学习成本初期验证阶段用podAntiAffinity就够了。示例片段spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: offline-batch topologyKey: kubernetes.io/hostname4.4 常见问题速查表症状可能原因排查命令解决方案在线P99延迟飙高CPU竞争/离线任务limit过大kubectl top pod; mpstat -P ALL 1缩短离线limit; 用cpuset隔离; 接入动态压制节点MemoryPressure离线任务内存超卖kubectl describe node设置ResourceQuota; 调整kubelet驱逐阈值在线Pod被驱逐重启优先级设置不当/QoS太低kubectl get events --sort-by.lastTimestamp提高在线Pod优先级; 设置QoS为Guaranteed混部后利用率还是上不去离线任务没跑起来/Quota过小kubectl describe quota -n offline扩大离线任务副本数; 放宽limits配额离线任务没有按预期收缩缺少动态调节promql查看CPU使用率趋势配置CronJob缩容; 引入Koordinator/Crane4.5 一些经验性的排查命令混部排障时我经常用到下面这些命令建议收藏。首先是一组kubectl基础命令kubectl top node和kubectl top pod -A可以快速看节点和Pod的实时资源用量kubectl describe node node能看节点上所有Pod的requests和limits汇总以及驱逐阈值和conditionskubectl get events --sort-by.lastTimestamp可以看到Pod被驱逐或抢占的原因。节点内部的资源竞争靠kubectl是看不出来的需要登录节点用系统命令。vmstat 1可以看CPU的us、sy和wa变化mpstat -P ALL 1可以定位是不是单个核被打爆pidstat -p pid 1可以看具体进程的CPU和内存消耗。如果发现CPU steal值长期偏高说明这个节点可能已经超卖过头在线服务随时会被拖垮。另外K8s的CPU throttling可以通过容器文件系统直查比如cat /sys/fs/cgroup/cpu/cpu.stat里如果nr_throttled增长很快说明容器CPU quota设得过小或竞争严重。这里的经验是先用这些命令定位问题再动手改配置不要上来就调资源数值否则很容易把问题搞复杂。我在实际项目里做过同样的验证混部之前节点CPU平均利用率大概18%左右混部后稳定在55%到65%在线P99延迟增幅控制在5%以内这个结果比较健康。如果你也想做我的建议是先花一周时间把监控和指标基线摸清楚再谈混部策略。先把在线Pod的requests校准再让离线任务小规模切入逐步放大整个过程一定要保证有回滚方案。混部不是某个开关一打开就完事它是调度、隔离、监控三者配合的结果。