🟢 HTTP 健康:门面四件套(核心盘 4 图)

门面生意好不好,看四件:QPS(谁最忙)/ 5xx(谁在出事)/ 并发(在不在排队,ops 排空口径盯发布)/ P99 Top10(优化待办清单)。覆盖核心盘 🟢 组 4 张面板(c16–c19)· backend(FastAPI · 4 worker)。

门面生意四件套 —— 请求流过 backend 时的四个观察点 用户请求 HTTP 页面与接口 handler(路由模板) 接口的柜台号 不含 /api/v2 前缀 backend ×4 worker(并行接客) 请求处理 慢查询 / 依赖抖动在这里变慢 变慢 → 排队 → 雪崩的起点 事故现场:5xx = 服务员自己把菜搞砸了(status=~"5..") 滚动发布:旧 worker 在途数应逐个归零(c18 ops 排空) 某接口流量归零 = 上游调用方挂了 无发布无活动却突增 = 被刷 / 爬虫 四条探针(本课 4 图) ① QPS(c16)· 谁最忙 / 流量结构突变 ② 5xx 错误率(c17)· 谁在出事故 ③ 并发(c18)· 排队 + 发布排空 ④ P99 Top10(c19)· 优化待办清单 铁三角:并发 ≈ QPS × 延迟(Little's Law) 店里人数 = 进店速度 × 每人待多久 —— 延迟翻倍,店里人数翻倍 三张图连看:c16 QPS · c18 并发 · c19 P99(LLM 侧配 c14 / c15) 4xx vs 5xx:谁的错 4xx = 客人的点单方式不对(参数错/没登录) 5xx = 厨房炸了 —— 错误率面板只盯 5xx 雪崩链 —— 慢一个点,全线崩(没有任何一辆车想堵车) 某依赖变慢 DB 慢查询 · LLM 抖动 请求排队 在 handler 前面等 并发抬升 QPS × 延迟 变大 池打满 DB / Redis 100% 5xx 雪崩 用户炸群 · 工单爆炸 事故现场:5xx > 1 req/s 就该排查(DB / Redis / 依赖故障),按 handler 直奔日志 —— c17 口径

💡 一句话理解

把 backend 想成一家奶茶店:c16 是进店速度(每个柜台每秒接几单),c17 是服务员自己把菜搞砸的次数(5xx,按柜台拆开),c18 是此刻店里有多少客人没走(在途请求,发布时看旧员工有没有把手上单子做完再走),c19 是全店最慢的十个柜台排行榜(你的性能优化待办清单)。

四张图不是孤立的:铁三角(并发 ≈ QPS × 延迟)把它们串成一条因果链——某依赖变慢 → 请求排队 → 并发抬升 → 池打满 → 5xx 雪崩。所以看门面生意永远四件套连看,单看一张最容易误判。

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

先分清两组数。"每分钟进来多少个客人"是 QPS(进店速度),"店里此刻有多少人在等咖啡"是并发(在途请求)——这是两个完全不同的东西:店里人满为患,可能是客人来得太猛(QPS 涨),也可能是做咖啡变慢了(延迟涨),分不清这两种就没法下结论。利特尔法则把它们连起来:店里人数 = 进店速度 × 每人待的时间。

再看"搞砸"的归责。4xx 是客人的点单方式不对——参数写错、没登录就下单,服务员把单子退回去就行,不用半夜爬起来;5xx 是后厨自己炸了——代码抛异常、DB 锁冲突、Redis 或 LLM 依赖挂了。所以错误率面板只盯 5xx,而且按 handler(柜台号)拆开:哪个柜台在搞砸,直奔哪个柜台的日志。口径卡给的行动线很具体:>1 req/s 就该排查了。

最后是排行榜和发布。c19 用 topk(10, …) 把全站最慢的十个接口挑出来——这就是你的性能优化待办清单,优化完再来看同一条查询,榜单名次的变化就是成效。c18 还多一条"ops 排空口径"参考线,专盯发布:滚动发布时,旧 worker 手上的在途请求应该逐个归零(旧员工把手上的单做完再下班);发布完还挂着不归零,就是排空卡住。而雪崩链在暗中随时待命:延迟翻倍 → 并发翻倍 → 连接池打满 → 排队更狠 → 5xx 铺开——这就是为什么"慢"从来不是小事。

类比里的东西系统里对应的东西
每个柜台每秒接几单c16 HTTP QPS(按 handler):handler=路由模板(柜台号,不含 /api/v2 前缀)
服务员自己把菜搞砸c17 5xx 错误率:status=~"5..",>1 req/s 就该排查,按 handler 直奔日志
店里此刻的客人数c18 在途请求(Gauge):in_progress + ops 排空口径;持续高位=在排队
最慢柜台排行榜c19 P99 Top10:topk(10, histogram_quantile(0.99, …))——性能优化待办清单
顾客把优惠券递反了4xx=客人的错(参数不对/没登录),一般不用半夜爬起来;面板只盯 5xx
打烊交接:单做完才走滚动发布:旧 worker 在途数应逐个归零(ops 排空口径);不归零=排空卡住

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

  1. 看 c16 找最忙接口——图例里最高的那条 handler 就是流量大头;记下它的常态量级。
  2. 看 c17 确认 5xx 贴地——健康时各 handler 的 5xx 速率贴近 0;心里记住">1 req/s 就该排查"这条线。
  3. 看 c18 的并发量级——找 ops 排空那条参考线;下次发布时盯它:旧 worker 的在途数应逐个归零。
  4. 读 c19 的 Top10 榜单——记住榜首是谁;它就是你本周性能优化待办的第一行。

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

handler(柜台号)
请求按路由模板记账:handler 就是接口的"柜台号",四两的 handler 不含 /api/v2 前缀——按它拆开才知道"谁"最忙、"谁"在出错。
# 按 handler 拆 = 每个柜台一条线(c16 口径)
sum(rate(siliang_http_requests_total[5m])) by (handler)
QPS = rate(请求数)
请求总数是 Counter(_total 只增不减),配 rate 才是"每秒几次"。窗口 [5m] 越大越平滑,排查细节临时改 [1m]。
# Counter 必配 rate(c16 口径)
sum(rate(siliang_http_requests_total[5m])) by (handler)
5xx vs 4xx
4xx=客人的点单方式不对(参数错/没登录),5xx=后厨炸了(异常/依赖挂)。错误率面板只盯 5xx,>1 req/s 就该看日志。
# 只筛 5xx:正则 5.. 匹配 500/502/503/504…(c17 口径)
sum(rate(siliang_http_requests_total{status=~"5.."}[5m])) by (handler)
status=~"5.." 正则
"5.." 匹配所有 5 开头的两位数状态码;写成 ="500" 会漏掉 502/503/504——雪崩时恰恰是它们。
# 错:只盯 500 一种;对:正则覆盖全部 5xx
{status=~"5.."}   # 500 · 502 · 503 · 504 全算
并发是 Gauge
在途请求(已到达未返回)是温度计,直接读数;它高只有两种可能:来得太多(QPS 涨)或走得太慢(延迟涨)。
# Gauge 直接读(c18 口径,含 ops 排空线)
sum(siliang_http_requests_in_progress) by (method)
  + sum(siliang_ops_http_in_flight)
ops 排空口径
滚动发布专用的参考线:旧 worker 手上的在途请求应逐个归零(单做完再下班);发布后不归零=排空卡住。
# 发布验证:by (instance) 变体逐 worker 看(c18 扩写)
sum(siliang_http_requests_in_progress) by (instance)
P99 从水桶里插值
延迟存在 _bucket 累积水桶里,histogram_quantile(0.99, …) 插值算出"最惨的 1%";by 里必须带 le。
# le 必进 by(c19 口径)
histogram_quantile(0.99, sum(rate(siliang_http_request_duration_seconds_bucket[5m])) by (le, handler))
topk 只看前 N 名
几十条 handler 的延迟线会糊成毛线团,topk(10, …) 只画最慢的前 10 条——现成的优化待办清单。
# 榜单套在分位数外面(c19 口径)
topk(10, histogram_quantile(0.99, …))

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

🟢 c16 · HTTP QPS(按 handler)

它是什么:backend 每个接口每秒被调几次。handler 是路由模板(接口的"柜台号",不含 /api/v2 前缀),请求总数是 Counter,配 rate 按 handler 拆开——这就是门面生意的"客流结构图"。

回答什么问题:谁是最忙接口?流量结构有没有突变?

✅ 各 handler 量级随业务节奏稳定,占比结构平稳

🚨 无发布无活动却突增 = 被刷/爬虫;某接口流量归零 = 上游调用方挂了。动作:突增先查时段(发布/活动),归零顺着调用链找上游。

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

联动:主图谱 c16 ↗ · 相关课程:rate():算速度 · job / instance / label

🟢 c17 · HTTP 5xx 错误率

它是什么:每秒有多少次"服务员自己把菜搞砸了"——服务端错误(status=~"5..")的速率,按 handler 拆开。它是事故的第一现场:现在有没有事故、哪个接口在出事故,看它最快。

回答什么问题:现在有没有事故?哪个接口在出事故?

✅ 各 handler 的 5xx 速率长期贴近 0,偶发成刺

🚨 >1 req/s 就该排查(DB / Redis / 依赖故障),按 handler 直奔日志。动作:主图谱排查速查表第一行——5xx → 锁冲突(c34/c35)→ LLM 错误率(c13)找连坐源头。

# 面板 PromQL(照抄主图谱口径)
sum(rate(siliang_http_requests_total{status=~"5.."}[5m])) by (handler)

联动:主图谱 c17 ↗ · 相关课程:5xx vs 4xx:谁的错 · 数据库锁冲突(5xx 常见源头)

🟢 c18 · HTTP 当前并发请求数

它是什么:此刻店里有多少客人没走——已到达还没返回的在途请求(Gauge,按 method 拆)。它还多一条"ops 排空口径"参考线,专盯发布:旧 worker 手上的请求必须做完才能退场。

回答什么问题:请求在不在排队?发布时旧 worker 有没有干净退场?

✅ 随业务节奏起伏,峰值后回落;滚动发布时每个 worker 的在途数应逐个归零

🚨 持续高位 = 排队(DB 慢查询或依赖抖动);发布后不归零 = 排空卡住。动作:配合铁三角定位是"来得太多"还是"走得太慢",再看池面板(m115)有没有打满。

# 面板 PromQL(照抄主图谱口径)
sum(siliang_http_requests_in_progress) by (method)
  + sum(siliang_ops_http_in_flight)

联动:主图谱 c18 ↗ · 相关课程:QPS·并发·延迟铁三角 · 连接池台账(排队的终点)

🟢 c19 · HTTP P99 延迟(Top 10 慢接口)

它是什么:全站最慢的 10 个接口排行榜——P99 是"最惨的 1% 请求"的等待时间,延迟从 _seconds_bucket 水桶里插值算出,再用 topk(10, …) 只画最慢前十。它不是事故面板,是性能优化待办清单。

回答什么问题:该优化哪个接口?优化后变快了吗?

✅ 榜单稳定、名次随优化有序变化;没有"莫名其妙冒出来的新榜首"

🚨 榜首 P99 突然整体抬升 = 该接口在恶化。动作:进排查链——c18 看排队、m115 看池、c15 看 LLM(等模型);优化上线后用同一条查询对比成效。

# 面板 PromQL(照抄主图谱口径)
topk(10, histogram_quantile(0.99,
  sum(rate(siliang_http_request_duration_seconds_bucket[5m])) by (le, handler)))

联动:主图谱 c19 ↗ · 相关课程:P50/P95/P99:排队 · 直方图水桶与 topk

🏭 生产实战 real world

场景 1 · 5xx 告警:>1 req/s 就该排查

行动线来自核心盘口径卡(>1 req/s 排查),按 handler 拆开让告警直接指到柜台。

# 告警规则:5xx 速率(阈值来自核心盘 c17 口径卡)
groups:
- name: siliang-http
  rules:
  - alert: Backend5xxRateHigh
    expr: sum(rate(siliang_http_requests_total{status=~"5.."}[5m])) by (handler) > 1
    for: 5m           # 持续 5 分钟才报,避开偶发成刺
    labels:
      severity: critical
    annotations:
      summary: "handler {{ $labels.handler }} 5xx 超 1 req/s —— 直奔该 handler 日志"

判读收尾:告警带上 handler 名,值班同学不用猜"是哪儿炸了"。

场景 2 · 用户炸群:事故定位三连

主图谱排查速查表第一行的完整路径:5xx → 锁冲突 → LLM 连坐。

# 1) 哪个 handler 在出事(c17 口径)
sum(rate(siliang_http_requests_total{status=~"5.."}[5m])) by (handler)
# 2) 是不是 DB 锁连坐(c35 口径:operation × code,1205/1213)
sum(rate(siliang_db_lock_errors_total[5m])) by (operation, code)
# 3) 是不是 LLM 依赖连坐(c13 口径)
rate(siliang_llm_calls_total{outcome="error"}[5m]) by (provider)
# 判读:三张图对时间戳——谁先冒头谁是源头,别急着回滚

场景 3 · 接口变慢:按速查表顺序钻

主图谱排查速查表第二行:谁最慢 → 在不在排队 → 池打满没有。

# 1) 谁最慢(c19 口径)
topk(10, histogram_quantile(0.99, sum(rate(siliang_http_request_duration_seconds_bucket[5m])) by (le, handler)))
# 2) 在不在排队(c18 口径)
sum(siliang_http_requests_in_progress) by (method) + sum(siliang_ops_http_in_flight)
# 3) DB 池打满没有(m115 口径:借出 ÷ 藏书,95% 红)
max_over_time(siliang_db_pool_connections{state="checked_out"}[1m])
  / on(instance,pool) max_over_time(siliang_db_pool_capacity[1m])
# 4) 还要排除:LLM 在拖(c15)与容器 CPU 挤占(k3)

场景 4 · 发布验证:ops 排空逐 worker 归零

滚动发布时,旧 worker 手上的在途请求应逐个归零——这是"死得干不干净"的标准。

# 发布窗口内盯:按 instance 逐 worker 看(c18 口径的 by 变体)
sum(siliang_http_requests_in_progress) by (instance)
  + sum(siliang_ops_http_in_flight) by (instance)
# 健康形态:每个旧 worker 的在途数在发布后几分钟内依次归零
# 危险形态:某 worker 的线挂住不归零 = 排空卡住(查慢请求/长连接)
# 配套确认:跨进程收尸(c11)——旧实例退场有没有留下烂尾任务

场景 5 · 优化闭环:Top10 榜单前后对比

性能优化的完整闭环:榜单定目标 → 优化 → 同一条查询验证成效。

# 优化前:锁定目标(c19 口径)
topk(10, histogram_quantile(0.99, sum(rate(siliang_http_request_duration_seconds_bucket[5m])) by (le, handler)))
# 优化后:跑同一条查询,看该 handler 的 P99 是否整体下移
# 判读:名次下降/绝对值下降 = 有效
#       被别的 handler 顶上榜首 = 达标,翻下一页待办

⚠️ 常见坑 pitfalls

坑 1 · 盯着 4xx 当事故 — 症状:看到错误率图里 401/422 很多就半夜爬起来。原因:4xx 是客人的点单方式不对(参数错/没登录),不是我们的错。正解:错误率面板只盯 5xx(status=~"5.."),4xx 交给产品侧治理。
# 错: 把 4xx 计入告警错误率
# 对: sum(rate(siliang_http_requests_total{status=~"5.."}[5m])) by (handler)
坑 2 · status 只写 ="500" — 症状:雪崩了错误率图却很平静。原因:雪崩时涌出的往往是 502/503/504,精确匹配 500 漏掉它们。正解:用正则 5.. 覆盖全部 5xx(面板口径)。
# 错: {status="500"}                          # → 漏 502/503/504
# 对: {status=~"5.."}                         # → 全部 5xx
坑 3 · 只看平均延迟 — 症状:"平均 80ms 挺快",用户却投诉卡。原因:平均被海量快请求稀释,一个 2s 的慢请求混在 999 个快请求里根本拉不平。正解:看 P99(c19 排行榜),它是"最惨的 1%"的诚实记录。
# 错: avg(rate(...)) / avg 折线看延迟
# 对: topk(10, histogram_quantile(0.99, … by (le, handler)))
坑 4 · histogram_quantile 忘 by le — 症状:延迟曲线低得离谱且死平。原因:所有水桶被 sum 搅成一锅,插值失去意义。正解:by 里永远带 le(再带 handler)。
# 错: sum(rate(..._duration_seconds_bucket[5m]))
# 对: sum(rate(..._duration_seconds_bucket[5m])) by (le, handler)
坑 5 · 并发高只看一张图 — 症状:c18 冲高就断言"流量爆炸"或"服务崩了"。原因:并发 = QPS × 延迟,"来得太多"和"走得太慢"都会推高它。正解:三图联动:c16 看量、c18 看排队、c19 看慢——分清病因再下药。
# 错: c18 高 → 直接扩容
# 对: c16 没涨 + c18 高 = 走得慢 → 去 c19/m115 找慢因
坑 6 · 发布后在途不归零误判为泄漏 — 症状:发布完某个 worker 的在途数挂着,以为连接泄漏。原因:这更可能是排空卡住——旧 worker 手上有慢请求或长连接没做完。正解:先按 c18 的 ops 排空口径确认,再查该 worker 上的慢请求(c19)。
# 错: 发布后在途不归零 → 判连接泄漏
# 对: sum(siliang_http_requests_in_progress) by (instance) → 找挂住的 worker 查慢请求
坑 7 · QPS 突增直接当被刷 — 症状:c16 翻倍就喊"被打了"。原因:发布、活动、上游放量都会让流量翻倍。正解:先对时间轴:有没有发布/活动?都不是再按被刷处置。
# 错: QPS 翻倍 → 直接封 IP
# 对: 对照发布/活动日历 → 无事件才按被刷/爬虫处置
坑 8 · 某接口流量归零不当回事 — 症状:"流量降了挺好",其实那个接口的上游调用方已经挂了。原因:归零不是没人用,是没人能调进来。正解:熟悉接口的调用关系,主力接口归零按故障处理。
# 错: handler 线归零 → 忽略
# 对: 主力 handler 归零 → 上游调用方挂了,立即按故障排查

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

Q1 · QPS、并发、延迟三者是什么关系?为什么并发高不能只怪流量?

参考答案:铁三角——并发 ≈ QPS × 延迟(店里人数 = 进店速度 × 每人待多久)。并发高有两种成因:客人来得太多(QPS 涨)或做单变慢(延迟涨)。所以 c18 冲高必须回看 c16(量)和 c19(慢),分清病因才能决定是扩容还是优化接口。

Q2 · 4xx 和 5xx 分别是谁的错?错误率面板为什么只盯 5xx?

参考答案:4xx 是客人的错——参数不对、没登录,服务员退回重递即可,不用半夜爬起来;5xx 是店里的错——代码异常、DB 锁冲突、Redis/LLM 依赖故障。监控要抓的是"我们自己的问题",所以错误率面板只盯 5xx;而且 >1 req/s 就该按 handler 直奔日志。

Q3 · 滚动发布时 c18 上应该看到什么?"不归零"说明什么?

参考答案:ops 排空口径下,每个旧 worker 的在途请求数应逐个归零——旧员工把手上的单做完再下班,谁先做完谁先走。发布完还挂着不归零 = 排空卡住,多半是该 worker 手上有慢请求或长连接没结束,去 c19 找慢接口、查长连接。

Q4 · c19 的 Top10 榜单怎么用?P99 为什么比平均值诚实?

参考答案:它是性能优化待办清单——榜首就是要优化的接口,优化完跑同一条查询对比名次就是成效验证。P99 比 averages 诚实是因为平均值会被海量快请求稀释:一个 2s 的请求混在 999 个 80ms 里,平均几乎不动,但 P99 会把它如实暴露出来。

← 上一课:🟡 LLM 健康:大模型供应商门诊(核心盘 4 图) 📚 课程目录 下一课:🔵 WebSocket 画布协作:神经系统 →