🚦 5xx vs 4xx:谁的错

每个 HTTP 状态码都在回答同一个问题:这事怪谁。4xx=客人点单方式不对(不用半夜爬起来),5xx=后厨自己炸了(立即排查)。所有错误率面板只盯 5xx,>1 req/s 就该看日志——口径见 c17(backend)/ b2(biz)两张图。

点单方式不对 后厨炸了 HTTP 状态码分流:4xx = 调用方的事,5xx = 我们的警报 同一个 handler 出口,两种归宿:一种是"退回重递",一种是"半夜爬起来" 用户请求 HTTP 进店点单 backend / biz handler 处理请求 → 吐状态码 4xx · 客人的点单方式不对 400 参数错 · 401/403 没登录没权限 · 404 没这个路径 😴 常见且无害,一般不用半夜爬起来 要改的是调用方:参数、登录态、URL——不是我们的代码 5xx · 店里自己炸了 代码异常 / 依赖挂了 / 内部超时 🚨 错误率面板只盯 5xx(c17 / b2 口径) >1 req/s:按 handler 直奔日志(面板口径) 事故现场三联:容器重启 + 一波 5xx + 用户掉线 5xx 常见来源(四两实战)——多半是依赖连坐,不全是自己代码的锅 DB 锁冲突 1205 锁等待超时 · 1213 死锁 → 锁冲突面板 c34/c35 确认 Redis / LLM 依赖故障 Redis 抖动连坐 · provider 限流(429)/key 失效 → LLM 错误率 c13 确认(>10% = 切流量) 连接池打满连坐 借出÷藏书贴 100% → 请求排队超时 → DB 池使用率 m115 / b5 确认 错误率的口径:只数 5xx(c17 原式) sum(rate(siliang_http_requests_total{status=~"5.."}[5m])) by (handler) 4xx 根本不在这个查询里——它不是"我们的错误率" 🚑 半夜 5xx 冒头三步(主图谱 triage) ① c17 看哪个 handler → ② b2 看是不是 biz 侧 ③ c34/c35 锁冲突 → c13 LLM 错误率(依赖连坐) 实线 = 请求流 · rose = 事故路径 · 4xx 归调用方 · 5xx 归我们 —— 监控的价值:3 分钟内说清"谁的错、哪个接口、什么原因"

💡 一句话理解

把服务想象成一家餐厅:4xx 是客人点单方式不对——优惠券递反了、没带会员卡、点了菜单上没有的菜,服务员退回重递就好;5xx 是后厨自己炸了——灶台灭火、供应商断货、厨房着火。前者是客人的功课,后者才是店家的故障。

所以监控有明确的站队:所有错误率面板只盯 5xx(正则 status=~"5.."),因为只有它代表"店里出了事";阈值经验是 >1 req/s 就该看日志。四两的 5xx 多半不是自己代码写错,而是依赖连坐——DB 锁冲突、Redis/LLM 依赖故障、连接池打满排队,三种来源各有专门面板可确认。

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

每次 HTTP 请求结束,服务器都会给一个三位数的状态码,第一位数代表"这事的结果归谁":2xx 成功上菜;3xx 换个窗口取餐(跳转);4xx 是客人的点单方式有问题——递错了优惠券(400 参数错)、没带会员卡(401/403 没登录/没权限)、点了菜单上没有的菜(404 路径不存在)。这些请求服务器处理得清清楚楚、礼貌拒绝,不是故障。

5xx 完全不同:客人点单没问题,后厨自己出事了——代码抛异常(500)、上游供应商断货(DB/Redis/LLM 依赖故障)、后厨忙到瘫痪(池打满排队超时)。这才是"店里的错",是半夜电话的来源。所以错误率面板的正则只写 status=~"5..":把 4xx 算进错误率,等于把"客人递错优惠券"也记成"后厨着火",告警会天天狼来了。

