背景
我维护一个 12 节点(16C64G,K8s 1.28,containerd 1.7)的自建集群,上面跑着 40+ 服务:API 网关、订单服务、日志采集(fluent-bit)、batch ETL、监控 agent(node-exporter / cAdvisor)、CI 构建 worker……
去年团队做了一次"资源治理",核心动作就一条:所有 Pod 统一 requests=limits,QoS 全部拉成 Guaranteed。理由是驱逐优先级最高、排障时不会被 cgroup 的 OOMKill 和节点驱逐搅在一起、容量规划简单。
当时确实爽了半年。但今年节点从 8 扩到 12 后,问题开始暴露:
- 一台 16C 节点上,fluent-bit 的 limit 设了 1C(实际稳态 80m),node-exporter 设了 0.5C(实际稳态 30m)——这些"守护型"组件白白锁掉了 1.5C 的调度空间;
- batch ETL 任务跑的时候 CPU 会飙到 8C,但稳态只要 2C,limits 设 8C 等于按 peak 付了 12 小时的账单;
- 反过来,API 服务的 limits 设太紧(=requests=4C),一次突发流量直接 OOMKill,比"被驱逐"还惨——至少驱逐还能 drain 30s。
三种常见做法(也是团队内部分歧)
| 派系 | 策略 | 典型理由 | |------|------|----------| | A:全 Guaranteed | requests=limits,所有服务 | 驱逐优先级最高;排障路径短;容量=limits,账单可算 | | B:分层 Guaranteed | API 服务 requests=limits;batch / agent 只设 requests 不设 limits(Burstable) | 关键路径保 SLO;非关键路径省成本 | | C:全 Burstable | 只设 requests,不设 limits(或 limits=requests×3) | 给 burst 留空间;避免 OOMKill;cgroup 兜底 |
我的倾向(欢迎打脸)
我目前倾向 派系 B + 一条硬规则:
- 按"故障爆炸半径"分层:直接面向用户、P0/P1 的服务 → Guaranteed(requests=limits);内部 batch、日志、监控 → Burstable(只设 requests);
- limits 的上限不是"能跑多少",而是"节点超卖比允许多少":我们把节点超卖比控制在 1.3× 以内(即 Σrequests ≤ 物理资源 × 1.3),limits 可以更高但节点上 Σlimits ≤ 物理资源 × 1.8。超了就不允许再调度;
- 一个反直觉的点:对 Java 服务,limits 设成"能触发 OOMKill 但不触发 Full GC 风暴"的值,比"永远不 OOMKill"更合理——因为 Full GC 风暴时整个 Pod 的所有请求都卡住,不如 OOMKill 一个 Pod 让 K8s 拉起新实例(前提是 PDB 和 readiness 配好)。
补充:我们集群的实际 limits 配置表(2026-10 更新)
上面讲完原则,补一张真实配置表,方便大家对照自己的集群看差距:
| 服务类别 | 代表服务 | requests | limits | QoS | 备注 | |----------|----------|----------|--------|-----|------| | API 网关 | nginx-ingress | 2C / 2Gi | 4C / 4Gi | Burstable | 突发流量多,limits 留 2× 余量 | | P0 业务 API | 订单/支付 | 4C / 4Gi | 4C / 4Gi | Guaranteed | -Xmx=3Gi,MaxRAMPercentage=75% | | P1 内部 API | 商品/库存 | 2C / 2Gi | 2C / 2Gi | Guaranteed | -Xmx=1.5Gi | | Batch ETL | 对账/结算 | 2C / 4Gi | 8C / 8Gi | Burstable | 跑批时 CPU 飙 8C,稳态 2C | | 日志采集 | fluent-bit | 100m / 256Mi | 500m / 512Mi | Burstable | 稳态 80m,peak 400m | | 监控 agent | node-exporter | 50m / 64Mi | 200m / 128Mi | Burstable | 稳态 30m | | CI worker | gitlab-runner | 4C / 8Gi | 16C / 16Gi | Burstable | 构建时 CPU/内存飙到 16C | | 定时任务 | cronjob | 1C / 1Gi | 1C / 1Gi | Guaranteed | 短任务,不需要 burst |
几个关键数字:
- P0 业务 API 的
-Xmx=3Gi对应limits=4Gi,余量 1Gi 给 native(Netty buffer / glibc arena / 线程栈),这个 1Gi 余量是踩了 3 次 OOMKill 后定的; - Batch ETL 的
limits=8Gi是按 peak 设的,但实际 95% 时间只用 2Gi——这就是"按 peak 付 12 小时账单"的具体体现,改成 Burstable 后节点可调度容量多了 6Gi; - CI worker 的
limits=16C是最离谱的一条:一台 16C 节点上如果同时跑 2 个 CI worker,limits 就超了——但我们用nodeAffinity限制每节点最多 1 个 CI worker,所以实际不会超。
补充:一次 OOMKill 事故复盘(2026-08,P1)
现象:订单服务(P0,Guaranteed 4C/4Gi)在 14:00~14:30 连续 OOMKill 4 次,每次重启后 2~3 分钟再次 OOMKill。业务影响:下单成功率从 99.9% 掉到 97.2%,持续 25 分钟。
根因(事后用 jcmd VM.native_memory + pmap 分析):
-Xmx=3Gi没问题,堆内 GC 正常;- Netty 的 PooledByteBufAllocator 在突发流量下预分配了 1.2Gi 的 off-heap buffer(
io.netty.allocator.numDirectArenas=16× 每个 arena 最大 8Mi × 实际用了 9000 个 chunk); - glibc arena 碎片又占了 400MB(
MALLOC_ARENA_MAX没设,默认8 × CPU = 128); - 线程栈 180 线程 × 1Mi = 180MB;
- 合计 native ≈ 1.8Gi,加上堆 3Gi = 4.8Gi > 4Gi limit → OOMKill。
修复:
MALLOC_ARENA_MAX=16(JVM 启动参数)——native 碎片从 400MB 降到 120MB;io.netty.allocator.numDirectArenas=4(从 16 降到 4)——off-heap buffer 从 1.2Gi 降到 600MB;- limits 从 4Gi 调到 5Gi(-Xmx 不变,给 native 留 2Gi 余量);
- 加了一个
memory.current / limits > 0.85的告警(Prometheus 15s 采样,提前量约 2~3 分钟)。
教训:Guaranteed 的"好处"(驱逐优先级最高)在 OOMKill 场景下完全没用——OOMKill 是 cgroup 直接杀进程,不经过 K8s 驱逐流程,PDB / drain 全部跳过。所以"Guaranteed 更安全"这个前提是你设的 limits 足够大,否则 Guaranteed 只是"被杀得更快"。
想听大家聊的
- 你们集群里 batch job / 日志采集 / 监控 agent 的 resource 策略分别怎么定?是全 Guaranteed 还是故意留 Burstable?
- 有没有踩过 limits 设太高 → 节点超卖 → 驱逐风暴 → 雪崩 的坑?具体什么配置组合触发的?
- 对 Java 服务,你们 limits 是设成"永不 OOMKill"还是"允许 OOMKill 但要 PDB 兜底"?JVM 的 -XX:MaxRAMPercentage 跟 K8s limits 怎么联动?
- 你们有没有用 VPA(Vertical Pod Autoscaler)做推荐,还是纯手工 + Prometheus 历史 P95?
- (新增) 你们的 Netty off-heap buffer 有没有做过定量?
numDirectArenas和maxOrder怎么跟 K8s limits 联动?
不追求"标准答案",就想看看大家在不同规模(10 节点 vs 500 节点)、不同业务形态(纯 API vs 有 batch)下,怎么在"成本"和"不被 OOMKill"之间画那条线。