- 背景与故障现象
- 排障思路总览:指标-链路-日志三线并进
- 第一步:Grafana 指标锁定异常窗口
- 第二步:SkyWalking 链路定位慢 Span
- 第三步:ELK 日志还原现场细节
- 根因分析与验证
- 复盘与长效防线
1. 背景与故障现象
某电商智能客服系统在大模型升级(切换为带 RAG 的 `gpt-4o` 多轮对话)上线后第三天,客服坐席集中反馈:"下午高峰时段,用户提问后平均要等 8 秒才出首字,超时工单明显增多"。系统架构如下:`nginx` → `gateway` → `llm-gateway`(编排层)→ `rag-service`(知识检索)→ `vector-db`(向量库)/ `llm-proxy`(模型网关)→ 外部大模型 API。
服务部署在 K8s,8 个 `llm-gateway` Pod,SkyWalking Agent 全量接入,Prometheus + Grafana 做指标,ELK 做日志,链路采样 10%。
故障现象(来自监控与工单):
- Grafana 上 `llm-gateway` 的 P99 从日常 1.2s 升至 8.5s,且只在 14:00~15:30 出现。
- SkyWalking 中该时段慢 Trace 占比从 2% 升到 35%。
- 工单系统超时告警(>5s)同时间段增长 6 倍。
- 非高峰时段一切正常。
正常时段: P99 ≈ 1.2s 慢 Trace 占比 ≈ 2%
高峰时段: P99 ≈ 8.5s 慢 Trace 占比 ≈ 35% 超时工单 x6
2. 排障思路总览:指标-链路-日志三线并进
这次排障严格遵循前三篇建立的方法论:先用**指标**圈定"何时、哪个服务、多严重",再用**链路**定位"具体哪个 Span 慢",最后用**日志**还原"为什么慢"。三者不是串行,而是互相印证。
阶段 | 工具 | 要回答的问题 | 输出 |
--- | --- | --- | --- |
圈定 | Grafana/Prometheus | 异常时间窗?哪个服务? | 14:00-15:30, llm-gateway |
定位 | SkyWalking | 哪类 Span 慢? | VectorSearch + LLM.Infer |
还原 | ELK | 为什么慢?有没有重试/排队? | 线程池打满 + 重试风暴 |
验证 | 三者联动 | 修复后是否回落? | P99 回到 1.5s |
3. 第一步:Grafana 指标锁定异常窗口
打开 Grafana 的 `llm-gateway` 大盘,先把时间窗放宽到当天,确认异常只在 14:00~15:30(与峰值时段的客服排班吻合)。用按 `endpoint` 分组的 PromQL 看是哪个接口拖后腿:
# 按端点分组的 P99
histogram_quantile(0.99,
sum(rate(skywalking_endpoint_latency_bucket{service="llm-gateway"}[5m]))
by (le, endpoint))
结果显示只有 `/v1/chat/completions` 飙升,其他接口平稳,排除全局资源问题。接着看并发线程:
jvm_thread{service="llm-gateway", state="RUNNABLE"} # 接近线程池上限
并发线程数在 14:00 后爬升到线程池上限(200),而 `BLOCKED` 线程也同步上升——这是关键信号:**不是单请求慢,而是请求在排队/阻塞**。
同时观察 `llm-proxy` 的成功率指标,发现该时段 `service_sla` 反而平稳在 99.2%,说明**外部模型 API 并不慢**。矛头指向内部:RAG 检索或线程编排。
4. 第二步:SkyWalking 链路定位慢 Span
在 SkyWalking UI 中,按 `service=llm-gateway`、`endpoint=/v1/chat/completions`、`minDuration=5000`(5s)拉取该时段慢 Trace。抽样一条 8.3s 的 Trace,Span 瀑布如下:
Total 8300ms
├─ Gateway 接收 20ms
├─ 鉴权/限流 30ms
├─ RAG 编排
│ ├─ VectorSearch 4200ms ⚠ 异常长
│ └─ ReRank 120ms
├─ LLM.Infer 3200ms
└─ 后处理/流式返回 760ms
但更关键的是另一条 9.1s 的 Trace,其 Span 结构不同:
Total 9100ms
├─ Gateway 接收 20ms
├─ RAG 编排
│ ├─ VectorSearch 150ms (正常)
│ └─ ReRank 100ms
├─ LLM.Infer (重试 x3) 8600ms ⚠ 三次重试累计
│ ├─ attempt-1 超时 3000ms
│ ├─ attempt-2 超时 3000ms
│ └─ attempt-3 成功 2600ms
这说明慢链路有两种形态:**A 类 = 向量检索慢**,**B 类 = 模型调用重试风暴**。继续在 SkyWalking 按 `endpoint` + `tag` 过滤,统计两类占比:A 类约 60%,B 类约 40%。
5. 第三步:ELK 日志还原现场细节
用一条 B 类慢 Trace 的 `tid` 去 Kibana 检索,看到:
15:02:11.020 [llm-pool-12] INFO LLM 调用开始 model=gpt-4o timeout=3000ms
15:02:14.021 [llm-pool-12] WARN LLM 调用超时, 触发重试 attempt=1
15:02:17.022 [llm-pool-12] WARN LLM 调用超时, 触发重试 attempt=2
15:02:19.622 [llm-pool-12] INFO LLM 调用成功 attempt=3 cost=2600ms
`timeout=3000ms` 却反复超时,结合 `llm-proxy` 成功率正常,怀疑是**客户端超时设置小于模型真实 P99 推理时间**,高峰时排队导致偶发超 3s,触发重试;而重试又加剧了线程占用,形成**重试风暴**。
再看 A 类,用其 `tid` 检索:
15:03:02.100 [llm-pool-31] INFO VectorSearch 开始 topK=20
15:03:06.300 [llm-pool-31] INFO VectorSearch 结束 cost=4200ms rows=20
向量检索在高峰变慢,查 `vector-db` 日志发现该时段有另一个批处理任务在重建索引,抢占 CPU 与 IO,导致在线检索 P99 从 150ms 涨到 4.2s。
6. 根因分析与验证
综合三线证据,根因有两个,且相互放大:
- **客户端超时过短 + 无退避重试**:`timeout=3000ms` 在高峰偶发触发,且重试是**立即重试**(无退避、无熔断),重试请求继续占用线程池,线程池打满后所有请求排队,延迟雪崩。
- **资源争用**:`vector-db` 的离线索引重建任务与在线检索同机运行,高峰抢占资源,VectorSearch 变慢进一步拉长链路、占用线程。
验证方式:先在预发环境分别复现两个因素。用压测工具模拟高峰并发,确认无重试时 P99 仅 4.5s(A 类主导);加上立即重试后 P99 飙到 9s(B 类放大)。
修复方案(下一篇会展开代码改造):
- 客户端超时调整为 8s,重试改为**指数退避 + 熔断**(Sentinel),并限制单请求最大重试 1 次。
- `vector-db` 索引重建任务改为低峰(凌晨)执行,并限制其 CPU cgroup 配额。
- 线程池从固定 200 改为按业务隔离:`rag-pool` 与 `llm-pool` 分离,避免互相拖垮。
7. 复盘与长效防线
修复后上线,连续观察三天:
指标 | 修复前(高峰) | 修复后(高峰) |
--- | --- | --- |
P99 延迟 | 8.5s | 1.6s |
慢 Trace 占比 | 35% | 3% |
超时工单/日 | 180 | 12 |
线程池打满 | 是 | 否 |
**长效防线建议**:
- 指标层:对 `llm-gateway` 增加"重试率"指标与告警(重试率 > 5% 即告警),比延迟告警更早暴露问题。
- 链路层:给 `LLM.Infer` 打 `retry_count` tag,SkyWalking 中可一键筛选重试链路。
- 日志层:重试必须 WARN 级并打印 `attempt` 与 `cost`,便于事后统计。
- 容量层:离线任务与在线服务错峰/隔离,必要时 HPA 按 P99 自动扩缩容。
**小结**:本次故障是"短超时 + 立即重试 + 资源争用"三者叠加。正是前三篇搭建的指标、链路、日志三件套,让我们在 40 分钟内从"用户说慢"定位到两个确切根因。下一篇我们把这次经验沉淀为"从链路洞察到代码改造"的性能优化闭环。