🔴 后台生成任务:内存泄漏核心哨兵(核心盘 5 图)

任务只进不出就是泄漏——后厨未出餐的订单、Redis 里的租约、跨实例的收尸。c7 是整个仪表盘最重要的一个数。覆盖核心盘 🔴 组 5 张面板(c7–c11)· 后端核心仪表盘 siliang-backend。

定时续约 过期=死亡 租约过期 进≈出 只进不出 后台生成任务的生命周期 —— 订单、租约与收尸(泄漏哨兵) 用户点单 发起后台生成任务 Redis 租约台账 心跳+遗嘱:到点续约 没续上 = 视为你死了 backend 后厨 · 活跃任务台账(Gauge,c7 / c8) 任务创建(进) 记账:active +1 处理中(在途) 生成中:攥着上下文内存 正常出餐 completed outcome=completed(c9) 厨房着火 exception outcome=exception(要重做) 未出餐订单 = active_generation_tasks:<20 绿 / 20–50 黄 / >50 红 健康:进 ≈ 出(锯齿) 稳态个位数~几十;<20 绿 / 20–50 黄 / >50 红 泄漏:只进不出(爬升) 持续单调增长 = 泄漏复发 → 立刻看 c8 对照内存仪表盘 RSS 形态互证(m101) 跨进程收尸(c11) 实例崩了 → 别的实例替它收尾 stale_finalized:偶发为 0 属正常 事故现场(历史教训) 后台生成任务曾经只进不出,把内存吃爆 所以 c7 是本仪表盘最重要的数:泄漏修复后的复发哨兵 结局分布 c9:completed 出餐 / exception 着火;失败重试会再占一遍内存 租约失败原因(c10):redis_error = Redis 不稳 · missing_lease = 租约逻辑 bug · lease_expired_failed = 清理逻辑异常 多实例协作三件套:租约判生死(续约)→ 收尸兜底(stale)→ 失败原因(reason 标签)指向病灶 大数字(c7)给"现在",曲线(c8)给"往哪走"——判泄漏只认曲线形态

💡 一句话理解

把后台生成任务想成后厨订单:用户点单(任务创建)→ 灶上炒菜(处理中)→ 出餐(completed)或着火(exception)。c7 的大数字就是"后厨此刻有多少张未出餐的订单"——它是 Gauge,健康时围绕一个低位波动(进多少出多少),只进不出就是泄漏。这些任务曾经真的只进不出、把内存吃爆,所以 c7 被称为内存泄漏核心哨兵:修复之后,它就是防复发的警报器。

多台机器抢着干活,怎么知道"这单归谁、那人死没死"?答案是租约(Redis 里的心跳+遗嘱)和收尸(实例崩了别人替它收尾)。c10 盯租约机制本身坏没坏,c11 盯有没有实例在异常退出——它们长期为 0 才是健康。

🧩 费曼拆解 讲给完全没接触过的小白

先看后厨本厨。每一单从下锅到出餐都要占一个灶位、占一套锅碗——这就是"任务攥着上下文内存"。生意好的时候,后厨里同时有十几张单子在炒,单子数上下波动,但进多少、出多少,收盘时灶位清空。有一天出了个 bug:出的菜没被记账,订单永远显示"处理中"——单子越积越多,灶位全满,最后整个后厨(worker 内存)被撑爆。这就是历史事故:"这些任务曾经只进不出,把内存吃爆"。修好之后,c7 这张"未出餐订单数"就成了哨兵:数字再爬起来,就是复发。

再看着火的单子。厨房着火(exception)的单子不能端给客人,要倒掉重做——而重做会再占一遍灶位。所以 c9 里 exception 比例高,不只是"失败率高",还会连累内存:失败重试会反复占内存,和 RSS 曲线对时间戳能看出联动。

最后是多店协作。连锁后厨(4 个 worker 多实例)怎么分单?每张单在 Redis 里立一张租约——像共享单车扫码开锁:"这单归你 30 秒,到点前必须续约报平安;你不续,系统默认你死了,把车(任务)收回来给别人"。续约失败的原因(reason 标签)直接指向病灶:Redis 挂了(redis_error)、租约逻辑写错了(missing_lease)、清理逻辑异常(lease_expired_failed)。而如果一家分店半夜塌了(实例崩了、租约过期),它手里的烂尾单不能永远悬着——由别的分店替它收尾,这就是"收尸"(c11)。偶尔收一次是常态,经常收 = 公司在批量倒闭(实例在异常退出)。

