working_set 顶到限额 → 内核 memcg 直接 SIGKILL:不打招呼、不留遗言(日志空白)。现场三联:容器重启 + 一波 5xx + 用户掉线。覆盖 容器盘 k1 / k2 / k5 / k6 · 内存盘 m128(rag 有前科)。
自助餐厅规矩:每人限取一盘。你叠了三盘,服务员不会来劝你少吃点——直接请你出餐厅。容器世界一模一样:每个容器被 cgroup 配额管家划了内存限额(backend 10G、rag 2G……),working_set 顶到限额那一刻,内核的 memcg 子系统触发 OOM kill,进程收到 SIGKILL——无法捕获、无法优雅退出,不打招呼、不留遗言。
「不留遗言」是 OOM 最坑的地方:优雅退出时程序来得及写日志、发告别指标;SIGKILL 是当场击毙,日志常常停在半句话、什么 stack trace 都没有。所以 OOM 的破案现场不在日志里,在曲线里:k5 的重启线跳一下、k1 的使用线此前一直贴着限额爬、m128 的 rag 内存冲高不回落。现场三联——容器重启 + 一波 5xx + 用户掉线——同时出现,基本就是实锤。
先理解「限额」从哪来。容器不是整台机器,是被 Linux 的配额管家 cgroup 圈出来的地盘:内存给你划 2G、CPU 给你划几核。这不是机器没资源了,是你这桌的份就这么多——自助餐「每人一盘」是店规,不是厨房没菜。四两的店规是:backend 10G / biz 3G / rag 2G / review 2G / frontend 1G,每个服务两台机器各一个容器。
然后在你的地盘里,内存用量(working_set,上一课讲过——地板上摊开的东西)会随业务波动:rag 解析一个大文档,内存蹭蹭往上涨。涨了不可怕,可怕的是顶到天花板还不回头。此时内核不会发警告、不会给你时间清理——memcg 直接裁决 OOM,发出 SIGKILL。这个信号的特殊之处在于进程无法捕获:try/except 接不住,优雅关闭钩子不执行,日志线程来不及刷盘。于是现场留下经典一幕:日志的最后一行停在半句话。
枪响之后的三件事几乎同时发生:容器重启(管家把桌子重新摆好,但客人全被请走了)、一波 5xx(打到这个容器上的请求全断)、用户掉线(WS/SSE 连接全部断开,在线数骤降)。三联齐了就是 OOM 实锤。四两的 rag 有前科——曾被 LibreOffice 解析大文件顶爆 2G 限额——所以内存盘给它留了单独的体检单(m128):冲高属预期,关键看之后回不回落;冲高不回落还逼近 2G,它就是下一个受害者。
| 类比里的东西 | 系统里对应的东西 |
|---|---|
| 每人限取一盘(店规) | cgroup 内存限额:backend 10G / biz 3G / rag 2G / review 2G / frontend 1G |
| 叠到第三盘 | working_set 顶到限额(内核 OOM 的判决定理,见上一课) |
| 直接请出餐厅 | memcg OOM kill → SIGKILL:无法捕获、无法优雅退出 |
| 被请出时手里还端着菜 | 日志空白:来不及写遗言——破案靠曲线(k5 重启线 / OOM 事件) |
| 换桌重吃 | 容器重启:内存归零冷启动,期间服务不可用(k6 存活 2 → 1 → 2) |
| rag 的案底 | LibreOffice 解析大文件曾顶爆 2G 限额 → m128 单独盯防,冲高必须回落 |
# 判定链:working_set 逼近 limit → 顶到 → OOM(k1/k2 的观察对象) sum by (service) (container_memory_working_set_bytes)
# 日志空白时的破案三件套:重启线 + OOM 事件 + 使用率回放 # 顺序:k5 定时 → k1/k2 回放 → m128 找冲高源头
# 四两限额(compose cgroup,每服务两台机器各 1 容器) # backend 10G · biz 3G · rag 2G · review 2G · frontend 1G
# k5 第一条线:1h 内重启次数 changes(container_start_time_seconds[1h])
# k5 第二条线:枪声记录(版本不支持时无数据,用 k1/k2 互证) increase(container_oom_events_total[1h])
# m128 口径:rag / review 每实例 RSS(1m 窗口最大值) max_over_time(siliang_worker_process_resident_memory_bytes{job=~"siliang-rag|siliang-review"}[1m])
# k6 口径:按 service 点名,预期 2 count by (service) (container_start_time_seconds{service=~"siliang-.*"})
# 内存盘核心告警口径:持续爬升先报警,别等顶到限额 deriv(siliang_process_resident_memory_bytes[1h]) # 持续 > 5MB/h = 真累积
① 容器内存使用 vs 限制(k1)——working_set 实线 vs 限额虚线一实一虚,使用线贴上限制线 = memcg OOM 倒计时:🟡 容器内存使用 vs 限制 ↗
② 内存使用率(k2)——working_set ÷ limit 百分比版,80% 黄 / 90% 红,>90% 持续 5m 应告警——OOM 前的最后窗口:🟡 内存使用率(working_set / limit)↗
③ 容器重启与 OOM 事件(k5)——枪声记录仪:changes 重启次数 + increase OOM 事件;发布窗口外的重启 + OOM 事件出现 = 实锤:🟡 容器重启与 OOM 事件 ↗
④ RAG / Review Worker 内存(m128)——有前科者的单独体检单:rag 冲高回落属预期,不回落 + 逼近 2G = 下一个受害者:👷 RAG / Review Worker 内存 ↗
⑤ 各服务存活实例数(k6)——点名器:预期 2;1 黄 = 单机在撑(另一台被杀还没起来),0 红 = 服务整体不可用:🟡 各服务存活实例数 ↗
半夜被告警吵醒,症状是服务闪断 + 一波 5xx。按顺序走四步:定时(k5)→ 回放(k1/k2)→ 点名(k6)→ 找源头(m128),十分钟内把「是不是 OOM、哪台、为什么」说清楚。
# 第一步:定时——哪个 service 重启了?有没有 OOM 事件(k5) changes(container_start_time_seconds[1h]) by (service) increase(container_oom_events_total[1h]) by (service) # 第二步:回放——被杀前使用率是不是贴着 90% 爬(k2 口径) sum by (service) (container_memory_working_set_bytes) / clamp_min(sum by (service) (container_spec_memory_limit_bytes), 1) # 第三步:点名——现在还剩几个容器在撑(k6,预期 2) count by (service) (container_start_time_seconds{service=~"siliang-.*"}) # 第四步:找源头——rag 是不是大文件冲高不回落(m128 口径) max_over_time(siliang_worker_process_resident_memory_bytes{job=~"siliang-rag|siliang-review"}[1m])
判读收尾:重启 + OOM 事件 + 被杀前贴 90% = OOM 实锤;接下来按场景 4 防复发。
一条告警抓「快被杀」,一条抓「已经被杀」。前者让你有时间止损,后者确保枪响必被知道(哪怕日志一片空白)。
groups: - name: siliang-oom rules: - alert: ContainerMemNearLimit expr: | sum by (service) (container_memory_working_set_bytes) / clamp_min(sum by (service) (container_spec_memory_limit_bytes), 1) > 0.9 for: 5m # 持续 5 分钟:OOM 倒计时(口径以 k0 建议为准) labels: severity: critical annotations: summary: "{{ $labels.service }} working_set 超 90% 持续 5m:OOM 倒计时" - alert: ContainerOomFired expr: increase(container_oom_events_total[5m]) > 0 labels: severity: critical annotations: summary: "{{ $labels.service }} 发生 OOM kill,立即走定位 SOP"
k5 的重启线跳了不一定是事故:滚动发布本来就会重启。判断方法是拿重启时刻对照发布窗口,窗口外 + 无 OOM 事件记录的奇怪重启才另立案子。
# 重启发生的准确时刻(把时间戳转成可读时间) changes(container_start_time_seconds[1h]) by (service) # 互证一:发布窗口内每个旧 worker 的 ops 排空线应逐个归零(c18 口径) sum(siliang_ops_http_in_flight) by (instance) # 互证二:OOM 事件为 0 + 重启在发布窗口内 = 计划内,别误报 increase(container_oom_events_total[1h]) # 判读:窗口外重启且 OOM=0 → 查崩溃日志;OOM>0 → 走 OOM SOP
有前科就要重点帮教:一看冲高回不回落(m128)、二看护栏拦了多少超大文件(m114 上限拦截)、三看爬升斜率(deriv 提前报警)。
# 一看:冲高后回不回落(m128 口径,关键形态) max_over_time(siliang_worker_process_resident_memory_bytes{job="siliang-rag"}[1m]) # 二看:斜率——持续爬升提前报警(内存盘核心告警口径) deriv(siliang_worker_process_resident_memory_bytes{job="siliang-rag"}[1h]) # 三看:护栏工作频率——上传/下载预检拦截了多少超规行李(m114 口径) rate(siliang_upload_precheck_rejected_total[5m]) + rate(siliang_media_download_rejected_total[5m]) # 判读:deriv 持续为正 + 逼近 2G = 提前介入(限流大文件/扩限额/重启预案)
每周例行看一眼「谁的盒子最满」。余量 = 限额 − working_set,排行最小的两个优先复查它们的 deriv。
# 五个容器的剩余空间(字节),按紧张程度排序 topk(5, sum by (service) (container_spec_memory_limit_bytes) - sum by (service) (container_memory_working_set_bytes) ) # 读法:backend 10G 余量最多属正常;rag/review 2G 盒子最小最紧; # frontend 1G 只跑静态页面,余量骤降反而最可疑(查有没有人塞了大东西)
# 错: 日志无 stack trace → 结案「未知原因闪断」 # 对: changes(container_start_time_seconds[1h]) + increase(container_oom_events_total[1h])
# 错: 重启线跳 1 → 直接 @所有人 # → 可能只是滚动发布 # 对: 先看 increase(container_oom_events_total[1h]) 再定性质
# 错: container_memory_usage_bytes / limit > 0.9 当告警依据 # 对: container_memory_working_set_bytes / limit > 0.9(k2 口径)
# 错: 容器重启成功 → 关单 # → 必复发 # 对: 关单前回答:deriv 回零了吗?冲高源头找到了吗?
# 错: k1 无数据 = 不会 OOM # → 没电表不等于没用电 # 对: 过渡期盯 deriv(siliang_worker_process_resident_memory_bytes[1h])
# 错: OOM 事件线空白 → 「我们从未被 OOM 杀过」 # 对: 三件套互证:重启线 + 使用率回放 + m128 冲高史
# 错: expr: working_set/limit > 0.9 # → 毛刺也响 # 对: 同款 expr + for: 5m # → 只抓持续贴线
参考答案:因为内核杀进程用的是 SIGKILL——这个信号进程无法捕获、无法处理,写日志、flush 缓冲、执行优雅关闭钩子的机会全都没有,日志常常停在半句话。所以 OOM 的破案现场不在日志里,在监控曲线里:k5 的重启线、OOM 事件计数、k1/k2 的使用率回放,这三样不会说谎。
参考答案:容器重启 + 一波 5xx + 用户掉线。内存被顶爆的瞬间进程被当场击毙:容器必须重启(k5/k6 可见)、打到这个容器上的请求全部失败(c17 出现 5xx 尖峰)、挂在上面的 WS/SSE 连接全断(c1 在线数骤降)。三条由同一次击毙同时引发、时间戳完全重合——这种「一因三果」的组合别的故障很难伪造。
参考答案:rag。它用 LibreOffice 解析大文件,处理时内存冲高是业务天性,历史上曾把 2G 限额顶爆被内核杀过。平时盯内存盘 m128:冲高本身属预期,关键是之后回不回落——冲高不回落且逼近 2G 限额,就是下一个受害者的预告;配合 deriv 斜率和 m114 上限拦截面板一起看。
参考答案:因为业务高峰或大文件过手时,working_set 会在几秒内冲过 90% 然后自己回落——这是正常的呼吸,不是 OOM 倒计时。不加 for 的告警会让毛刺天天狼来了,最后没人再理会告警。持续 5 分钟贴着 90% 不下来,才说明内存真的下不去——那才是动手止损的最后窗口。