四两实战里,5xx 冒头先别急着怀疑自己的代码——按主图谱 triage 的顺序溯源:① 看 c17 哪个 handler 在报 5xx → ② 看 b2 是不是 biz 侧 → ③ 查三大来源:DB 锁冲突(1205 锁等待超时 / 1213 死锁,看 c34/c35)、LLM 供应商失约(错误率 >10% = 限流 429 或 key 失效,看 c13)、连接池打满连坐(借出÷藏书贴 100%,看 m115/b5)。另有一个经典事故现场要会认:容器被 OOM 杀掉重启时,会同时出现"容器重启 + 一波 5xx + 用户掉线"三联。

类比里的东西系统里对应的东西
优惠券递反了400 参数错误:请求参数不对,退回重递即可,不是故障
没带会员卡就想进 VIP 区401/403 未登录 / 无权限:鉴权拦住,礼貌拒绝,不用修代码
点了菜单上没有的菜404 路径不存在:URL 写错,属于调用方的功课
后厨着火500 内部错误:代码异常,5xx 的一种,要立即排查
供应商断货 / 煤气停了依赖故障连坐:DB 锁冲突(1205/1213)、Redis/LLM 挂了,5xx 的常见真凶
经理只看"厨房事故记录本"错误率面板只盯 5xx:status=~"5..",>1 req/s 就看日志(c17/b2 口径)

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

  1. 打开核心盘「HTTP 5xx 错误率」(c17),时间窗 6h——健康时每条 handler 线都贴地(接近 0)。
  2. 故意找一条冒头的线——点开图例看它是哪个 handler;再确认纵轴单位是 req/s(每秒几次),不是百分比。
  3. 切到 biz 盘「HTTP 5xx 错误率」(b2)——如果 c17 平静而 b2 冒头,问题在 biz 侧;反过来同理,这就是"分店定位"。
  4. 按溯源三连确认原因——c34/c35 看锁冲突近 1h 次数是否 ≥1、c13 看 LLM 错误率是否 >10%、m115 看池使用率是否贴 100%。

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

状态码第一位 = 定责
2xx 成功、3xx 跳转、4xx 客人的错、5xx 店里的错。看监控先看首位数字——它直接决定"要不要半夜爬起来"。
# 一条成功请求的完整记录长这样
siliang_http_requests_total{handler="/me", status="200"}
4xx = 客人的错
参数不对、没登录、路径不存在——服务器正常工作、礼貌拒绝。常见且无害,一般不用半夜爬起来;要改的是调用方。
# 4xx 明细:不告警,但值得偶尔看一眼分布
sum(rate(siliang_http_requests_total{status=~"4.."}[5m])) by (status)
5xx = 店里的错
代码异常、依赖挂了、内部超时——这是故障,是错误率面板的唯一主角。c17 的人话:每秒多少次"服务员自己把菜搞砸了"。
# c17 主口径:只数 5xx,按 handler 拆开
sum(rate(siliang_http_requests_total{status=~"5.."}[5m])) by (handler)
正则 5.. 别写死
status=~"5.." 用正则匹配所有 5 开头的两位后缀(500/502/503/504…)。写死 status="500" 会漏掉网关超时等其他 5xx。
# 错: {status="500"} 只逮一种   对: {status=~"5.."} 全逮
{status=~"5.."}   # 正则匹配:5 开头 + 任意两位
>1 req/s 看日志
偶发一两个 5xx 是分布式世界的灰尘;面板口径给的行动线是 >1 req/s——此时不再是个例,按 handler 直奔日志查根因。
# 行动线(c17/b2 口径原文)
# > 1 req/s:直奔该 handler 的日志
三大来源 = 溯源地图
四两 5xx 多半是依赖连坐:① DB 锁冲突(1205 锁等待超时 / 1213 死锁)② Redis/LLM 依赖故障(provider 限流 429 / key 失效)③ 连接池打满排队超时。各有专门面板确认。
# 溯源三连(对时间戳一起看)
sum(increase(siliang_db_lock_errors_total[1h]))            # ① 锁
rate(siliang_llm_calls_total{outcome="error"}[5m])        # ② LLM
max_over_time(siliang_db_pool_connections{state="checked_out"}[1m])  # ③ 池
OOM 三联事故现场
容器被内核 OOM 杀掉重启的现场表现是三联:容器重启 + 一波 5xx + 用户掉线。看到 5xx 尖峰先对时间戳:是不是正好压着一次重启。
# 5xx 尖峰时刻 × 容器重启次数对表(k5 口径)
changes(container_start_time_seconds[1h])
biz 同款口径
biz 盘 b2 与 c17 一字不差,只是指标换成 siliang_biz_http_requests_total。两张图一起看,还能区分"backend 炸了还是 biz 炸了"。
# b2 主口径
sum(rate(siliang_biz_http_requests_total{status=~"5.."}[5m])) by (handler)