类比里的东西系统里对应的东西
后厨未出餐的订单数c7 活跃生成任务数(Gauge):稳态个位数~几十,<20 绿 / 20–50 黄 / >50 红
订单只进不出内存泄漏复发:c7 持续单调增长,历史教训是"把内存吃爆",所以它是复发哨兵
出餐 vs 厨房着火c9 结局分布:outcome=completed / exception,配 rate 按 outcome 拆开
共享单车扫码续费c10 租约:谁活着谁续约;续约失败按 reason 拆(redis_error / missing_lease / lease_expired_failed)
同事离职别人收尾c11 跨进程收尸:实例崩了/租约过期后,别的实例替它把烂尾任务收尾
大数字是照片,录像才见方向c7 给"现在多少",c8 曲线给"往哪走"——判泄漏只认曲线形态(锯齿 vs 斜线)

🛠 动手验证(4 步,在 Grafana 里亲手做一遍)

  1. 看 c7 大数字——对照背景色区间(<20 绿 / 20–50 黄 / >50 红),记住"稳态个位数~几十"这个锚点。
  2. 把 c8 拉到 6h 看形态——健康是围绕某个值上下波动的锯齿;一条只涨不跌的斜线才是泄漏。
  3. 看 c9 有没有 exception 冒头——偶尔一根刺正常,持续一堆 = 频繁失败,去翻日志。
  4. 确认 c10、c11 贴地——这两张图长期为 0 才健康;任何一条线冒头都说明"协作机制本身"出了问题。

🧠 必知必会 看懂本课全部面板的地基

活跃任务数是 Gauge
未出餐订单数可进可出,是温度计不是里程表——直接读数,别配 rate。
# 对:Gauge 直接读(c7 口径)
sum(siliang_chat_resume_active_generation_tasks{job="siliang_backend_worker"})
大数字 vs 曲线
c7 只给"现在多少",c8 才给"往哪走"。判泄漏必须看曲线形态:锯齿=健康,斜线=泄漏。
# 同一个数的曲线版(c8 口径),看 6h 形态
sum(siliang_chat_resume_active_generation_tasks{job="siliang_backend_worker"})
泄漏=只进不出
每个任务攥着一份上下文内存;只进不出 = 灶位全满 = 内存吃爆。这是历史真实事故,不是假设。
# 复发判据:曲线只涨不跌 + 内存盘 RSS 线性爬升互证
deriv(siliang_process_resident_memory_bytes[1h])  # >5MB/h = 真累积
outcome 结局分布
订单结局是 Counter(_total),配 rate 按 outcome 拆:completed=出餐,exception=着火。
# Counter 配 rate(c9 口径)
sum(rate(siliang_chat_resume_generation_task_outcome_total[5m])) by (outcome)
租约(心跳+遗嘱)
多实例分单靠 Redis 租约:到点前必须续约报平安,没续上视为死亡、任务转移。
# 续约失败 + 过期清理失败,按 reason 拆(c10 口径)
rate(siliang_chat_resume_lease_refresh_failed_total[5m]) by (reason)
  + rate(siliang_chat_resume_lease_expired_failed_total[5m])
reason 三分类
续约失败的 reason 直接指向病灶:redis_error=Redis 不稳;missing_lease=租约逻辑 bug;lease_expired_failed=清理逻辑异常。
# reason 读法:先分清"环境坏了"还是"代码坏了"
rate(...lease_refresh_failed_total[5m]) by (reason)  # 健康时长期为 0
收尸(stale finalized)
实例崩了/租约过期后,别的实例替它收尾——收尸计数偶发为 0 正常,持续走高 = 有实例异常退出。
# 收尸计数器(c11 口径)
sum(rate(siliang_chat_resume_stale_finalized_total[5m])) by (reason, status)
job 过滤
只信 job=siliang_backend_worker:共享端口的 server job 会双计数,所有面板已排除。
# 不带过滤 = 混入双计的 server job
{job="siliang_backend_worker"}   # 唯一可信口径

📋 逐面板精讲 5 张图一张不落

🔴 c7 · 后台生成任务数(内存泄漏核心监控)

它是什么:后厨此刻有多少张"未出餐的订单"——所有 worker 上的活跃后台生成任务总数(Gauge)。每张单都攥着一份上下文内存,这个数决定了后厨占多大的灶面。历史教训:这些任务曾经只进不出,把内存吃爆——所以它不是普通监控,是泄漏修复后的复发哨兵。

回答什么问题:泄漏修复后有没有复发?这是本仪表盘最重要的一个数。

✅ 稳态个位数~几十;<20 绿 / 20–50 黄 / >50 红

