👷 RAG / Review Worker 体检单(内存盘 3 图)

rag 与 review 是 backend 之外的两个独立小弟:各一个进程、各 2G 限额、各一本借书证(DB / Redis 池)。rag 干重活(LibreOffice 解析大文件)冲高属预期,关键看回不回落;review 池子小(独立池 5+5),慢任务一借就空。覆盖 m128 · m129 · m130 · 后端内存仪表盘「👷 RAG / Review Worker」组(内存盘 3 图)。

👷 两份独立体检单:rag 冲高看回不回落,review 小池防被借空 rag · 单进程 · 限额 2G RAG 检索 · LibreOffice 解析大文件 ⚠️ 有 OOM 前科(曾被打爆) 处理大文件冲高 → 回落 = 健康(锯齿) 本课体检单(3 张) m128 · 内存 RSS(每实例) m129 · 池使用率 m130 · 池借出数与上限 job=siliang-rag | siliang-review 单进程:每服务一条线 没有 per-worker 多线 review · 单进程 · 限额 2G 审阅服务 · 独立 DB 池 5+5 常备 5 overflow 5 慢任务占着书不还 → 很快借空 = 排队 对照:backend sync30/async80 · biz 10+20 RSS 判定:冲高之后,看回不回落 2G 限额线 回落 = 锯齿健康 不回落 + 贴限额 量化爬升速度(核心告警口径) deriv(rss[1h]) > 5MB/h = 每小时涨多少字节;持续爬升即告警 事故现场 · OOM 不打招呼 rag 贴 2G 限额 → memcg 直接 SIGKILL 不打招呼、不留遗言(日志空白) 现场三联:重启 + 5xx + 用户掉线 读图:rag 看形态(回落 or 不回落);review 看小池借出;两小弟的指标前缀是 siliang_worker_*,别拿 backend 的池面板来找它们。

💡 一句话理解

backend 之外还有两个「独立小弟」:rag(RAG 检索,要解析用户上传的大文件)和 review(审阅服务)。它们各自是单进程、各顶 2G 内存限额、各有自己的 DB / Redis 借书证。体检单看三样:体温计(RSS:rag 处理 LibreOffice 大文件时冲高属预期——关键看之后回不回落;冲高不回落+逼近 2G 限额 = 下一个 OOM 受害者,rag 有前科)、借书证使用率和借出数绝对值(review 是独立小池 5+5,特别容易被慢任务借空)。

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

先认清这两个小弟是谁。backend 是四 worker 的大店,rag 和 review 是两间独立小作坊:各只有一个进程、各分到 2G 的「房间面积」(容器限额)。因为单进程,它们的体检单没有 per-worker 多线——一个服务一条线,跟 backend 那种「四条线一起看」的画面完全不同。指标前缀也不同:siliang_worker_*,job 是 siliang-rag / siliang-review,别拿 backend 的面板来找它们。

rag 的体检重点是「冲高看回落」。它的工作就是用 LibreOffice 解析用户上传的大文档——解析期间内存必然冲高,这不是病,是干活的样子。干完活回落 = 健康(锯齿)。要警惕的是另一种形态:冲上去之后横住不回落,并且一步一步逼近 2G 限额线——这不是「正在干活」,这是「干完的活没收拾」。rag 是有前科的:它历史上就因为 LibreOffice 大文件被 OOM 杀过。OOM 不打招呼:working_set 顶到限额,内核直接 SIGKILL,日志空白,现场只剩「重启 + 5xx + 用户掉线」三联。所以「冲高不回落 + 贴限额」必须在被枪毙之前发现——这正是这张体检单存在的意义。

review 的体检重点是「小池借空」。它不像 backend 有 sync 30 / async 80 的大书架,只有一个独立小池 5+5(常备 5 + overflow 5)。小池的脾气是:平时够用,但慢任务一多——几条查询抱着连接不还——很快借空,后面所有请求排队,审阅接口集体变慢。看它就用和 backend 一样的两张卡(使用率 + 绝对值),只是脑子里要换算:这里的天花板是 5,不是 80。