🔗 在四两监控里哪里用到 理论落回你的 89 张图

🟢 核心盘 · HTTP 5xx 错误率(c17)↗——本课的主面板:每秒多少次"服务员自己把菜搞砸了",按 handler 拆开;🚨 >1 req/s 就该排查(DB / Redis / 依赖故障),按 handler 直奔日志。

🟢 biz 盘 · HTTP 5xx 错误率(b2)↗——biz 侧同款口径;biz 的 5xx 多半源自 DB / Redis / 依赖故障。与 c17 对照还能快速分流"backend 炸还是 biz 炸"。

🔴 核心盘 · 数据库锁冲突率(c35)↗——5xx 的头号内源性来源:1205=锁等待超时(等太久放弃)、1213=死锁(互相等,数据库强滚一个)。按 operation × code 拆开看谁在抢锁。

🟡 核心盘 · LLM 错误率(c13)↗——依赖连坐的另一路:>10% = provider 限流(429)或 key 失效,切流量/换 key,不是我们代码的 bug。以及主图谱 🚑 排查速查表 ↗ 第一行「接口报 500 / 用户炸群」就是本课 SOP。

🏭 生产实战 real world

场景 1 · 巡检:5xx 有没有冒头、冒头的是谁

每天巡检把 c17 的曲线扫一眼,再手跑一遍它的口径,确认图和查询理解一致。

# c17 口径:每秒多少次服务端错误,按 handler 拆开
sum(rate(siliang_http_requests_total{job="siliang_backend_worker", status=~"5.."}[5m])) by (handler)
# 判读:全部贴地(≈0)= 健康;
#       某条线抬升但 < 1 req/s = 记录观察;
#       某条线 > 1 req/s = 按 handler 直奔日志排查

场景 2 · 把"谁的错"变成告警:只盯 5xx、阈值 1

告警规则里只写 5xx——把 4xx 算进去只会制造狼来了。行动阈值用面板口径的 1 req/s。

groups:
  - name: siliang-http-5xx
    rules:
      - alert: BackendHTTP5xxRate
        # 只数 5xx;> 1 req/s = 面板口径的行动线
        expr: |
          sum(rate(siliang_http_requests_total{job="siliang_backend_worker", status=~"5.."}[5m])) by (handler)
            > 1
        for: 5m    # 持续窗口为通用示例,线上以核心盘口径卡为准
        labels:
          severity: critical
        annotations:
          summary: "handler {{ $labels.handler }} 5xx > 1 req/s:按 triage 溯源三连排查"

场景 3 · 溯源三连:锁 → LLM → 池,对时间戳一起看

5xx 冒头后不要先翻代码,先回答"是不是依赖连坐"。三条查询拉同一时间窗,谁同时在动谁就是嫌疑人。

# 嫌疑一:DB 锁冲突(近 1h 次数,c34 口径)
sum(increase(siliang_db_lock_errors_total{job="siliang_backend_worker"}[1h]))
# 嫌疑二:LLM 供应商失约率(c13 口径,>10% = 限流/key 失效)
rate(siliang_llm_calls_total{outcome="error"}[5m])
  / clamp_min(rate(siliang_llm_calls_total[5m]), 1)