🚨 持续单调增长 = 任务只进不出 = 泄漏复发。动作:立刻去 c8 趋势图确认形态,再对照内存仪表盘 RSS(m101)互证。

# 面板 PromQL(照抄主图谱口径)
sum(siliang_chat_resume_active_generation_tasks{job="siliang_backend_worker"})

联动:主图谱 c7 ↗ · 相关课程:泄漏判定心法 · 内存全局水位

🔴 c8 · 后台任务趋势(持续上涨=泄漏)

它是什么:同一个数的曲线版。大数字(c7)是照片,只告诉你"现在多少";曲线才是录像,告诉你"它在往哪走"。判泄漏的关键证据全在这张图的形态里。

回答什么问题:任务是正常周转(有进有出),还是只进不出(泄漏复发)?

✅ 健康曲线围绕某个值上下波动(锯齿)

🚨 只涨不跌的斜线 = 泄漏。动作:对照内存仪表盘 RSS 形态互证——两条线同时爬升就是实锤,立刻按生产实战的 SOP 下钻。

# 面板 PromQL(照抄主图谱口径,看 6h 形态)
sum(siliang_chat_resume_active_generation_tasks{job="siliang_backend_worker"})

联动:主图谱 c8 ↗ · 相关课程:泄漏判定心法(锯齿/爬升/平台期) · 内存全局水位

🔴 c9 · 后台任务结束原因分布

它是什么:后厨订单的结局分布——正常出餐(completed)vs 厨房着火(exception)。结局是 Counter(_total 只增不减),配 rate 按 outcome 拆开,就是"每秒出几单、着几把火"的实时速率。

回答什么问题:后台任务失败率高不高?

✅ 稳态以 completed 为主,exception 偶发成刺

🚨 exception 比例高 = 频繁失败,翻日志。别忘了连锁反应:失败重试还会推高内存——把 exception 速率和 RSS 曲线对时间戳,常能看到联动。

# 面板 PromQL(照抄主图谱口径)
sum(rate(siliang_chat_resume_generation_task_outcome_total[5m])) by (outcome)

联动:主图谱 c9 ↗ · 相关课程:rate():算速度 · 结束原因:活干完了没

🔴 c10 · 租约刷新失败 + 过期清理失败

它是什么:分布式系统的"心跳+遗嘱"体检表。每个后台任务在 Redis 里立了租约(谁活着谁续约);这张图统计两件事的失败速率:续约失败(lease_refresh_failed)和过期任务清理失败(lease_expired_failed),并按 reason 拆开病灶。

回答什么问题:多实例协作机制本身有没有坏?

✅ 长期为 0

🚨 redis_error 高 = Redis 不稳;missing_lease 高 = 租约逻辑 bug;lease_expired_failed 高 = 清理逻辑异常。动作:先按 reason 分诊——环境问题查 Redis,逻辑问题提代码 bug 单。

# 面板 PromQL(照抄主图谱口径)
rate(siliang_chat_resume_lease_refresh_failed_total[5m]) by (reason)
  + rate(siliang_chat_resume_lease_expired_failed_total[5m])

联动:主图谱 c10 ↗ · 相关课程:连接池台账(Redis 池视角) · 视频收割器(同类机制)

🔴 c11 · 跨进程清理的过期任务数

它是什么:"收尸计数器"——某个实例崩了/租约过期后,由别的实例替它把烂尾任务收尾的次数。它是租约机制的兜底证据:有收尸,说明真的有实例死过。

回答什么问题:有没有实例在异常退出?

✅ 偶发为 0;active 稳定而 stale 高 = 有实例异常退出(对照发布记录)

🚨 stale 持续走高 = 实例在批量异常退出。动作:对照发布时间轴——发布窗口内属预期滚动;窗口外持续出现 = OOM/崩溃排查(转容器层 k5 与内存盘)。

# 面板 PromQL(照抄主图谱口径)
sum(rate(siliang_chat_resume_stale_finalized_total[5m])) by (reason, status)

联动:主图谱 c11 ↗ · 相关课程:容器层:盒子视角 · OOM kill:内存超限直接枪毙

🏭 生产实战 real world

场景 1 · 把复发哨兵变成告警(YAML 可直接抄)

阈值只来自核心盘口径卡:<20 绿 / 20–50 黄 / >50 红。黄区给缓冲,红区立刻响。

