Java 程序员第 46 阶段10:大模型调用链路追踪,SkyWalking 排查线上性能,慢调用与瓶颈定位的 Trace 详情分析与性能剖析

前几篇我们打通了链路透传、修复了异步断点、配置了采样、用拓扑图锁定了瓶颈节点。现在到了最后一公里:当拓扑图告诉你「推理服务这一跳很慢」,你还需要钻进一条具体的慢 Trace,看清它到底慢在哪一段;如果慢在 Java 业务侧,还要进一步用性能剖析(Profile)把瓶颈钉到具体方法。本文把「Trace 详情分析」和「性能剖析」两套武器讲透,作为本系列收尾。
- 从拓扑下钻到慢调用 Trace
- Trace 详情页面怎么读:Span 树与耗时分布
- 实战:解剖一次大模型慢调用
- 性能剖析 Profile:把瓶颈钉到方法级
- 连续剖析与最佳实践
1. 从拓扑下钻到慢调用 Trace

排查路径是一条标准流水线:拓扑图定位节点(第 09 篇) -> 点击节点进入服务仪表盘 -> 切到「Trace」标签页 -> 按响应时间排序,挑出最慢的那几条。SkyWalking 的 Trace 列表已按延迟降序排列,并标注了每条 Trace 的总耗时、跨度数量、成功率。
点开一条慢 Trace,进入 Trace 详情。这一步之前请确认已经开启了合理的采样(第 08 篇):如果慢调用被采样丢弃,列表里就看不到它。好在 `slowTraceSegmentThreshold` 会强制保留慢链路,所以「为什么这么慢」的线索基本不会丢。
2. Trace 详情页面怎么读:Span 树与耗时分布