类比里的东西系统里对应的东西
两个独立小作坊rag / review:backend 之外的两个单进程服务,各 2G 限额,job=siliang-rag / siliang-review
小弟的体温计siliang_worker_process_resident_memory_bytes(RSS,主口径;单进程每服务一条线)
干活的样子rag 用 LibreOffice 解析大文件时 RSS 冲高——属预期,关键看之后回不回落
干完没收拾冲高不回落 + 逼近 2G 限额 = OOM 倒计时(rag 被 LibreOffice 大文件 OOM 杀过,有前科)
小号借书证review 独立 DB / Redis 池,天花板 5+5;siliang_worker_db_pool_connections 等
爬升速度计deriv(rss[1h]) > 5MB/h:内存盘核心告警口径,把「在爬」量化成数字

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

  1. 打开 m128,只看 rag 那条线——找到最近一次冲高,放大时间轴:它回去了吗?回去了就是锯齿健康。
  2. 把 2G 限额换算成参照物——rag 的线离限额参考还有多远?若长期在低位徘徊,说明它健康;若冲高后台阶式抬升、一次比一次高,敲响警钟。
  3. 切到 m129/m130 看 review 的池——天花板是 5+5,不是 backend 的 80;看看慢查询时段借出数是不是顶平 5。
  4. 顺手确认图例的 job——这两张图的线来自 siliang-rag / siliang-review,没有 backend 的 worker 线;有,反而是配置错了。

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

两个独立小弟
rag(RAG 检索)与 review(审阅服务)是独立于 backend 的单进程服务,各自 2G 容器限额。单进程=每服务一条线,没有 per-worker 多线。
# 身份证(主图谱口径)
job=~"siliang-rag|siliang-review"   # 两个小弟的 job
# 限额:rag 2G · review 2G(容器层口径,OOM 判定看 working_set)
rag 的冲高属预期
LibreOffice 解析大文件必然吃内存——冲高是干活的样子,不是病。判定只看一件事:干完之后回不回落。
# 判定口径(面板原话)
# 冲高 → 回落 = 预期,锯齿健康
# 冲高 → 不回落 + 逼近 2G = 下一个 OOM 受害者(rag 有前科)
2G 限额与 OOM 前科
容器限额 rag 2G / review 2G;顶到限额内核直接 SIGKILL(memcg OOM),不打招呼不留遗言。rag 历史上被 LibreOffice 大文件打爆过。
# 现场三联(OOM 后只剩这些)
# 容器重启 + 一波 5xx + 用户掉线;日志空白,别指望遗言
deriv 量化爬升
「在爬」要变成数字才好告警:deriv(rss[1h]) 算斜率=每小时涨多少字节。这是内存盘核心告警口径之一。
# 核心告警(口径卡)
deriv(siliang_worker_process_resident_memory_bytes{job="siliang-rag"}[1h]) > 5MB/h
# → 持续爬升;[1h] 窗口抹掉抓取抖动
review 独立小池 5+5
review 不用 backend 的大书架,自己一本小借书证:常备 5 + overflow 5。慢任务一占就是五分之一,几条就借空。
# 池天花板对照(别背混)
# backend: sync 30 / async 80 / checkpointer 30
# biz:      10 + overflow 20 = 30
# review:   5 + 5                # ← 特别容易被慢任务借空
指标前缀 siliang_worker_*
两个小弟的池/RSS 指标都带 worker 前缀,与 backend 的 siliang_db_pool_*、siliang_process_* 是两套。找错前缀=图上永远没数据。
# 前缀对照
siliang_worker_process_resident_memory_bytes   # 小弟 RSS
siliang_worker_db_pool_connections             # 小弟 DB 池
siliang_worker_redis_pool_connections          # 小弟 Redis 池
池和内存互为因果
池被借空→请求排队→排队请求占着的内存迟迟不释放→RSS 冲高逗留更久。体检单三张要连着看。
# 互证顺序
# 接口慢 → 池借空(m129/m130)→ RSS 冲高逗留(m128)→ 回源:慢任务是谁

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

