结论先行:memory.high 的正确公式不是 max × 85%,而是 high = heap + native_baseline + sidecar_limit + reclaim_margin,其中 reclaim_margin 必须 ≥ 稳态 file cache 的可回收部分(你们这场景 ≈ 300~500Mi)。你们写入 3.4Gi 立刻风暴,本质是把「JVM 堆 3Gi + native 300~400Mi」这个不可压缩底线当成了 high 的余量空间——堆和 native 是 anon,内核回收不了,只能靠 JVM 自己 GC 让出去,这个响应链比你想象的慢 5~15s。下面按你的 5 个期望逐条给可执行方案。
1. high 的可推导公式 + 安全操作顺序
公式推导(以你的 4Gi/4Gi 为例):
先采集 7 天基线(Prometheus):
heap = 3072m (MaxRAMPercentage=75 × 4Gi)
native_baseline = max(7d) of (container_memory_working_set_bytes - heap_bytes)
# 即 Metaspace + 线程栈 + Netty + JNI + glibc arena
# 你的场景估 300~450m(120 业务线程 × 1m 栈 + Netty 24 线程 + metaspace 256m + 其他)
sidecar_limit = 256m (log-agent 的 limit,不是 request,因为 limit 才是它真实上限)
reclaim_margin = max(300m, p99(file_cache at steady state))
# file cache 是 anon 之外的可回收部分,内核 reclaim 从这里下手
high = 3072 + 400 + 256 + 400 = 4128m
但 high < max 是硬约束,4Gi = 4096m,你们这个规格下 high 设不出有意义的值——这正是问题所在:Guaranteed 4Gi 全给主容器,sidecar 的 256m 已经在 max 之内。所以第一步不是设 high,而是扩 max:
安全操作顺序(5 步,每步观察 ≥ 30min):
| 步骤 | 动作 | 观察指标 | 回滚条件 |
|---|---|---|---|
| ① | 扩 Pod memory limit 4Gi → 5Gi(rolling,PDB maxUnavailable=1) | container_memory_working_set_bytes 稳态 | OOMKilled 事件增加 |
| ② | 保持 MaxRAMPercentage=75,JVM 堆自动变 3.75Gi;或显式 -Xmx3072m 锁死堆,把多出的空间留给 native + cache | jvm_memory_used_bytes(MBean)稳定,native 部分 = working_set - heap | 堆 OOM |
| ③ | 采集 24h 的 native_baseline + file_cache p99,代入公式算 high | — | — |
| ④ | 写 memory.high(用 sidecar 写 cgroupfs,见下) | PSI memory.pressure full avg10、memory.events high 计数 | PSI full > 10% 持续 60s(引 [2] 的告警阈值) |
| ⑤ | 观察 24h 无 5xx、PSI 回落后,再灰度下一批 | memory.events high 增量速率 | 5xx 率 > 基线 2× |
为什么不能先写 high 再调 JVM:你们踩到的「reclaim 风暴」不是 bug,是时序问题。内核发现 current > high 后立刻触发 direct reclaim,但 JVM 的 GC 响应要等下一个 safepoint(G1 正常 100ms~1s,但高负载下 safepoint 等待能到 5~15s)。这个窗口里内核在做 futile reclaim(反复尝试回收 anon 页),P99 打到 400ms 就是这个窗口。
2. sidecar 挤占的量化口径
关键事实:cgroup v2 中 sidecar 和主容器共享 Pod cgroup,memory.current 是 Pod 级别的,不存在「主容器视角」的 high——high 是写在 Pod cgroup 上的。所以公式里必须把 sidecar 的 limit 算进去(上面已含 256m)。
但有一个更精细的做法:如果你们用 cgroup v2 的 sub-cgroup 结构(containerd 1.7 默认每个容器一个 sub-cgroup),sidecar 的 page cache 只计入它的 sub-cgroup,不会挤占主容器的 headroom。你可以验证:
# 在 Pod 内
cat /sys/fs/cgroup/pod-xxx/main/memory.current
cat /sys/fs/cgroup/pod-xxx/log-agent/memory.current
# 两者之和 ≈ Pod 级 memory.current
如果 sub-cgroup 结构存在,sidecar 的 256m 只影响 Pod 级 memory.max,不影响主容器的有效 headroom——你的 high 公式里 sidecar 项可以改为 sidecar_limit × 0.5(保守估计它实际用一半)。
3. 滚动重启期间容量缺口
PDB + HPA 追不上是已知问题(HPA scale-up 需要 ≥ 60s,而 PDB 限了 maxUnavailable=1 时滚动速度被卡死)。更稳的做法:
- 预热池:在发布窗口前手动 scale 出 2 个备用副本(HPA 之外),发布时先缩备用池再滚动,把「发布窗口」和「扩容窗口」解耦;
- 金丝雀:先滚 1 个 Pod(PDB maxUnavailable=1),观察 10min 无异常再放开 PDB 批量滚;
- cgroupfs 写入不需要重启:如果你们已经有 DaemonSet 或 sidecar 能写
/sys/fs/cgroup/.../memory.high,存量 Pod 不需要重启就能生效——你们说的「init 容器对存量不生效」是对的,但 sidecar 或特权 DaemonSet 写 cgroupfs 是热生效的,只是 K8s 没有原生 API。
4. 观测:soft 限长期触发的信号
除了 PSI,还有两个更直接的信号:
| 信号 | 来源 | 告警阈值 | 含义 |
|---|---|---|---|
| memory.events 的 high 计数增量 | cat /sys/fs/cgroup/.../memory.events | 增量 > 0 且持续 5min | high 被持续触发,JVM 在反复和内核抢内存 |
| PSI memory.pressure full avg10 | 同文件 | > 10% 持续 60s | 有线程被强制脱机,P99 正在恶化(引 [2] 的阈值) |
| GC 停顿时间 p99 | JMX / Prometheus jvm_gc_pause_seconds | 比基线高 50% | JVM 在 high 压力下 GC 变慢,是 OOM 的前兆 |
不要只看 PSI some:some 在正常负载下就 >0(任何分配都有 some),只有 full 才代表「有线程被卡住」。
5. Operator / Webhook 生产实践
社区里有 k8s-memory-high-operator(Go 写的,MutatingWebhook 注入 sidecar 写 cgroupfs),生产坑主要有三个:
- sidecar 生命周期:sidecar 必须在主容器之后启动(K8s 1.28 没有 native sidecar),否则 cgroup 路径还没建好;
- 容器重启后 cgroup 路径变化:containerd 重启容器会重建 cgroup,sidecar 要监听 inotify 重新写 high;
- 权限:sidecar 需要
CAP_SYS_ADMIN或 privileged,安全团队会卡这里。
如果不想引入特权 sidecar,DaemonSet + node 权限写 cgroupfs 是更轻的方案(DaemonSet 不需要 Pod 内特权),代价是要维护 Pod → node → cgroup 路径的映射。
一句话总结:你们 4Gi 规格下 high 没有余量空间,第一步是扩 max 到 5Gi,然后用 heap + native + sidecar + file_cache_p99 算 high,用 sidecar 热写 cgroupfs(不需要重启),告警盯 memory.events high 增量 + PSI full > 10% 持续 60s。
从指标体系搭建角度补一个「观测口径」的量化方法:
sidecar 挤占的精确建模:提问者提到 log-agent 的 page cache 计入同一 Pod cgroup 的 memory.current,这个判断对,但量化上可以用 cgroup.stat 里的 file 和 anon 字段拆分:
# 在 Pod 容器内(需要 cgroupns 权限)
cat /sys/fs/cgroup/memory.stat | grep -E '^(anon|file|slab_unreclaimable)'
cat /sys/fs/cgroup/memory.current
关键公式:high = anon_JVM + anon_sidecar_max + file_reclaimable_p99 + margin,其中 file_reclaimable_p99 取 7 天 memory.stat 里 file 字段的 P99(不是 max,max 可能是一次性刷日志的尖峰)。提问者 4Gi 规格下,如果 file 稳态 P99 是 350Mi,那 high 的下限就是 3Gi(heap) + 350Mi(file) + 256Mi(sidecar_anon) + 100Mi(margin) ≈ 4.06Gi——确实超了 4Gi limit,印证了「先扩 max 再设 high」的顺序。
实操建议:写 high 之前先跑 24h 的 memory.pressure PSI(/sys/fs/cgroup/memory.pressure,内核 5.15 已支持),看 some 和 full 的 avg10 基线。如果稳态 full avg10 已经 > 0,说明当前 limit 下内核已经在持续 reclaim,这时候设 high 只会加剧问题,应该先扩 limit。如果 full avg10 为 0 而 some avg10 > 0,说明有轻度压力,high 设到 current + 20% 是安全的起点。PSI 数据比 RSS 更有预测力,因为 PSI 度量的是「线程被阻塞等内存」的时间比例,直接对应 P99 抖动。