# 嫌疑三:DB 池打满(m115 口径,贴 100% = 请求排队超时)
max_over_time(siliang_db_pool_connections{state="checked_out"}[1m])
  / on (instance, pool) max_over_time(siliang_db_pool_capacity[1m])

场景 4 · biz 侧同款:先分流"哪家店在炸"

c17 与 b2 一起看,10 秒钟分清 backend 还是 biz 的问题——这是分店定位的价值。

# b2 口径:biz 每秒搞砸几次,按 handler 拆
sum(rate(siliang_biz_http_requests_total{status=~"5.."}[5m])) by (handler)
# 分流判读:
#   c17 冒头 b2 平静 → backend 侧(LLM/WS/任务连坐为主)
#   b2 冒头 c17 平静 → biz 侧(多半 DB / Redis 依赖)
#   两边同时冒头 → 找共同依赖:DB 锁冲突 / Redis 抖动

场景 5 · 4xx 突增:不告警,但值得看一眼

4xx 不进错误率告警,但按 status 拆开看分布,能发现"被刷"和"上游改参数"这类事。

# 4xx 明细:按具体状态码拆开(400/401/403/404)
sum(rate(siliang_http_requests_total{status=~"4.."}[5m])) by (status, handler)
# 判读:401/403 突增 = 有人拿过期 token 猛刷(注意来源);
#       400 集中在某个 handler = 上游调用方改了参数没对齐;
#       404 突增 = 扫描器在猜路径,确认网关有兜底即可

场景 6 · 发布后 5xx 抬升:先排除排空涟漪再定责

滚动发布期间出现一波 5xx,先看是不是旧 worker 排空的正常涟漪,别急着 revert。

# 在途请求:发布时每个 worker 应逐个归零(c18 ops 排空口径)
sum(siliang_http_requests_in_progress) by (method) + sum(siliang_ops_http_in_flight)
# 存活点名:每个 service 预期 2(k6 口径)
count by (service) (container_start_time_seconds{service=~"siliang-.*"})
# 判读:在途逐 worker 归零 + 存活=2 + 5xx 随后贴地 → 发布涟漪,结案;
#       排空卡住不归零 或 存活<2 → 真问题,按 OOM 三联/重启面板继续查

⚠️ 常见坑 pitfalls

