【背景】 15 年嵌入式出身,近 5 年转容器化与 K8s 运维。去年在一个 30 节点生产集群(K8s 1.28,cgroup v2)做降本时撞上这道题:60% 以上服务都按 requests=limits 配(Guaranteed QoS),节点 allocated 率看着 95%,实际利用率只有 40% 上下——等于为「永远用不到的余量」付钱。与此同时,一个 requests=256m、limit=4C 的 Burstable 服务,在节点内存压力下被 kubelet 驱逐,Pod 重建 + JVM 冷启动花了 2 分半,业务侧直接报障。两件事放在一起,团队里就吵起来了:到底该统一 Guaranteed,还是放开超卖?
【常见分歧 / 几种做法】 做法 A(稳定优先):全部 requests=limits,Guaranteed QoS。 优点:驱逐顺序可预期(节点内存压力时 BestEffort/Burstable 先死,Guaranteed 最后死),没有 limit thrashing,排障路径短。 缺点:超卖率低,成本贵;limit 给小了,JVM / Go 运行时一次 GC 或协程暴涨就 OOMKilled,「稳定」反而变成了「随机死亡」。
做法 B(成本优先):requests 低配,limit 设为 requests 的 2~4 倍,甚至不设 limit。 优点:超卖率高、账单低,突发负载有天然余量。 缺点:QoS 等级低,节点压力下「谁先被杀」不可预测;limit thrashing(周期性 OOMKilled)排查很痛苦,尤其 Go runtime 的内存行为不透明。
做法 C(分级):有状态 / DB / 网关走 Guaranteed,无状态在线走 Burstable + limit,批处理走 BestEffort 并用 taint 隔离。 我见过的大多数团队实际都在做 C,但分界线画在哪、limit 给多少,几乎全靠拍。
【我的倾向与理由】 我倾向 C,而且分界线不看「业务重不重要」,看「单次失败的爆炸半径 + 恢复成本」:
- 启动慢的服务(JVM 预热、大缓存加载、模型加载)→ Guaranteed。驱逐代价太高,宁可买余量。JVM 的 limit 我按 max heap + metaspace + 线程栈 + 直接内存 + 20% 余量算,不是拍脑袋。
- 无状态、秒级启动、可水平扩展 → Burstable,limit ≈ 2× requests,并且 HPA 跟业务指标(队列深度 / P99)而不是 CPU——超卖后 CPU 利用率本身就是失真信号,跟它扩缩容等于闭眼开车。
- batch / 离线任务 → BestEffort,taint + toleration 推到专属节点,不跟在线混跑;要混跑就上 cgroup v2 的 cpu.max / io.max 兜底。 数据说话:我们这个集群按 C 分级跑了 3 个月,节点平均利用率从 40% 提到 62%,驱逐事件从约 4 次/月降到 0,账单降了约 15%。但注意:这是 30 节点、70% 无状态服务的集群,结论未必能平移到 3000 节点或大量有状态服务的集群。
【想听大家在什么场景下怎么选】
- 你们生产集群 Guaranteed / Burstable / BestEffort 的实际比例大概多少?是统一规范还是逐服务定?
- JVM 服务的 limit 你们怎么给?有公式吗,还是压测后拍?留多少余量?
- 超卖后节点驱逐阈值(memory.available、nodefs.available、PSI)你们调过默认值吗?调成什么?
- 批处理和在线混跑:你们靠 taint 分节点,还是 cgroup 限制资源?踩过什么坑?
顺带引用两篇站内相关讨论作为背景:驱逐代价量化(id=2079)、混合负载 I/O 控制(id=1476)。欢迎贴你们集群的真实数字——比例、阈值、驱逐频率,比观点更有用。