🟣 RAG / Review Worker 内存(每实例)

它是什么:rag 与 review 两个单进程服务的 RSS 体温计,每实例一条线。这是两个小弟最重要的体检项——因为它们各自顶 2G 限额,而 rag 干的活(LibreOffice 解析大文件)天生就在限额边上跳舞。

回答什么问题:两个小弟的内存现在什么形态?rag 这次冲高是「正在干活」还是「干完没收拾」?离 OOM 还有几个台阶?

✅ 健康长啥样:rag 处理大文件(LibreOffice 解析)时内存冲高属预期——关键看之后回不回落;回落形成锯齿 = 健康的干活节奏。review 平稳低位。

🚨 危险长啥样:冲高后不回落 + 逼近 2G 限额 = 下一个 OOM 受害者(rag 有前科:历史上被 LibreOffice 大文件 OOM 杀过)。动作:立即配合 deriv(>5MB/h) 量化爬升,定位卡住的大文件任务并处理,不等内核开枪。

# 主图谱口径:两个小弟的 RSS(1 分钟抹抖,job 正则圈住两家)
max_over_time(siliang_worker_process_resident_memory_bytes{job=~"siliang-rag|siliang-review"}[1m])

联动:主图谱 · RAG/Review Worker 内存 ↗ · 相关课程:OOM kill:内存超限直接枪毙 · 泄漏判定心法(冲高/回落/平台期)

🟣 Worker 连接池使用率

它是什么:两个小弟自己的 DB / Redis 借书证使用率(借出 ÷ 藏书)。它们和 backend 各用各的池,所以这套面板的前缀是 siliang_worker_*——这本书的「图书馆」是分开管的。

回答什么问题:小弟的池子紧张吗?审阅/检索变慢是不是因为借不到连接(而不是服务本身慢)?

✅ 健康长啥样:使用率在低位从容波动;偶发高峰(一批任务集中处理)后回落。

🚨 危险长啥样:使用率持续贴近 100% = 池借空、请求排队。小池容量小(review 5+5),几条慢任务就能打满;动作:先找占着连接不还的慢任务(对照 m130 看借出绝对值),再考虑池参数。

# 主图谱口径:DB 池 + Redis 池使用率(前缀 siliang_worker_*)
siliang_worker_db_pool_connections{state="checked_out"} / siliang_worker_db_pool_capacity
  + siliang_worker_redis_pool_connections / siliang_worker_redis_pool_capacity

联动:主图谱 · Worker 连接池使用率 ↗ · 相关课程:连接池:图书馆借书 · backend 视角的同款双卡

🟣 Worker 连接池借出数与上限

它是什么:小弟池子的绝对值版:实线=各服务借出几条,水平参考线=上限。review 是独立小池 5+5(常备 5 + overflow 5),天花板画出来就贴着地面——这张图让你直观看到「离借空还差几条」。

回答什么问题:review 现在借出几条、离 5 还差几条?借空是均匀分摊还是被某几条慢任务独占?

✅ 健康长啥样:借出数在 0~3 之间起伏,高峰短促不驻留;overflow 启用后能及时归还。

🚨 危险长啥样:借出数顶平 5 = 小池被借空,后续请求全部排队(review 池特别容易被慢任务借空)。动作:定位那几条慢任务(它们抱着连接不还),先治慢,再谈加池。

# 主图谱口径:借出数 + 上限参考线(含 review 独立池 5+5)
siliang_worker_db_pool_connections / siliang_worker_db_pool_capacity
  + siliang_worker_redis_pool_connections / siliang_worker_redis_pool_capacity

联动:主图谱 · Worker 连接池借出数与上限 ↗ · 相关课程:连接池:图书馆借书 · 锁冲突(慢任务的常见病根)

🏭 生产实战 real world

场景 1 · rag 内存冲高:三步判定「干活还是泄漏」

rag 冲高天天有,全是警报等于没有警报。用形态+斜率把「干活」和「泄漏」分开。

