Little's Law 大白话:并发 ≈ QPS × 延迟(店里人数 = 进店速度 × 每人待多久)。延迟一翻倍、并发就翻倍 → 池打满 → 雪崩。覆盖 核心盘 c16 / c18 / c19 三图联动 · LLM 侧 c14 / c15 同理。
你是奶茶店店长:每分钟进来 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 并发,时间戳必须对齐 |
# c16 口径:每个 handler 每秒被调几次 sum(rate(siliang_http_requests_total[5m])) by (handler)
# c18 口径:业务在途 + ops 排空口径两条线 sum(siliang_http_requests_in_progress) by (method) + sum(siliang_ops_http_in_flight)
# 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)
# 发布后逐 worker 看排空线:归零 = 干净退场 sum(siliang_ops_http_in_flight) by (instance)
# c14 口径:每家 provider 几通电话在通话中 sum(siliang_llm_active_calls{job="siliang_backend_worker"}) by (provider)
# m115 口径:借出 ÷ 藏书,分子分母都抹抖动 max_over_time(siliang_db_pool_connections{state="checked_out"}[1m]) / on(instance, pool) max_over_time(siliang_db_pool_capacity[1m])
# c17 口径:服务端错误按 handler 拆 sum(rate(siliang_http_requests_total{status=~"5.."}[5m])) by (handler)
① 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 ↗
用户反馈「系统卡」。第一步不是翻日志,是把 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 突起 = 延迟型故障,不是流量型
判读收尾:三条曲线同一时刻异动,谁先动谁就是起点。
产品说下周活动流量翻 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 池,需提前处理
判读收尾:算出来的活动期并发接近任何一个池的上限,就要先优化延迟或调池,而不是等活动炸。
用户等生成结果等到超时。看 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% = 供应商故障,动作是切流量。
滚动发布最怕旧 worker 还攥着请求不放。ops 排空口径就是干这个的:发布后每条线应逐个归零,卡着不归零 = 排空卡住。
# 发布后排空观察:每个旧 worker 的在途数应逐个归零 sum(siliang_ops_http_in_flight) by (instance) # 互证:业务在途也应在旧 worker 上回落、新 worker 上爬起 sum(siliang_http_requests_in_progress) by (instance)
判读收尾:5 分钟内逐 worker 归零 = 干净退场;某条线长期不归零 = 排空卡住,查那个 worker 在等什么。
雪崩从「有点慢」到「全崩」只要几分钟,等 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 告警响再动手,成本是全站故障复盘。
# 错: 只看 sum(rate(siliang_http_requests_total[5m])) 平稳 → 宣布无事 # 对: 三图对齐时间戳:QPS × 延迟 = 并发,看哪个量在动
# 错: avg(rate 包不住延迟分布) → 平均值骗人 # 对: histogram_quantile(0.99, ... by (le, handler)) → P99 诚实暴露长尾
# 错: 并发高 → 直接扩容 → 池还是满,钱白花 # 对: QPS 涨=流量型考虑扩容;QPS 平=延迟型,c19 找慢接口修根因
# 错: 上午看 QPS、下午看并发 → 两个数不在同一天,没法算 # 对: 同一时间窗并排 c16/c18/c19,光标竖线对齐同一时刻
# 错: 看到 P99 = 60s 就当 P1 事故 # 对: gpt-5 长输出 60s+ 正常;P50 整体抬升 + 错误率涨才动手
# 错: sync 池 = 30 条(整机) # → 少算了 4 倍 # 对: sync 池 30 条/worker × 4 worker = 120 条
# 错: rate(siliang_http_requests_in_progress[5m]) # → Gauge 求 rate 无意义 # 对: siliang_http_requests_in_progress # → 直接读数
参考答案:店里人数 = 进店速度 × 每人待多久。出餐慢一倍,客人没变多,店里人数却翻倍;后端同理——延迟一涨,卡在处理中的请求(并发)就涨,连接池被占满,后来的请求连池子都借不到只能排队,排队又让延迟更高,几分钟内全线崩掉。所以「慢一点」从来不是小事,它是雪崩的第一张多米诺骨牌。
参考答案:拿同一时间戳的 QPS 图来对照。QPS 也涨了约 5 倍 = 流量型(活动、被刷),考虑扩容和限流;QPS 基本没动 = 延迟型,去 P99 Top10 找哪个接口变慢,再顺着它查下游(慢查询、LLM 变慢、Redis 抖动)。公式本身就是判别器:并发 ≈ QPS × 延迟,两个因子里看谁在动。
参考答案:80% 是「刹车点」。池快满时说明并发已经顶上来了(多半是延迟在恶化),这时动作成本最低——查一条慢查询往往就解决。等 100% 才响,请求已经在排队,延迟更高、并发更高,正反馈已经转起来,从「有点慢」到全站 5xx 只要几分钟,那时的动作是救火而不是排查。
参考答案:看 c18 里的 ops 排空口径(siliang_ops_http_in_flight 按 instance 拆)。滚动发布时,每个旧 worker 的在途请求数应该逐个归零——把手上活干完再走。如果某条线卡在高位不归零,说明排空卡住(还在等一个慢请求或卡死的连接),要去查那个 worker 在等什么。