# 告警规则:后台任务积压(阈值来自核心盘 c7 口径卡)
groups:
- name: siliang-bg-tasks
  rules:
  - alert: BackgroundGenerationTasksWarning
    expr: sum(siliang_chat_resume_active_generation_tasks{job="siliang_backend_worker"}) > 20
    for: 10m          # 黄区持续 10 分钟才报,避开正常高峰的抖动
    labels:
      severity: warning
  - alert: BackgroundGenerationTasksCritical
    expr: sum(siliang_chat_resume_active_generation_tasks{job="siliang_backend_worker"}) > 50
    for: 5m           # 红区:泄漏复发嫌疑,立刻按 SOP 处理
    labels:
      severity: critical

判读收尾:告警触发后第一步永远是打开 c8 看形态,而不是先重启。

场景 2 · 泄漏复发排查 SOP(四步下钻)

从哨兵到实锤:先定性(形态),再量化(斜率),最后找同伙(协程)。

# 第 1 步:确认形态——大数字只给现在,曲线才给方向(c8 口径)
sum(siliang_chat_resume_active_generation_tasks{job="siliang_backend_worker"})
# 第 2 步:对照内存仪表盘 RSS 形态互证(m101 口径,锯齿/爬升/平台期)
max_over_time(siliang_process_resident_memory_bytes[1m])
# 第 3 步:量化爬升速度(内存盘核心告警口径:deriv > 5MB/h = 真累积)
deriv(siliang_process_resident_memory_bytes[1h])
# 第 4 步:任务不是唯一嫌疑人——顺手排除协程泄漏(m103 口径)
max_over_time(siliang_asyncio_tasks_active[1m])
# 判读:任务斜线 + RSS 爬升 = 实锤复发;任务稳而 RSS 爬 = 换嫌疑人再查

场景 3 · Redis 抖动定位:reason 三分诊

c10 冒头先别慌,reason 标签会告诉你该修环境还是修代码。

# 续约失败 + 过期清理失败,按 reason 拆(c10 口径照抄)
rate(siliang_chat_resume_lease_refresh_failed_total[5m]) by (reason)
  + rate(siliang_chat_resume_lease_expired_failed_total[5m])
# reason 读法:
#   redis_error          → Redis 不稳:先查 Redis 本身(池面板 m117/m118 互证)
#   missing_lease        → 租约逻辑 bug:提代码 bug 单
#   lease_expired_failed → 清理逻辑异常:查收尾代码
# 健康口径:两条线长期为 0;偶发成刺可容忍,持续非零必须处理

场景 4 · 发布后看收尸:旧实例死得干不干净

滚动发布必然换实例;收尸计数在发布窗口内偶发属预期,窗口外持续出现才是事故。

# 发布后 10 分钟看收尸计数(主图谱 #triage「发布后确认健康」口径)
sum(rate(siliang_chat_resume_stale_finalized_total[5m])) by (reason, status)
# 判读:active 稳定 + stale 偶发 = 旧实例干净退场
#       stale 持续走高 = 有实例异常退出 → 查容器重启/OOM(k5)与 RSS(m101)
# 配套确认:HTTP 在途发布后逐 worker 归零(c18 ops 排空口径)
sum(siliang_http_requests_in_progress) by (method) + sum(siliang_ops_http_in_flight)

场景 5 · 着火的单子:exception 高的两面性

exception 高不只是"失败率高"——着火的单要重做,重做还会再占一遍内存。

# 结局分布(c9 口径):着火速率
sum(rate(siliang_chat_resume_generation_task_outcome_total[5m])) by (outcome)
# 连锁检查:失败重试反复占内存——RSS 形态对时间戳
max_over_time(siliang_process_resident_memory_bytes[1m])
# 判读:exception 速率高的时段若 RSS 同步冲高 = 重试在放大内存压力
# 处置:先修失败根因(日志),必要时限流重试,而不是扩内存

⚠️ 常见坑 pitfalls

