📐 QPS·并发·延迟铁三角

Little's Law 大白话:并发 ≈ QPS × 延迟(店里人数 = 进店速度 × 每人待多久)。延迟一翻倍、并发就翻倍 → 池打满 → 雪崩。覆盖 核心盘 c16 / c18 / c19 三图联动 · LLM 侧 c14 / c15 同理。

铁三角:并发 ≈ QPS × 延迟(Little's Law) 类比:店里人数 = 进店速度 × 每人待多久 —— 延迟一翻倍,并发就翻倍 同一时间戳,往下看两条路 QPS · 进店速度 每秒进来多少请求 rate(请求数[5m]) 算出来 面板 c16 · HTTP QPS(按 handler) × 延迟 · 每人待多久 P95/P99:慢的那批人等多久 histogram_quantile 从桶里算 面板 c19 · P99 延迟 Top10 慢接口 ≈ 并发 · 店里同时多少人 在途请求:进来了还没返回 Gauge 直接读数 面板 c18 · HTTP 当前并发请求数 ✅ 健康路:延迟稳,三角各自安好 100 QPS 进店速度正常 延迟 0.2s 处理很快 并发 20 店里不挤 公式验算:100 QPS × 0.2s = 20 个在途 DB 池使用率约 40%(30 条借出 12 条) P99 平稳 · 5xx 为 0 · 一切从容 谁都没事 → 三张图都是水平的 🚨 危险路:延迟一恶化,雪崩开始 延迟 2s 依赖变慢 并发 200 店里爆满 池 100% 全员排队 客人没变多:100 QPS × 2s = 200 个在途 延迟 ×10 → 并发 ×10:这就是雪崩的数学 💥 事故现场:DB 池 checked_out = capacity(全借光) 新请求借不到连接排队等还书,延迟更高,排队更狠 刹车点:池使用率 80% 黄 / 95% 红(m115)· 5xx 超过 1 req/s 直奔日志(c17) 三图连看公式:c16 QPS × c19 延迟 → c18 并发;时间戳不对齐,结论全作废

💡 一句话理解

你是奶茶店店长:每分钟进来 100 个客人(QPS),每人从点单到拿到奶茶待 3 分钟(延迟),那店里「同时」有多少人?不是「很多」,是一个能算的数:100 × 3 分钟 = 约 300 人。这就是利特尔法则:并发 ≈ QPS × 延迟——三个量里知道两个,第三个就被锁死了。

它解释了后端最凶险的连锁反应:某个依赖变慢 → 延迟翻倍 → 在途并发跟着翻倍(客人没变多,是走得不快了)→ 连接池被打满 → 后来的请求连池子都借不到 → 延迟更高……正反馈转起来就是雪崩。所以 QPS、并发、延迟这三张图必须对齐时间戳连着看,单看任何一张都可能得出完全错误的结论。

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

想象你经营一家奶茶店。进店速度是每分钟进来多少个客人——这是 QPS;每人待多久是从进门到拿着奶茶出门——这是延迟;店里人数是某一瞬间店里有几个人——这是并发。三个数不是独立的:每分钟进 100 人、每人待 30 秒,店里就稳定有约 50 人;每人待 3 分钟,店里就有 300 人。店里挤不挤,不只看来了多少人,还要看每个人赖了多久。

现在想象厨房出事了:出一杯从 30 秒变成 3 分钟。注意——客人一个都没多来,每分钟还是进来 100 个,但店里人数从 50 涨到了 300:座位坐满、队伍排到门外、后来的人一看队伍扭头就走(超时放弃)。后端一模一样:数据库或 LLM 变慢 → 延迟涨 → 在途请求堆积 → 连接池被打满 → 更多请求排队 → 延迟更高。没有任何一步是「谁犯了错」,雪崩是系统自己涌现的,起点只是「慢了一点」。

所以排查「并发为什么高」永远只有两个方向:来得太多(QPS 真的涨了,比如活动、被刷)或走得太慢(延迟涨了,比如慢查询、依赖抖动)。分清这两种,才知道该扩容接流量,还是该去修慢查询——这就是为什么 c16 / c18 / c19 三张图要摆在一起、对齐时间戳看。

类比里的东西系统里对应的东西
进店速度(每分钟 100 人)QPS:rate(siliang_http_requests_total[5m]),面板 c16 按 handler 拆开看谁最忙
每人待多久(30 秒 vs 3 分钟)延迟:P95/P99 分位数(面板 c19),平均值会被快请求稀释,不能代表排队体验
店里同时的人数(50 vs 300)并发:在途请求 Gauge(面板 c18),进来了还没返回的请求个数
厨房变慢、店里爆满延迟 0.2s 恶化到 2s,QPS 不变并发也 ×10——延迟恶化是雪崩的起点
座位与排队的队连接池与排队:池使用率 80% 黄 / 95% 红(m115),100% = 全员等还书
看店要看三个表三图连看:c16 QPS × c19 延迟 = c18 并发,时间戳必须对齐

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

  1. 看进店速度——打开核心盘 🟢 HTTP QPS(c16),记住一个平峰时段的每秒总量,这就是「进店速度」。
  2. 看店里人数——同一时段切到 🟢 HTTP 当前并发请求数(c18),读出在途数;用公式粗算:并发 ≈ QPS × P50 延迟,数量级对得上就说明你看懂了三角关系。
  3. 看每人待多久——切到 🟢 P99 Top10 慢接口(c19),用最慢接口的 P99 代入公式,解释为什么并发会比 QPS「大得多」。
  4. 亲眼看一次延迟带动并发——找一段 LLM 变慢的时间(c15 的 P50 整体抬升),回看 c18 并发曲线是否同步抬高——这就是「客人没变多,店里却满了」的现场。

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

QPS 是算出来的速度
请求数是 Counter(只增不减的里程表),必须配 rate 才是「每秒几个」——这是三角的第一条边。
# c16 口径:每个 handler 每秒被调几次
sum(rate(siliang_http_requests_total[5m])) by (handler)
并发是 Gauge 直接读
在途请求是温度计,当前多少就读多少,不用任何加工——这是三角的得数,也是最直观的「挤不挤」。
# c18 口径:业务在途 + ops 排空口径两条线
sum(siliang_http_requests_in_progress) by (method)
  + sum(siliang_ops_http_in_flight)
延迟是一个分布
延迟从来不是一个数,是一堆请求的排队分布。用 P95/P99 代表「慢的那批人」,平均值会被海量快请求稀释。
# c19 口径:全站最慢的 10 个接口排行榜
topk(10, histogram_quantile(0.99,
  sum(rate(siliang_http_request_duration_seconds_bucket[5m])) by (le, handler)))
利特尔法则会算术
知道任意两个就能估第三个。它是「雪崩为什么发生」的数学原理,也是容量预算的算尺。
# 并发 ≈ QPS × 延迟(延迟单位是秒)
# 100 QPS × 0.2s = 20 个在途
# 延迟恶化到 2s → 200 个在途(×10)
ops 排空口径专盯发布
c18 里多画的 ops 参考线是给发布用的:旧 worker 退出前应把在途请求干完,逐个归零才算干净退场。
# 发布后逐 worker 看排空线:归零 = 干净退场
sum(siliang_ops_http_in_flight) by (instance)
LLM 侧三角同理
等模型回话也是「每人待多久」:gpt-5 长输出跑到 60s+ 属正常,别当故障;但并发表会诚实地堆积。
# c14 口径:每家 provider 几通电话在通话中
sum(siliang_llm_active_calls{job="siliang_backend_worker"}) by (provider)
池是雪崩的中继站
并发一涨,先顶到的是连接池(每 worker 一座池)。使用率 80% 黄 / 95% 红,贴 100% 就是池打满开始排队。
# m115 口径:借出 ÷ 藏书,分子分母都抹抖动
max_over_time(siliang_db_pool_connections{state="checked_out"}[1m])
  / on(instance, pool) max_over_time(siliang_db_pool_capacity[1m])
5xx 是雪崩的终点
池打满后排队超时,错误率飙升——5xx 超过 1 req/s 就该排查。它是三角恶化的最后一环。
# c17 口径:服务端错误按 handler 拆
sum(rate(siliang_http_requests_total{status=~"5.."}[5m])) by (handler)

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

① HTTP QPS 按 handler(c16)——三角的「进店速度」,谁最忙、流量结构有没有突变看它:🟢 HTTP QPS(按 handler)↗

② HTTP 当前并发请求数(c18)——三角的「店里人数」,持续高位 = 在排队;发布时看 ops 排空线逐 worker 归零:🟢 HTTP 当前并发请求数 ↗

③ HTTP P99 延迟 Top10(c19)——三角的「每人待多久」,也是性能优化待办清单——延迟恶化就是雪崩的起点:🟢 HTTP P99 延迟(Top 10 慢接口)↗

④ LLM 并发与延迟(c14 / c15)——同一个三角在下游依赖上重演:gpt-5 长输出 60s+ 属正常,但并发持续高位说明大家在排队等模型:🟡 当前 LLM 并发调用数 ↗ · 🟡 LLM 调用延迟 P50/P95/P99 ↗

⑤ biz 盘镜像(b3 / b4)——biz 服务的并发与 P95 Top5,三角关系一模一样,池打满时与 b5 使用率对时间戳看:🟢 biz HTTP 并发 ↗ · 🟢 biz P95 Top5 ↗

🏭 生产实战 real world

场景 1 · 接口突然变慢:三图对时间戳定责

用户反馈「系统卡」。第一步不是翻日志,是把 c16/c18/c19 三张图调到同一时间窗:QPS 没涨而并发涨 = 走得慢了,去 c19 找哪个接口慢,再顺着它查下游。

# 第一步:流量涨没涨?(按 handler 看,找异常放量)
sum(rate(siliang_http_requests_total[5m])) by (handler)

# 第二步:并发在不在堆?(在途持续高位 = 在排队)
sum(siliang_http_requests_in_progress) by (method)

# 第三步:谁最慢?(P99 Top10 就是嫌疑人列表)
topk(10, histogram_quantile(0.99,
  sum(rate(siliang_http_request_duration_seconds_bucket[5m])) by (le, handler)))

# 判读:QPS 平 + 并发高 + P99 突起 = 延迟型故障,不是流量型

判读收尾:三条曲线同一时刻异动,谁先动谁就是起点。

场景 2 · 用公式做容量预算:流量要涨 3 倍,池子扛得住吗

产品说下周活动流量翻 3 倍。不用拍脑袋:先用公式算出当前「每 worker 并发」,乘 3,对照池容量(backend sync 30 / async 80 / checkpointer 30,per-worker)看余量。

# 当前每 worker 的平均在途并发(分母是当前 QPS,得到平均延迟,再乘回 QPS)
sum(siliang_http_requests_in_progress) by (instance)

# 活动期预算:并发_活动 ≈ QPS_活动 × 延迟_当前
# 例:现在 20 并发 @ 50 QPS(延迟 0.4s)→ 150 QPS 时并发 ≈ 60
# 对照 DB 池:sync 池每 worker 30 条 → 60 并发会打满 sync 池,需提前处理

判读收尾:算出来的活动期并发接近任何一个池的上限,就要先优化延迟或调池,而不是等活动炸。

场景 3 · LLM 变慢把并发顶高:确认瓶颈在下游

用户等生成结果等到超时。看 c15 发现 P50 整体抬升、c14 并发堆高、而我们自己的 HTTP 并发也高——这是下游模型慢的连坐,不是我们代码出 bug。

# 模型侧延迟:P50 抬升 = provider 变慢/限流
histogram_quantile(0.50, sum(rate(siliang_llm_call_duration_seconds_bucket[5m])) by (le, model))

# 模型侧并发:持续高位 = 大家在排队等回话
sum(siliang_llm_active_calls) by (provider)

# 再看错误率互证:大于 10% 多半是限流(429)/key 失效 → 切流量,不是修 bug
rate(siliang_llm_calls_total{outcome="error"}[5m])
  / clamp_min(rate(siliang_llm_calls_total[5m]), 1)

判读收尾:P50 抬升 + 并发高位 + 错误率不高 = 变慢;错误率大于 10% = 供应商故障,动作是切流量。

场景 4 · 发布窗口:确认旧 worker 干净退场

滚动发布最怕旧 worker 还攥着请求不放。ops 排空口径就是干这个的:发布后每条线应逐个归零,卡着不归零 = 排空卡住。

# 发布后排空观察:每个旧 worker 的在途数应逐个归零
sum(siliang_ops_http_in_flight) by (instance)

# 互证:业务在途也应在旧 worker 上回落、新 worker 上爬起
sum(siliang_http_requests_in_progress) by (instance)

判读收尾:5 分钟内逐 worker 归零 = 干净退场;某条线长期不归零 = 排空卡住,查那个 worker 在等什么。

场景 5 · 雪崩三阶段告警:在「刹车点」叫醒自己

雪崩从「有点慢」到「全崩」只要几分钟,等 5xx 响就晚了。按三阶段配告警:池 80% 是刹车点,并发高位是滑动中,5xx 是已经撞了。

# 阶段一(刹车点):DB 池使用率超 80% 持续 5 分钟(阈值以口径卡为准)
groups:
- name: siliang-pool
  rules:
  - alert: DbPoolHighUsage
    expr: |
      max_over_time(siliang_db_pool_connections{state="checked_out"}[1m])
        / on(instance, pool) max_over_time(siliang_db_pool_capacity[1m]) > 0.8
    for: 5m
    annotations:
      summary: "DB 池使用率超 80%:延迟恶化将是雪崩起点,立即查慢查询"

# 阶段二/三:并发持续高位 + 5xx 超 1 req/s(c17 口径)逐级升级

判读收尾:80% 告警响时动手,成本是查一条慢查询;等 5xx 告警响再动手,成本是全站故障复盘。

⚠️ 常见坑 pitfalls

坑 1 · 只看 QPS 宣布「流量没问题」 — 症状:QPS 曲线平稳,但用户在炸。原因:并发 ≈ QPS × 延迟,QPS 不变延迟翻倍,并发照样翻倍——店里挤不挤不只看进来多少人。正解:QPS 必须和延迟、并发放在一起看。
# 错: 只看 sum(rate(siliang_http_requests_total[5m])) 平稳 → 宣布无事
# 对: 三图对齐时间戳:QPS × 延迟 = 并发,看哪个量在动
坑 2 · 拿平均延迟当用户体验 — 症状:平均 160ms「一切正常」,客服却收到一堆超时投诉。原因:999 个 100ms 快请求把 1 个 60s 慢请求稀释得看不见。正解:看 P95/P99,它们才是慢那批人的代言人。
# 错: avg(rate 包不住延迟分布) → 平均值骗人
# 对: histogram_quantile(0.99, ... by (le, handler)) → P99 诚实暴露长尾
坑 3 · 见并发高就扩容 — 症状:加了机器并发还是高。原因:没分清「来得太多」还是「走得太慢」——延迟型堆积扩容治标不治本,新机器立刻被同样的慢查询拖住。正解:先用 QPS 排除流量型,延迟型去修慢查询/依赖。
# 错: 并发高 → 直接扩容 → 池还是满,钱白花
# 对: QPS 涨=流量型考虑扩容;QPS 平=延迟型,c19 找慢接口修根因
坑 4 · 三张图分开看、不对时间戳 — 症状:结论自相矛盾,「QPS 没涨啊」「并发明明高了」。原因:三个量不同时刻读数,三角关系对不上。正解:Grafana 里锁定同一时间窗,光标对齐同一时刻读三个数。
# 错: 上午看 QPS、下午看并发 → 两个数不在同一天,没法算
# 对: 同一时间窗并排 c16/c18/c19,光标竖线对齐同一时刻
坑 5 · 把 LLM 60s+ 当故障乱动 — 症状:深夜被告警吵醒,发现「LLM 延迟 60 秒」惊慌升级。原因:不知道口径——gpt-5 长输出跑到 60s+ 属正常。正解:看 c15 分模型判读:P50 突然整体抬升才是 provider 变慢的信号。
# 错: 看到 P99 = 60s 就当 P1 事故
# 对: gpt-5 长输出 60s+ 正常;P50 整体抬升 + 错误率涨才动手
坑 6 · 池容量忘了 per-worker 相乘 — 症状:以为 sync 池整机有 30 条,实际每 worker 各 30、4 个 worker 共 120 条——容量预算差 4 倍。正解:面板读到的都是每 worker 一条线,容量规划要乘 worker 数。
# 错: sync 池 = 30 条(整机)            # → 少算了 4 倍
# 对: sync 池 30 条/worker × 4 worker = 120 条
坑 7 · 对并发 Gauge 求 rate 算「堆积速度」 — 症状:给在途请求数套 rate,曲线忽正忽负没法判读。原因:并发是温度计不是里程表。正解:并发直接读数看水位,要看趋势用 deriv。
# 错: rate(siliang_http_requests_in_progress[5m])  # → Gauge 求 rate 无意义
# 对: siliang_http_requests_in_progress             # → 直接读数

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

Q1 · 用一句话向产品经理解释「为什么接口变慢会把系统拖垮」?

参考答案:店里人数 = 进店速度 × 每人待多久。出餐慢一倍,客人没变多,店里人数却翻倍;后端同理——延迟一涨,卡在处理中的请求(并发)就涨,连接池被占满,后来的请求连池子都借不到只能排队,排队又让延迟更高,几分钟内全线崩掉。所以「慢一点」从来不是小事,它是雪崩的第一张多米诺骨牌。

Q2 · 并发翻了 5 倍,怎么判断是「来得太多」还是「走得太慢」?

参考答案:拿同一时间戳的 QPS 图来对照。QPS 也涨了约 5 倍 = 流量型(活动、被刷),考虑扩容和限流;QPS 基本没动 = 延迟型,去 P99 Top10 找哪个接口变慢,再顺着它查下游(慢查询、LLM 变慢、Redis 抖动)。公式本身就是判别器:并发 ≈ QPS × 延迟,两个因子里看谁在动。

Q3 · 为什么池使用率 80% 就要告警,而不是等 100%?

参考答案:80% 是「刹车点」。池快满时说明并发已经顶上来了(多半是延迟在恶化),这时动作成本最低——查一条慢查询往往就解决。等 100% 才响,请求已经在排队,延迟更高、并发更高,正反馈已经转起来,从「有点慢」到全站 5xx 只要几分钟,那时的动作是救火而不是排查。

Q4 · 发布完成后,看哪条线确认旧 worker「干净退场」?标准是什么?

参考答案:看 c18 里的 ops 排空口径(siliang_ops_http_in_flight 按 instance 拆)。滚动发布时,每个旧 worker 的在途请求数应该逐个归零——把手上活干完再走。如果某条线卡在高位不归零,说明排空卡住(还在等一个慢请求或卡死的连接),要去查那个 worker 在等什么。

← 上一课:max_over_time 与 deriv:抹抖动与算斜率 📚 课程目录 下一课:连接池:图书馆借书 →