坑 1 · 错误率把 4xx 算进去 — 症状:错误率常年"飘红",告警天天狼来了,最后大家都不看告警了。原因:把客人的错(参数/未登录)记成了店里的错。正解:错误率只盯 5xx,正则 status=~"5.."。
# 错: rate(siliang_http_requests_total[5m]) 不分 status 全算"错误"  # → 误报工厂
# 对: rate(siliang_http_requests_total{status=~"5.."}[5m])          # → 只数店里的错
坑 2 · status 写死 "500" — 症状:明明在出事,查询却显示 0。原因:5xx 是一家子(500/502/503/504…),写死一个等值漏掉其他。正解:用正则 "5.." 匹配整个家族。
# 错: {status="500"}    # → 漏掉 502/503/504
# 对: {status=~"5.."}   # → 5 开头全逮
坑 3 · 一两个 5xx 就拉人半夜排障 — 症状:偶发尖峰就全员上桌。原因:分布式系统有灰尘,个位数的偶发没有行动价值。正解:按面板口径:>1 req/s 才行动,之前只记录观察。
# 错: 尖峰 0.2 req/s → 拉群排查          # → 灰尘级事件兴师动众
# 对: > 1 req/s 或持续抬升 → 按 triage 排查  # → 行动线明确
坑 4 · 只看总数,不按 handler 拆 — 症状:知道"在报 5xx",不知道在哪报。原因:sum 掉 handler 维度后只剩一条总线。正解:by (handler) 拆开,直奔冒头的那条线。
# 错: sum(rate(…{status=~"5.."}[5m]))                  # → 只知总量
# 对: sum by (handler)(rate(…{status=~"5.."}[5m]))     # → 定位接口
坑 5 · 5xx 一冒头就怀疑自己的代码 — 症状:翻半天代码没头绪,最后发现是 DB 锁或 LLM 供应商挂了。原因:四两的 5xx 多半是依赖连坐。正解:先跑溯源三连(锁 c34/c35 → LLM c13 → 池 m115),确认内因外因再动手。
# 错: git blame → 改代码 → 上线       # → 代码没问题白忙一场
# 对: 锁 → LLM → 池 三连对时间戳     # → 先分清谁的锅
坑 6 · 自己写查询忘了 job 过滤 — 症状:数字比面板虚高。原因:混入了会双计的 server job(面板全部排除它)。正解:永远带 job="siliang_backend_worker"(或 siliang-biz)。
# 错: siliang_http_requests_total{status=~"5.."}                        # → 可能双计
# 对: siliang_http_requests_total{job="siliang_backend_worker", status=~"5.."}
坑 7 · 5xx 尖峰归因错窗口 — 症状:把发布重启或 OOM 期间的 5xx 当成新 bug 排查。原因:重启/OOM 时刻天然有一波请求失败(事故三联)。正解:先对时间戳:5xx 尖峰是否压着容器重启(k5),发布窗口内另当别论。
# 错: 尖峰 → 立刻 git revert            # → 可能只是发布重启涟漪
# 对: changes(container_start_time_seconds[1h]) 对表后再定责
坑 8 · 4xx 当纯噪音完全不看 — 症状:4xx 翻了十倍没人发现,其实是上游改了参数或被刷。原因:只盯 5xx 盯成了"只看 5xx"。正解:4xx 不告警,但巡检时看一眼分布;突增=调用方在变。
# 错: 4xx 从此再也不看               # → 错过被刷/调用方变更信号
# 对: 巡检看 by (status) 分布,突增再深究  # → 不告警但留眼睛

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

Q1 · 用一句话向产品经理解释 4xx 和 5xx 的区别?

参考答案:4xx 是用户自己操作不对(参数错、没登录、链接失效),系统在礼貌拒绝,不用修;5xx 是系统自己出故障了(代码异常、数据库/大模型依赖挂了),用户什么都没做错——这才是要我们半夜起来修的事。所以错误率面板只盯 5xx。

Q2 · 半夜 5xx 冒头,前三步做什么?

参考答案:① 打开 c17 看哪个 handler 冒头、是否 >1 req/s;② 对照 b2 分流是 backend 还是 biz 在炸;③ 跑溯源三连对时间戳——锁冲突(c34/c35:1205/1213)、LLM 错误率(c13:>10%=限流/key 失效)、池使用率(m115:贴 100%=排队超时)。先分清谁的锅再动手。

Q3 · 为什么错误率面板只盯 5xx,4xx 完全不管吗?

参考答案:盯 5xx 是因为只有它代表"店里出了事",是告警和排障的对象;4xx 是调用方的功课,算进去只会制造误报。但"不告警"不等于"不看"——巡检时看一眼 4xx 按 status 的分布,401 突增可能是被刷、400 集中可能说明上游改了参数,这是有价值的信号。

Q4 · 四两的 5xx 常见三大来源是什么?分别去哪张图确认?

参考答案:① DB 锁冲突——1205 锁等待超时 / 1213 死锁,看 c34(近 1h 次数)和 c35(按操作与错误码拆);② Redis / LLM 依赖故障——provider 限流(429)或 key 失效,看 c13(错误率 >10% 红线);③ 连接池打满连坐——借出÷藏书贴 100% 请求排队超时,看 m115(backend)/ b5(biz)。半数以上是依赖连坐,不全是自己代码的锅。

← 上一课:job / instance / label:指标的身份证 📚 课程目录 下一课:🟣 在线用户与协作规模(核心盘 6 图) →