# 第 1 步:看形态——冲高之后回不回落?
max_over_time(siliang_worker_process_resident_memory_bytes{job="siliang-rag"}[1m])
# → 回落成锯齿 = 干活;横住不回落 = 嫌疑

# 第 2 步:量化斜率——是真在爬还是在抖?
deriv(siliang_worker_process_resident_memory_bytes{job="siliang-rag"}[1h])
# → > 5MB/h 持续 = 真爬升(核心告警口径);正负跳变 = 抖动

# 第 3 步:对照限额——还有几个台阶到 2G?
# 贴 2G = OOM 倒计时(rag 有前科),立刻处理卡住的大文件任务

判读:回落 + 斜率归零 = 收工;不回落 + 斜率持续为正 + 贴限额 = 处理事故,别等内核开枪。

场景 2 · 给 rag 上「OOM 前兆」两级告警

OOM 不打招呼,告警必须抢在内核前面。一级抓爬升速度,二级抓绝对水位。

# 一级:爬升速度(核心告警口径 deriv > 5MB/h)
- alert: RagMemoryClimbing
  expr: deriv(siliang_worker_process_resident_memory_bytes{job="siliang-rag"}[1h]) > 5 * 1024 * 1024
  labels: {severity: warning}    # 在爬:定位卡住的大文件任务

# 二级:绝对水位逼近 2G 限额(示例阈值 85%,按限额换算)
- alert: RagMemoryNearLimit
  expr: max(siliang_worker_process_resident_memory_bytes{job="siliang-rag"})
        > 0.85 * 2 * 1024 * 1024 * 1024
  labels: {severity: critical}   # 贴限额 = OOM 倒计时(rag 有前科)

场景 3 · 审阅接口变慢:先看小池是不是被借空

review 接口集体变慢,别急着 profile 服务——先看那本 5+5 的小借书证还剩几条。

# 借出绝对值:天花板 5,几条慢任务就能顶平
siliang_worker_db_pool_connections{state="checked_out",job="siliang-review"}

# 使用率互证:贴 100% = 借空排队
siliang_worker_db_pool_connections{state="checked_out",job="siliang-review"}
  / siliang_worker_db_pool_capacity{job="siliang-review"}

# 找病根:慢任务常见来源是锁窗口(1205=等太久,1213=死锁)
sum(rate(siliang_db_lock_errors_total[5m])) by (operation, code)

判读:借出顶平 5 + 使用率 100% = 池借空实锤;顺着慢任务查锁与慢查询,治好慢,池自然松。

场景 4 · 大文件来源排查:和 RSS 冲高对时间戳

rag 冲高不回落时,反问一句「它在给谁干活」:把冲高时间戳与文件/媒体处理字节分布对照,找到那个「搬运工」。

# 冲高时刻的伴读图:文件/媒体处理的典型体重(P95)
histogram_quantile(0.95,
  sum by (le) (rate(siliang_file_batch_archive_bytes_bucket[5m])))
# → 大文件在冲高窗口内被解析:冲高有了出处

# 结局对照:这批活干成了还是反复失败重试(重试=反复占内存)
sum by (outcome) (rate(siliang_file_batch_archive_outcome_total[5m]))
# → exception 高 = 反复重试反复占内存,RSS 不回落的重要原因

场景 5 · 发布后巡检两个小弟还活着

rag / review 各只有一条命(单进程),发布后确认抓取目标都在、指标在更新。

# 每个小弟的抓取目标都应是 1(up 是 Prometheus 通用指标)
up{job=~"siliang-rag|siliang-review"}
# → 0 = 该服务的 /metrics 没被抓到:进程挂了或抓取配置断了

# 复活后 RSS 应从低位重新开始(重启后内存归零再增长)
max_over_time(siliang_worker_process_resident_memory_bytes{job=~"siliang-rag|siliang-review"}[1m])

⚠️ 常见坑 pitfalls

