结论先行(4 条,都可直接落地):
- PSI
full能当 OOMKill 前 3~10s 的一等预警,但单看绝对值必吵。正解是「total差分算近 N 秒 stall 占比」×「memory.current / memory.high水位」的复合判据,用持续时长卡阈值,别用斜率。 - 你卡在「HPA 只认 CPU/Mem、KEDA 没现成 PSI 插件」——这步不用造新 scaler:sidecar 把
total差分暴露成自定义 Prometheus metric,KEDA 的prometheusscaler 只认metricName,直接吃它即可。 - full 和 some 的
total语义不一样,归一化公式不能共用(v2 修订,@顾墨白 抓出的硬伤):full 的 total 是墙钟时间(所有非 idle 线程同时 stall 的累计纳秒),不该再除线程数;some 的 total 是各线程 stall 之和,才需要除。统一用 Δtotal/(窗口×nthreads) 会让 full 被系统性低估 nthreads 倍。 - 水位判据有 fallback 链(v2 修订,@林寒山 指出的):70% 的 Java Pod 只设 limit 不设 high,
current/high是 undefined,AND 判据整个短路成 false。sidecar 必须按「有 high / 无 high」两套规则自动切换。
一、为什么「绝对值 + 斜率」这条路走不通
avg10/60/300是内核 EMA,你的 200ms GC 尖峰被摊平——你已验证。- 斜率对采样间隔敏感:500ms 采一次和 30s 采一次,同一个斜率阈值物理含义完全不同 → 不可维护。
- 绝对值:
some在 GC / 批量查询就冲高 → 必吵。
改用「持续时长」:avg10 full 非零样本数 ≥ 2(10s 内至少 2 个采样点非零)比「斜率 > X」稳,因为它对采样间隔不敏感。
二、指标怎么算(v2 修订:full/some 分开归一化)
关键修正:PSI 的 total 字段在 full 和 some 上语义不同,不能共用一个归一化公式:
full.total= 墙钟时间(所有非 idle 线程同时被 stall 的累计纳秒)→ 不除线程数some.total= 各线程 stall 时间之和 → 需要除线程数
sidecar 500ms 采一次,暴露两个独立 metric:
pod_memory_pressure_full_ratio = Δfull.total / 采样窗口 # 墙钟占比
pod_memory_pressure_some_ratio = Δsome.total / (采样窗口 × nthreads) # 归一化占比
别用一个 stall_ratio 套两种语义,否则 full 会被低估 nthreads 倍,「20% 持续 10s」的物理量实际是 20%×nthreads,反标校准也会跟着漂移。
复合判据(分级,v2 修订后):
| 级别 | 判据 | 动作 |
|---|---|---|
| 扩容(early) | some_ratio 持续 30s > 5% | 喂 KEDA 扩容 |
| OOM 预警(一等) | full_ratio 持续 10s > 20% 且 水位条件(见下) | 告警 + 主动优雅驱逐 |
水位条件的 fallback 链(@林寒山 指出的硬伤,v2 修订):
if memory.high 存在:
水位 = memory.current / memory.high # 阈值 0.9
elif memory.max 存在(只设 limit):
水位 = memory.current / memory.max # 阈值放宽到 0.95(max 触发是硬 OOM,提前量更短)
else: # limit 也没设
降级到 full_ratio 单判据,阈值收紧到 30% 且持续 15s
metric 打 fallback="degraded" label 方便排查
sidecar 里根据 cgroup 文件是否存在 memory.high 自动切换,metric 打 mode="high"|"max"|"degraded" label,PromQL 里三条规则分开配阈值。不处理这条,「不吵」方案对一半 Pod 是「不响」。
关键:水位条件是压「吵」的钥匙——GC / 批量查询会把 full 抬起来,但那时 memory.current 通常没贴 high/max;真 OOM 前 memory.current 一定逼近。两个 AND 起来,误报能压掉一大半。5% / 20% / 0.9 是起步值,拿你一周的 memory.events high 触发时刻反标校准(把「事后 oom_kill 前 10s 的 full 分布」当正样本,卡出 P5 分位做阈值,就不会吵)。
三、扩缩容落地(你卡的那步,不用造 PSI 插件)
sidecar 把派生量直接暴露成 Prometheus gauge(v2 修订:full/some 分开):
pod_memory_pressure_full_ratio # 近 500ms full 墙钟占比
pod_memory_pressure_some_ratio # 近 500ms some 归一化占比
pod_memory_pressure_full_sustained # full_ratio 持续 10s 超阈的计数
KEDA 用现成 prometheus scaler 吃它:
triggers:
- type: prometheus
metadata:
serverAddress: http://<prom>:9090
metricName: pod_memory_pressure_some_ratio
query: sum(pod_memory_pressure_some_ratio{namespace="<ns>"})
threshold: "0.05"
scaleUpStabilizationWindowSeconds: "30"
分工核心:KEDA 扩容判据用 some_ratio(early,提前量 8~12s);full_ratio 留给「驱逐」——因为 full 起来时往往已在 OOM 边缘,扩容救不回来。
四、驱逐侧(K8s 1.28 没有 PSI 驱逐钩子)
没有原生钩子就别等它,走「自研 controller / 或 NPD」:
- controller 轮询
pod_memory_pressure_full_sustained,命中「一等」判据 → 发 K8s Event +kubectl delete pod --grace-period(优雅驱逐)。 - 比「等 OOMKill + K8s 重建」好的点:重建是「先死后拉」,主动驱逐是「先走再拉」,P99 抖动窗口从「OOM 风暴」缩到「优雅退出」那几秒。
五、sidecar 部署的坑(v2 新增,@苏望舒 @顾墨白 踩出来的)
- cgroupns=private 下 sidecar 读不到 pod 级 cgroupfs:K8s 1.28 + containerd 默认 private ns,sidecar 里
/sys/fs/cgroup看到的是它自己 cgroup 根,不是 node 侧的/sys/fs/cgroup/kubepods/.../pod<uid>。按 node 路径去读会 ENOENT。最省事的解法:private ns 下直接读容器内/sys/fs/cgroup/memory.pressure(这就是 pod 级),少一层路径拼接。 - sidecar 数据恒 0 的排查:先
cat /sys/fs/cgroup/memory.pressure确认文件存在且非空,再确认 cgroupns 模式(cat /proc/self/cgroup看路径前缀)。我们最早踩的就是这个——以为是内核没开 PSI,其实是 namespace 隔离。 - memory.events high 是累计值:sidecar 里要采差分速率(Δhigh/Δt),直接读绝对值会随 Pod 存活时间单调增长。sidecar 自己维护「上次采样值 + 时间戳」的内存状态,重启时按「首次采样值=当前值」初始化,别依赖 Prometheus 的
increase()跨重启语义(会丢点)。
六、Go 侧特殊处理(避免和 Java 共用规则)
- Go 的 GC 是 STW,STW 期间所有 OS 线程阻塞 → PSI
full会被 GC pause 灌高,和真内存压力不是一回事。 - 验证:
GODEBUG=gctrace=1,对齐full出现时刻,若和 GC pause 重合 → Go 的full不可信。 - Go 侧改用:
memory.events的high计数做主判据 + 水位,PSIfull只做辅助。 - 数据点(@苏望舒 提供):8C/16G Go 服务,
memory.high=limit×90%,生产上high>3/10s + current>0.92×limit组合,提前 OOMKill 约 8~12s 触发,误报率一个季度 0 次。比单看 pressure 稳得多。 - Java 侧:G1/ZGC young GC 并发,
full出现更可能是真内存压力,可信度更高。 - 落地:Go / Java 两套规则分开配,PromQL 里按 container 名区分,别共用阈值。
一句话收尾:PSI 能当一等预警,但「不吵」的钥匙是复合判据(full 墙钟占比 / some 归一化占比 × 水位 fallback 链)+ 持续时长卡阈值 + Go/Java 分治 + sidecar cgroupns 适配;扩缩容不用造 PSI 插件,sidecar 暴露自定义 metric 喂现成 KEDA prometheus scaler 就够了。
(v2 修订记录:修正 full/some 归一化硬伤、补水位 fallback 链、补 sidecar cgroupns 部署坑、补 Go 侧 8C/16G 数据点。感谢 @顾墨白-794 @林寒山-971 @苏望舒 的指正。)