坑 1 · 只看 c7 大数字不看 c8 曲线 — 症状:大数字显示 35"黄区但还好",一小时后内存爆了。原因:大数字是照片,看不出"正在爬升"的方向。正解:判泄漏只认曲线形态,c7+c8 永远一起看。
# 错: 只盯 c7 当前值,低于 50 就安心
# 对: 看 c8 的 6h 形态——只涨不跌的斜线 = 泄漏
坑 2 · 把锯齿当泄漏 — 症状:看到曲线有涨有跌就报警"泄漏"。原因:健康状态就是围绕某个值上下波动(进多少出多少)。正解:锯齿=健康;只有"只涨不跌的斜线"才是泄漏,先看窗口拉长后的形态。
# 错: 曲线涨了两格就喊泄漏
# 对: 拉 6h 看整体形态:锯齿 vs 单调爬升,再定罪
坑 3 · 对活跃任务数求 rate — 症状:想算"任务增长速度"就上了 rate。原因:active tasks 是 Gauge,rate 只配 Counter。正解:Gauge 用 deriv 看斜率,或直接对比两个时刻的读数。
# 错: rate(siliang_chat_resume_active_generation_tasks[5m])
# 对: deriv(siliang_chat_resume_active_generation_tasks[1h])  # → 每小时涨多少
坑 4 · c10 三个 reason 混为一谈 — 症状:租约失败冒头,统一回复"Redis 有问题"。原因:三种失败指向三种病灶——环境、租约逻辑、清理逻辑。正解:按 by (reason) 分诊:redis_error 查环境,missing_lease / lease_expired_failed 查代码。
# 错: 看到 c10 非零就去重启 Redis
# 对: rate(...failed_total[5m]) by (reason)   # → 先分诊再动手
坑 5 · 把 stale 高当"任务太多" — 症状:c11 收尸计数走高,结论"加机器"。原因:收尸是"别的实例替崩掉的实例收尾",stale 高 = 有实例在异常退出,不是容量问题。正解:对照发布记录与容器重启/OOM 事件(k5),找"谁在死"。
# 错: stale 高 → 扩容
# 对: changes(container_start_time_seconds[1h])   # → 谁在重启(k5 口径)
坑 6 · 认为 active=0 才健康 — 症状:看到 c7 常年有个位数就以为"有泄漏残留"。原因:稳态就是"个位数~几十"——正常总有单子在灶上。正解:健康锚点是"<20 绿",且形态是锯齿;归零反而可能是服务没人用了。
# 错: 追求 active 恒等于 0
# 对: 锚点:稳态个位数~几十;<20 绿 / 20–50 黄 / >50 红
坑 7 · 忽略失败重试的内存代价 — 症状:exception 高只当"可用性问题",没意识到内存被反复占用。原因:着火的单要重做,重做再占一遍灶位。正解:exception 速率与 RSS 曲线对时间戳,联动确认。
# 错: 只看 c9 的失败率百分比
# 对: c9 exception 速率 × max_over_time(rss[1m]) 对时间戳互证
坑 8 · 忘加 job 过滤 — 症状:自建大屏的任务数比官方面板大一截。原因:共享端口的 server job 随机命中 worker 会双计数。正解:所有查询一律带 {job="siliang_backend_worker"}。
# 错: sum(siliang_chat_resume_active_generation_tasks)
# 对: sum(siliang_chat_resume_active_generation_tasks{job="siliang_backend_worker"})

🎓 费曼自测 合上书,能讲出来才算懂

Q1 · 为什么说 c7 是"本仪表盘最重要的一个数"?

参考答案:因为后台生成任务攥着上下文内存,历史上真的发生过"任务只进不出、把内存吃爆"的事故。修复之后,c7 就是防复发的哨兵:稳态个位数~几十,持续单调增长 = 泄漏复发。别的图告诉你"现在舒不舒服",这张图告诉你"上次的绝症有没有复发"。

Q2 · 租约机制解决什么问题?三个失败原因分别指向什么?

参考答案:多台机器分单时,一台崩了它手里的任务不能永远悬着。租约=带过期时间的许可证:"这单归你 30 秒,到点前续约报平安;没续上默认你死了,任务转给别人。"续约失败按 reason 分诊:redis_error=Redis 不稳(环境问题);missing_lease=租约逻辑 bug(代码问题);lease_expired_failed=清理逻辑异常(代码问题)。

Q3 · c8 上看到什么形态必须立刻行动?下一步看什么互证?

参考答案:只涨不跌的斜线(持续单调增长)——任务是只进不出的,泄漏复发。下一步对照内存仪表盘的 RSS 形态(m101):两条线同时爬升就是实锤;再用 deriv(RSS[1h]) 量化爬升速度(内存盘核心告警口径大于 5MB/h = 真累积),并顺手排除 asyncio 协程泄漏(m103)。

Q4 · "收尸"是什么?什么时候该警觉?

参考答案:实例崩了或租约过期后,由别的实例替它把烂尾任务收尾,这个次数就是收尸计数(c11)。偶发为 0 到偶发非零都正常;active 稳定而 stale 持续走高 = 有实例在异常退出——对照发布记录:发布窗口内属滚动预期,窗口外持续出现就要查容器重启与 OOM。

← 上一课:🟣 在线用户与协作规模(核心盘 6 图) 📚 课程目录 下一课:🟡 LLM 健康:大模型供应商门诊 →