坑 1 · 把 rag 冲高当泄漏乱报警 — 冲高即 pager,天天狼来了。原因:rag 的本职就是解析大文件,冲高是干活的样子。正解:只把「冲高不回落 + 贴 2G」当事故,回落的一律放行。
# 错: rss > 1G 即报            # 大文件一来就误报
# 对: deriv(rss[1h]) > 5MB/h 持续 + 不回落   # 抓真爬升
坑 2 · 拿 backend 的池面板找 rag — 打开 siliang_db_pool_* 找小弟的池,图上永远没有。原因:前缀不同——小弟是 siliang_worker_*,job 也不同。正解:认准 siliang_worker_ 前缀 + job=~"siliang-rag|siliang-review"。
# 错: siliang_db_pool_connections{job="siliang-rag"}      # 前缀就错了
# 对: siliang_worker_db_pool_connections{job=~"siliang-rag|siliang-review"}
坑 3 · 以为小弟也有多 worker 线 — 期待 rag 图上出现 4 条线。原因:backend 是 4 worker,rag/review 是单进程——每服务一条线。正解:看到一条线才对;多条反而要查是不是配置串了。
# 错: "rag 图只有一条线,是不是少抓了 worker"
# 对: 单进程=一条线;数错了去查抓取配置
坑 4 · 池容量张冠李戴 — 用 backend 的 80 给 review 定告警线。原因:三个服务三套池——backend sync30/async80/checkpointer30、biz 10+20、review 5+5。正解:每本借书证用自己的天花板;review 的「满」就是 5。
# 错: review 借出 > 60 才报          # 天花板才 5,永远不报
# 对: review 使用率 = 借出/5,贴 100% 即借空
坑 5 · 只盯 RSS 不看池 — RSS 高就查内存,忽略了池借空导致的排队逗留。原因:借空→请求排队→占着的内存迟迟不释放,RSS 冲高只是果。正解:三张体检单连读:池(m129/m130)→ RSS(m128)互为因果。
# 错: RSS 高 → 直接查大对象
# 对: 先看池是否借空排队 → 再查大对象/慢任务
坑 6 · rag 贴限额还在等 — 「再观察观察」,然后进程消失。原因:OOM 是内核直接 SIGKILL,不打招呼不留遗言,观察窗口为零。正解:贴 2G 限额就是最后窗口,立即行动(处理卡住的大文件/重启服务)。
# 错: "到 95% 再处理"           # 内核不陪你等
# 对: 逼近 2G 限额 = 立即处置 + 复盘大文件来源

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

Q1 · rag 内存冲高为什么「属预期」?那什么时候才算危险?

参考答案:rag 的本职工作是用 LibreOffice 解析用户上传的大文档,解析期间把文件内容摊进内存,冲高就是干活的样子——和处理 200MB 视频内存先涨 200MB+ 是同一个道理。危险的形态只有一个:冲高之后不回落,并且一步步逼近 2G 限额——说明活干完了没收拾,而且 rag 有 OOM 前科,内核随时开枪。用 deriv(rss[1h])>5MB/h 把「在爬」量化出来,抢在 OOM 前处理。

Q2 · review 的连接池和 backend 的有什么本质区别?监控时要换算什么?

参考答案:本质区别是量级:backend 是大书架(sync 30 / async 80 / checkpointer 30),review 是独立小池 5+5(常备 5 + overflow 5)。监控时脑子要换算天花板——review 借出 4 条就已经是 80% 的危险水位,几条慢任务抱着连接不还就能借空整个池子,让审阅接口集体排队。

Q3 · 在 Grafana 里怎么一眼区分「这张图是 backend 的还是小弟的」?

参考答案:看三个身份标记:指标前缀(小弟是 siliang_worker_*,backend 是 siliang_db_pool_* / siliang_process_*)、job(小弟是 siliang-rag / siliang-review,backend 是 siliang_backend_worker)、线的数量(小弟单进程一条线,backend 四 worker 四条线)。三者对上,图就没找错。

← 上一课:🛡 防护与缓存:护栏拦了什么 📚 课程目录 下一课:🎬 视频后台流水线:结算工厂 →