Trace 详情本质是一棵 Span 树。读懂它,关键是区分两个时间:
- **Total Duration(总耗时)**:该 Span 及其所有子 Span 的累计耗时。
- **Self Duration(自身耗时)**:该 Span 自己执行、不包含子 Span 的时间。
判断瓶颈的核心口诀是:**谁的总耗时长,说明它 subtree 慢;谁的自身耗时长,说明它自己这个环节在磨洋工**。例如「推理调用」Exit Span 总耗时 3s、自身耗时 3s,说明时间全花在等待推理服务返回,瓶颈在下游;而「提示词组装」Local Span 总耗时 800ms、自身耗时 800ms,说明业务逻辑自身慢(可能是 JSON 序列化、模板渲染)。
每个 Span 还带有标签(Tags)和日志(Logs),是分析的金矿:
|
标签/日志 |
含义 |
排障用途 |
|
--- |
--- |
--- |
|
http.method / http.url |
HTTP 方法与路径 |
定位是哪个接口 |
|
db.type / db.statement |
数据库类型与语句 |
定位慢 SQL / 慢向量查询 |
|
status_code |
响应状态码 |
区分成功但慢 vs 失败 |
|
error / log |
异常堆栈 |
定位报错根因 |
|
peer |
对端地址 |
确认下游目标实例 |
在 SkyWalking UI 里,Span 树展开后,颜色越红代表越慢,悬停能看到 self/total 耗时。顺着最红的路径往下点,通常三步之内就能定位到具体慢环节。
3. 实战:解剖一次大模型慢调用
假设一条对话 Trace 总耗时 3.4s,展开 Span 树如下(示例):
POST /api/chat total 3400ms self 5ms [Entry]
├─ buildPrompt total 820ms self 820ms [Local] <- 自身慢
├─ Redis GET (cache) total 12ms self 12ms [Exit]
├─ vectorSearch (HTTP) total 450ms self 450ms [Exit] <- 向量检索慢
├─ POST /v1/chat/completions total 2050ms self 2050ms[Exit] <- 推理慢(下游)
└─ postProcess total 60ms self 60ms [Local]
分析结论:
- `buildPrompt` 自身 820ms,是业务逻辑瓶颈,应检查提示词模板渲染、大对象拷贝、JSON 序列化。
- `vectorSearch` 450ms,向量检索偏慢,应优化索引或降级。
- `POST /v1/chat/completions` 2050ms,这是下游推理服务耗时,结合拓扑图已知推理节点变红,根因在 GPU 侧,不是 Java 的问题。
- 缓存命中很快(12ms),说明缓存有效。
为了在分析时拿到更多业务维度,建议在代码里用 `@Trace` 和 `ActiveSpan.tag` 给 Span 打上模型名、token 数等标签:
import org.apache.skywalking.apm.toolkit.trace.Trace;
import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;
import org.apache.skywalking.apm.toolkit.trace.Tag;
@Trace(operationName = "inference.call")
@Tag(key = "model", value = "arg[0].model")
public String callInference(ChatRequest req) {
// 把关键业务字段写进 Span,便于 Trace 列表过滤与下钻
ActiveSpan.tag("model", req.getModel());
ActiveSpan.tag("promptTokens", String.valueOf(req.getPromptTokens()));
ActiveSpan.tag("peer", "inference-service:8000");
try {
return doCall(req);
} catch (Exception e) {
ActiveSpan.error(e); // 异常写入 Span 日志
throw e;
}
}
这样在 Trace 列表里就能按 `model = qwen2.5-72b` 过滤,快速对比不同模型的耗时差异,定位是否某个特定模型拖慢了整体。
4. 性能剖析 Profile:把瓶颈钉到方法级
Trace 详情能告诉你「哪个环节慢」,但如果慢在 Java 业务自身(如 `buildPrompt` 自身 820ms),你还想知道「这 820ms 里,具体是哪个方法吃掉的」。这就是性能剖析(Profile)的用途。
SkyWalking 的 Profile 是按需触发的轻量剖析:你对某个服务的某个端点创建一条 Profile 任务,探针会在一段时间内周期性地采集目标实例的线程栈,最后聚合成「方法耗时列表」和「火焰图(Flame Graph)」。它不需要提前埋点,对线上性能影响很小。
通过 GraphQL 创建 Profile 任务:
curl -X POST 'http://oap:12800/graphql' \
-H 'Content-Type: application/json' \
-d '{
"query": "mutation { createProfileTask(input: { serviceId: \"llm-inference\", endpointName: \"POST:/api/chat\", duration: 10, interval: 10, minDurationThreshold: 1000, maxSamplingCount: 100 }) { id startTime } }"
}'
参数含义:
|
参数 |
含义 |
建议值 |
|
--- |
--- |
--- |
|
serviceId |
目标服务 |
从拓扑节点获取 |
|
endpointName |
剖析的端点 |
如 POST:/api/chat |
|
duration |
剖析持续分钟数 |
5~15 分钟 |
|
interval |
采样间隔(毫秒) |
10~20ms |
|
minDurationThreshold |
仅剖析超过该耗时的请求(毫秒) |
对齐慢调用阈值 |
|
maxSamplingCount |
最大采样数 |
100 左右 |
任务跑完后,在 SkyWalking UI 的「Profiling」里查看结果。火焰图中,横向宽度代表该方法占用 CPU/时间的比例,越宽的栈帧越可能是瓶颈。常见大模型 Java 侧瓶颈:
- 巨大的提示词对象被反复 JSON 序列化(Jackson 大对象)。
- 模板引擎(如 FreeMarker / Thymeleaf)渲染复杂提示词。
- 同步阻塞的 token 流处理未切异步。
- 锁竞争(如共享的 tokenizer 实例)。
找到宽栈帧后,针对性优化(缓存序列化结果、异步化、替换锁为 ThreadLocal),再用同样的 Profile 任务验证效果,形成闭环。
5. 连续剖析与最佳实践
除按需 Profile 外,SkyWalking 9.x 起支持连续性能剖析(Continuous Profiling),基于 eBPF 持续采集 CPU、网络等系统指标,无需手动建任务,适合长期观察。但它对内核版本有要求,落地前需确认环境支持。
把本系列五篇串起来,慢调用排查的最佳实践清单:
- **先拓扑后 Trace 再 Profile**:拓扑定位节点(第 09 篇)→ Trace 看环节(本文第 2~3 节)→ Profile 钉方法(本文第 4 节),层层下钻。
- **善用 self/total 区分**:self 慢是自身问题,total 慢看子 Span。
- **给 Span 打业务标签**:模型名、token 数让过滤和下钻更高效(本文第 3 节)。
- **Profile 选对端点与阈值**:只剖析慢请求,避免噪声;duration 别太长以免数据过大。
- **结合采样与强制采样**:保证慢 Trace 不被丢弃(第 08 篇),否则 Profile 也无从下手。
- **异步链路先修复**:若链路在 CompletableFuture 处断裂(第 07 篇),Trace 树会缺片段,Profile 采样到的线程栈也对不上,必须先保证链路连续。
6. Trace 标签与日志的进阶用法
基础打标签(第 3 节)已经能帮我们过滤,但工程里还有几个进阶技巧值得掌握。
**多标签与参数提取**。一个方法往往想记录多个业务维度,用 `@Tags` 组合多个 `@Tag`,并用 SpEL 从参数里取值,无需手写 `ActiveSpan.tag`:
@Trace(operationName = "inference.call")
@Tags({
@Tag(key = "model", value = "arg[0].model"),
@Tag(key = "promptTokens", value = "arg[0].promptTokens"),
@Tag(key = "stream", value = "arg[0].stream")
})
public String callInference(ChatRequest req) {
return doCall(req);
}
**结构化日志写入 Span**。除了 tag,还可以把关键中间结果以日志形式写进 Span,排查时直接在 Trace 详情里展开看到:
ActiveSpan.info("retrievedDocs=" + docs.size());
ActiveSpan.debug("ttftMs=" + ttft);
if (ttft > SLOW_THRESHOLD) {
ActiveSpan.tag("slowFirstToken", "true");
}
**隐私红线**。标签和日志会随 Trace 一起落库,千万不要把用户对话原文、手机号、token 等敏感信息写进 tag——既违反合规,也会让存储成本飙升。只记录「维度」和「计数」(如模型名、token 数、文档条数),不记录「内容」。
7. 火焰图深度解读与方法级优化闭环
火焰图看似复杂,记住三条规则就能读:
- **纵向是调用栈深度**:从最底下的线程根帧,往上每一层是它被谁调用。
- **横向宽度是「在该栈帧上采样到的比例」**:越宽的帧,在 CPU/时间上占比越高,越该优先优化。
- **最顶部的边缘帧是「叶子」(真正干活的方法)**:瓶颈几乎总是在某个很宽的叶子帧上。
以第 3 节那个 `buildPrompt` 自身 820ms 为例,火焰图把它拆成了「渲染模板 300ms + 序列化超大提示词 500ms」。优化动作很明确:提示词模板是固定的,把渲染结果缓存起来,避免每次请求重新渲染;超大提示词改用流式写出而非先拼成巨型 String 再序列化。改完后,用同样的 Profile 任务再跑一轮,火焰图里「序列化超大提示词」那个红框明显变窄,总耗时从 820ms 降到 200ms 左右——这就是「剖析 → 优化 → 再剖析验证」的闭环。
需要提醒:Profile 是采样而非全量计时,火焰图宽度是统计近似值,个别窄帧的微小差异不必纠结,关注「明显最宽的几个帧」即可。
8. 连续性能剖析(Continuous Profiling)实战注意
除按需 Profile 外,SkyWalking 9.x 起支持连续性能剖析。它基于 eBPF 在内核态持续采集目标实例的 CPU(on-CPU / off-CPU)、网络等指标,无需手动建任务,适合长期观察「偶发、说不清什么时候发生」的毛刺。
落地注意点:
- **环境要求**:需要 Linux 内核 4.x 以上,OAP 与探针开启对应模块,且探针进程具备 `CAP_BPF` / `CAP_SYS_ADMIN` 权限。容器化部署时要给探针容器加相应 capability,否则采集不上。
- **与按需 Profile 互补**:按需 Profile 适合「已知某端点慢、定点剖析」;连续剖析适合「不知道何时慢、长期监控」。大模型推理偶发的 GPU 等待会让线程 off-CPU(让出 CPU 等显存/计算),这类「不在 CPU 上跑却很慢」的瓶颈,on-CPU 采样看不出,off-CPU 的连续剖析反而能抓到。
- **开销控制**:连续剖析持续运行,开销高于按需任务,建议只在对性能最敏感的核心服务(如推理前置、提示词编排)开启,而非全量铺开。
- **权限与合规**:eBPF 采集的是系统级数据,需评估安全合规,避免在多租户共享节点上过度采集。
把本系列五篇串起来,慢调用排查的最佳实践清单:
- **先拓扑后 Trace 再 Profile**:拓扑定位节点(第 09 篇)→ Trace 看环节(本文第 2~3 节)→ Profile 钉方法(本文第 4 节),层层下钻。
- **善用 self/total 区分**:self 慢是自身问题,total 慢看子 Span。
- **给 Span 打业务标签**:模型名、token 数让过滤和下钻更高效(本文第 3、6 节)。
- **Profile 选对端点与阈值**:只剖析慢请求,避免噪声;duration 别太长以免数据过大。
- **结合采样与强制采样**:保证慢 Trace 不被丢弃(第 08 篇),否则 Profile 也无从下手。
- **异步链路先修复**:若链路在 CompletableFuture 处断裂(第 07 篇),Trace 树会缺片段,Profile 采样到的线程栈也对不上,必须先保证链路连续。
9. Trace 与日志、指标三位一体
可观测性的三大信号是「指标(Metrics)、链路(Traces)、日志(Logs)」,SkyWalking 主要覆盖前两者,日志由你的日志框架负责,而把它们粘起来的就是 TraceId。三者配合的典型排查流:
- **指标先报警**:拓扑图/告警发现推理服务 p99 突增(Metrics 层)。
- **链路定位环节**:下钻慢 Trace,确认耗时卡在 `POST /v1/chat/completions` 这个 Exit Span(Traces 层)。
- **日志看细节**:拿 traceId 去日志系统捞出这次请求在推理服务里的完整日志,看到具体的 GPU 排队、KV Cache 未命中或超时堆栈(Logs 层)。
缺任何一环都会变慢:只有指标没有链路,你不知道慢在哪一段;只有链路没有日志,你不知道那一段内部发生了什么;只有日志没有 traceId,海量日志里根本捞不出同一次请求。所以前面各篇强调的「链路透传不断」「异步不断」「traceId 进日志(第 07 篇 MDC)」都是在为这一刻服务。
10. 系列速查卡
把五篇的核心能力浓缩成一张卡,方便贴在工位上:
|
篇 |
解决的核心问题 |
核心工具 / 配置 |
|
--- |
--- |
--- |
|
06 跨进程透传 |
网关到推理服务链路串不起来 |
SW8 头、网关/webflux 插件、sw-python |
|
07 异步连续性 |
CompletableFuture / 线程池断链 |
ContextSnapshot、RunnableWrapper、@TraceCrossThread |
|
08 采样策略 |
高并发下开销与存储成本 |
agent.sample_rate、force_sample_error、slow_trace_segment_threshold、TTL |
|
09 拓扑分析 |
不知道瓶颈在哪个节点 |
服务/实例/端点拓扑、百分位、alarm-settings |
|
10 Trace 与 Profile |
不知道瓶颈在哪个方法 |
Trace Span 树 self/total、Profile 火焰图、%tid 日志联动 |
排障口诀可以记成一句话:**透传不断链,异步不断点,采样不漏错,拓扑先定位,Trace 下钻段,Profile 钉方法**。把这六步串成肌肉记忆,大模型线上任何性能问题都能在几分钟内有据可依地定位。
总结:SkyWalking 排查大模型线上性能,是一条「全局拓扑 → 慢 Trace 下钻 → 方法级 Profile」的完整链路。前四篇解决了数据「有没有、全不全、省不省、在哪慢」,本篇解决「到底哪个方法慢」。掌握这一套,你就能把大模型接口的任何性能问题,从模糊的「好慢啊」变成精确的「buildPrompt 里 JSON 序列化占了 600ms,换成缓存即可」,真正用可观测性驱动性能优化。
(本系列「Java 程序员第 46 阶段:大模型调用链路追踪,SkyWalking 排查线上性能」到此完结。)
更多推荐

所有评论(0)