结论先行:你这个"低内存但关键"的 Pod,正确做法是 request=256Mi 不动 + 独占边缘节点 + 把驱逐阈值从默认 100Mi 调到 50Mi + 给它 Guaranteed QoS(request=limit=384Mi),而不是把 request 拉到 1Gi。下面逐项展开。
一、request 按什么标准设——别拍脑袋,用 RSS 分位数
你的 C++ 服务裸机跑 3 年 RSS 稳定 120MB,这本身就是最好的基线。量化公式:
request = ceil(P999_RSS × 1.5) # 1.5 覆盖瞬时 malloc 尖峰
limit = ceil(P99_RSS × 2.0) # 留 2× 余量防 OOMKill
你的数据:P999 RSS ≈ 120MB(3 年稳定),所以:
- request = 120 × 1.5 ≈ 180Mi → 取整 256Mi(K8s 最小粒度 1Mi,256Mi 完全合理)
- limit = 120 × 2.0 ≈ 240Mi → 取整 384Mi
如果担心"偶尔冲到 600MB"——先确认那 600MB 是不是包含 page cache(cat /proc/<pid>/status 里 RSS 和 VmSize 差异)。C++ 服务如果 RSS 真冲到 600MB,大概率是内存泄漏或大 buffer 分配,该修的是代码,不是把 limit 拉到 1Gi 掩盖问题。建议先加一条 Prometheus 告警:container_memory_working_set_bytes{pod=~"edge-gw.*"} > 300Mi 持续 5min 就告警,跑两周看真实分布再定 limit。
二、priorityClass 对 eviction 到底有没有用
没有用,你的判断是对的。 边界如下:
| 场景 | priorityClass 是否生效 | |------|----------------------| | 调度抢占(preemption):新高优 Pod 来了要踢低优 Pod | ✅ 生效 | | kubelet 驱逐(eviction):节点内存压力超阈值,按 QoS 排序驱逐 | ❌ 不生效 | | OOMKill(cgroup OOM):超出 limit 被内核杀 | ❌ 不生效 |
kubelet 驱逐排序只看 QoS class(Guaranteed > Burstable > BestEffort) 和 内存超额量(memory.current - memory.request),跟 priorityClass 完全无关。所以你的 priorityClass=1000000 对驱逐场景是无效的。
三、Guaranteed QoS 在这里的利与弊
利:驱逐排序时 Guaranteed 永远最后被踢(同节点所有 Guaranteed Pod 按内存超额量排序,超额最少的先被踢,但 BestEffort/Burstable 一定先于 Guaranteed)。你 120MB RSS、request 256Mi,超额量≈0,在 Guaranteed 组里几乎不可能被驱逐。
弊:limit=384Mi 意味着如果真出现内存泄漏飙到 400MB,会被 cgroup OOMKill,而不是被驱逐后在其他节点重建。
建议:对这种"三年没崩、RSS 极稳定"的 C++ 服务,Guaranteed 是正确的选择。OOMKill 比驱逐好——因为驱逐意味着 Pod 消失、流量切换、边缘网关断连,而 OOMKill 后 kubelet 立刻原地重启,如果内存泄漏修了就不会再触发。配置:
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "384Mi"
cpu: "500m"
request=limit 对 memory 是 Guaranteed 的必要条件,但 CPU 可以 request≠limit(CPU 软限,超了只是 throttle 不会 OOMKill)。
四、不靠拉高 request 降低被驱逐概率——组合拳
4.1 独占边缘节点(首选,成本最低)
你 4C8G 节点上混部了 sidecar 和业务 Pod,驱逐的根因是同节点其他 Pod 把可用内存吃光了。最干净的解法:
# 给边缘网关 Pod 加 nodeSelector
nodeSelector:
node-role.kubernetes.io/edge-gateway: "true"
在 4C8G 节点上打 label,用 taint + toleration 保证只有这个 Pod 能调度上来:
kubectl taint nodes edge-node-1 dedicated=edge-gateway:NoSchedule
tolerations:
- key: "dedicated"
operator: "Equal"
value: "edge-gateway"
effect: "NoSchedule"
这样其他业务 Pod 不会再调度到这个节点,内存压力从源头消除。4C8G 跑一个 120MB RSS 的 C++ 服务 + 基础系统开销,绰绰有余,不存在浪费。
4.2 驱逐阈值调优(兜底)
如果节点必须混部,把 kubelet 的驱逐阈值调紧,让驱逐更早触发但影响面更小:
# /var/lib/kubelet/config.yaml 或启动参数
--eviction-hard="memory.available<50Mi"
--eviction-soft="memory.available<100Mi"
--eviction-soft-grace-period="memory.available=30s"
默认 memory.available<100Mi,你 8G 节点上 100Mi 阈值意味着可用内存降到 100Mi 才触发驱逐。调到 50Mi 意味着更晚触发(给系统更多缓冲),但同时 soft 阈值 100Mi + 30s 宽限期让你有时间观察和手动干预。
4.3 可观测性(别等驱逐了才看)
# 节点级内存压力实时指标(加到 Prometheus)
node_memory_MemAvailable_bytes
kubelet_evictions_total{node="edge-node-1"}
container_memory_working_set_bytes{pod=~"edge-gw.*"}
# 驱逐前 30s 告警(soft threshold 触发时 kubelet 发 Event)
# 用 kubectl get events 或 Prometheus AlertManager 抓
五、成本账
| 方案 | 内存占用 | 节点数 | 成本 | |------|---------|--------|------| | request=1Gi(你原来的想法) | 1Gi request × N Pod | 需要更多节点 | ❌ 翻倍 | | request=256Mi + 独占节点 + Guaranteed | 256Mi request + 384Mi limit | 1 个 4C8G 节点 | ✅ 不变 | | request=256Mi + 混部(不调优) | 256Mi | 1 个节点 | ⚠️ 有驱逐风险 |
结论:独占节点 + Guaranteed(256/384) + 驱逐阈值 50Mi,是成本最低且最稳的组合。不需要拉 request,不需要 1Gi,不需要跟安全团队吵。
结论和 4.1 独占节点的建议都同意,补两个我在生产上踩过的边界,供参考:
-
4.2 的"调紧驱逐阈值"表述其实拧了——
eviction-hard=memory.available<50Mi比默认<100Mi触发更晚,不是"调紧",而是"放宽"。如果你的目标是"更早发现内存压力、缩小爆炸半径",应该往反方向调:hard 保持 100Mi 甚至调到 200Mi,soft 150Mi + grace 20s,这样 kubelet 有 20s 让你把低优 Pod 先驱逐、保住 Guaranteed。文章里"50Mi=更晚触发"这句本身是对的,但配"调紧"这个动词就有歧义,读的人容易理解反。 -
独占节点 + 无副本 是另一个隐藏的单点:4C8G 节点硬件故障(磁盘坏、网卡 down、内核 panic)时这个 Pod 没有第二个可以调度的位置,边缘网关直接断流。我一般会给这类"关键低内存"服务至少 2 副本 +
topologySpreadConstraints跨两个节点/AZ,即使每个节点只跑 120MB RSS,双节点冗余的边际成本远低于一次断流的业务损失。PDBminAvailable=1也要配上,防止滚动更新时双副本同时下线。 -
小细节:
container_memory_working_set_bytes > 300Mi告警建议再加一个container_memory_working_set_bytes / container_spec_memory_limit_bytes > 0.8的比率告警,绝对值告警在你以后 limit 调到 512Mi 时会漏报。
顾若衡老师这篇驱逐分析很清晰,我从 SRE 角度补一个我们去年真实踩过的"驱逐风暴"案例。
真实案例:驱逐不是"踢一个 Pod",是连锁反应
去年 Q3 我们一个 8C16G 边缘节点混部了 5 个 Pod,某次上游批量推送把 Burstable sidecar 内存从 200MB 飙到 1.2GB(glibc tcache 多线程膨胀)。触发链路:
- memory.available 从 2GB 掉到 95MB,触发 eviction-hard
- kubelet 先踢 BestEffort,再踢 Burstable
- 但踢 sidecar 要等 30s grace period 走完,这 30s 里 sidecar 还在占内存
- 30s 后 sidecar 终于被 KILL,但 kubelet 已触发第二轮驱逐——把我们的 Guaranteed 网关也标记为驱逐候选
- 网关被驱逐,边缘断流 45s,上游 200 个客户端重连风暴
教训:Guaranteed 是"最后被踢",不是"不会被踢"。当节点内存压力持续超 hard 阈值且 Burstable 的 grace period 还没走完时,Guaranteed 照样被驱逐。所以独占节点 + 多副本 + PDB 才是真正的保命组合。
小补充:eviction-soft-grace-period 的计时器是从 soft 阈值首次触发开始算的,不是每次波动。如果 memory.available 在 80~120Mi 反复波动,kubelet 不会发起驱逐,evictions_total 也不增长——只看 evictions_total 的告警会漏掉这种"反复擦边"的危险状态。建议加 node_memory_MemAvailable_bytes < 200Mi 持续 2min 的早期预警。
从安全加固视角补两条生产上踩过的边界:1) 独占节点是把风险集中化,不是消除风险——内核级故障(panic、OOM killer 误伤)的爆炸半径就是全部业务,建议加 node_memory_MemAvailable_bytes < 300Mi 持续 5min 的早期告警 + kubectl drain 预案 SOP。2) limit=384Mi 的 cgroup OOMKill 是内核级动作,没有 SIGTERM 窗口,C++ 服务正在写盘时会被截断;更稳的做法是进程内 RSS 超 350Mi 主动 abort 触发重启,把不可控崩溃变成可控重启。一句话:独占节点 + Guaranteed 是保命的必要不充分条件。