当大模型应用上线之后,一个用户发起的对话请求往往会穿过多个进程:CDN / 负载均衡、API 网关、Java 业务服务、推理服务(如 vLLM / Triton)、向量数据库、缓存等。任何一个进程出现延迟或异常,都可能影响整体响应时间。要定位问题,第一步也是最关键的一步,就是让同一请求的链路上下文(TraceId)在所有进程之间保持一致并透传下去。本文聚焦 SkyWalking 的跨进程链路传播机制,带你把网关到推理服务的整条链路串起来。
- 链路透传的核心价值与典型场景
- SkyWalking 跨进程传播协议 SW8 详解
- 实战:网关到推理服务的链路透传配置
- 自定义业务字段与 Header 透传
- 跨语言调用与常见问题排查清单
1. 链路透传的核心价值与典型场景
在单体应用时代,一次请求通常在一个 JVM 内完成,问题排查靠本地日志和 IDE 断点即可。但大模型应用是典型的分布式系统:请求进入网关后,Java 业务服务负责鉴权、限流、提示词组装,再转发给推理服务;推理服务调用向量数据库做检索增强(RAG),调用缓存降低重复计算。任何一个环节的毛刺都会被放大到用户侧。
没有链路透传时,每个服务各自打印日志,彼此之间没有关联。你拿着网关的时间戳去业务服务里捞日志,发现对不上;业务服务报了一个慢调用,却无法确认是不是推理服务拖慢的。SkyWalking 的分布式追踪正是为了解决这个痛点:它给每次请求分配一个全局唯一的 TraceId,并且通过「上下文透传」让下游进程继续沿用这个 TraceId,从而在后端把这些分散的 Span 拼接成一条完整链路。
链路透传在大模型场景下有三个特别突出的价值:
- **端到端耗时拆分**:可以清晰看到提示词组装、推理首 token 延迟(TTFT)、整段生成耗时各占多少。
- **跨团队协同**:网关团队、Java 团队、算法团队(推理服务)各看各的 Span,但共用一个 TraceId,沟通成本骤降。
- **根因定位**:当某个大模型的 P99 延迟飙升时,能立刻区分是网关排队、业务编排慢,还是推理卡瓶颈。
2. SkyWalking 跨进程传播协议 SW8 详解
SkyWalking 在跨进程传播时使用名为 `sw8` 的请求头(HTTP Header)。它是一个由 `-` 连接的字符串,SkyWalking 探针在发起跨进程调用(Exit Span)时自动写入该 Header,在接收端(Entry Span)自动解析并延续链路。其标准格式如下:
1-{trace_id}-{segment_id}-{span_id}-{service}-{instance}-{endpoint}-{peer}
各字段含义如下表所示:
字段位置 | 字段名 | 含义 | 示例 |
--- | --- | --- | --- |
1 | 协议版本 | 固定为 1,表示 SW8 协议 | 1 |
2 | trace_id | 全局链路 ID,保证整条链路一致 | 7c8e1a2b3c4d5e6f |
3 | segment_id | 当前进程的段 ID(Segment 是进程内 Span 集合) | 9a0b1c2d3e4f5a6b |
4 | span_id | 父 Span 在当前段内的编号 | 0 |
5 | service | 上游服务名 | gateway-service |
6 | instance | 上游实例名(通常含 IP/端口) | gateway-service@10.0.1.2 |
7 | endpoint | 上游入口端点(如 HTTP 路径) | POST:/api/chat |
8 | peer | 对端地址(下游目标地址) | 10.0.2.5:8080 |
除了 `sw8`,SkyWalking 还定义了两个扩展头:
- `sw8-x`:用于透传跨进程的「关联上下文(Correlation Context)」,也就是业务自定义 KV。
- `sw8-correlation`:旧版本中用于透传相关性数据,新版本统一走 `sw8-x`。
理解这个协议很重要,因为它解释了为什么链路「断」了:只要接收端没有正确解析 `sw8`(比如探针未覆盖、插件缺失、或跨语言手动注入出错),下游就会生成一条全新的 TraceId,链路从中间断开。
需要特别强调的是,SW8 协议是 SkyWalking 私有的跨进程传播格式,与 W3C Trace Context(`traceparent`)并不互通。如果你的网关是 Nginx 或 Envoy,并且希望和 SkyWalking 协同,需要确认其是否兼容 SW8,或者在边界处做协议转换。
3. 实战:网关到推理服务的链路透传配置
下面以「Spring Cloud Gateway + Java 业务服务 + 推理服务(HTTP 调用)」为例,给出落地配置。核心思路是:网关作为链路的第一个 Entry Span,后续所有 HTTP 调用由 SkyWalking 的插件自动注入 `sw8` 头,无需手写代码。
第一步,为网关和业务服务分别准备 `agent.config`。两者结构一致,仅 `service_name` 不同:
agent.service_name=${SW_AGENT_NAME:gateway-service}
collector.backend_service=${SW_AGENT_BACKEND:oap:11800}
agent.sample_rate=${SW_AGENT_SAMPLE_RATE:1}
agent.ignore_suffix=${SW_IGNORE_SUFFIX:.jpg,.jpeg,.png,.css,.js,.html}
第二步,启动网关时挂载探针,并确保使用与网关版本匹配的插件。Spring Cloud Gateway 在不同大版本下需要不同插件目录:
java -javaagent:/opt/skywalking/agent/skywalking-agent.jar \
-DSW_AGENT_NAME=gateway-service \
-DSW_AGENT_COLLECTOR_BACKEND_SERVICES=oap:11800 \
-jar gateway-service.jar
SkyWalking 对 Spring Cloud Gateway 提供了 `apm-spring-cloud-gateway-2.x-plugin`、`3.x-plugin`、`4.x-plugin`,版本必须对齐,否则网关内部基于 WebFlux 的响应式链路无法被拦截,最终导致网关这一跳「消失」。
第三步,Java 业务服务以 OpenFeign 或 RestTemplate 调用推理服务,SkyWalking 的 `apm-httpclient-*`、`apm-feign-*` 插件会自动把 `sw8` 写入出站请求。如果你使用的是 `WebClient`(响应式),则需要 `apm-spring-webflux-*` 插件。下面是一段调用推理服务的示例代码,无需任何链路相关代码,纯业务即可:
@Service
public class InferenceClient {
private final WebClient webClient;
public InferenceClient(WebClient.Builder builder) {
this.webClient = builder.baseUrl("http://inference-service:8000").build();
}
public Flux<String> streamChat(ChatRequest request) {
return webClient.post()
.uri("/v1/chat/completions")
.bodyValue(request)
.retrieve()
.bodyToFlux(String.class);
}
}
只要插件就位,上面的 `streamChat` 调用会自动产生一个 Exit Span,并在请求头里携带 `sw8`,推理服务收到后延续同一 TraceId。
4. 自定义业务字段与 Header 透传
仅靠 TraceId 有时不够。排查大模型问题时,我们常希望把 `userId`、`sessionId`、`modelName`(如 qwen2.5-72b)这些业务字段也随链路一起透传,这样在 SkyWalking UI 上就能按业务维度过滤。SkyWalking 提供了「关联上下文(Correlation Context)」机制。
在业务服务中,可以通过 `ActiveSpan` 设置关联字段,它们会随 `sw8-x` 透传到下游:
import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
public void handleChat(String userId, String sessionId, String modelName) {
// 把业务字段写入关联上下文,随 sw8-x 透传到下游
ActiveSpan.setCorrespondingTraceId(userId);
ActiveSpan.tag("sessionId", sessionId);
ActiveSpan.tag("modelName", modelName);
// 也可以读取当前 TraceId,写入业务日志,实现日志与链路打通
String traceId = TraceContext.traceId();
log.info("start chat traceId={} userId={} model={}", traceId, userId, modelName);
}
如果需要在跨进程的 HTTP 调用里手动携带额外的自定义 Header(例如某些审计字段),可以通过网关过滤器统一注入:
@Component
public class TraceHeaderGatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String traceId = TraceContext.traceId();
ServerHttpRequest req = exchange.getRequest().mutate()
.header("X-Trace-Id", traceId)
.build();
return chain.filter(exchange.mutate().request(req).build());
}
@Override
public int getOrder() { return -1; }
}
这样业务服务和推理服务都能从 `X-Trace-Id` 读取链路 ID,即使推理服务没有被 SkyWalking 探针覆盖(见下一节),也能通过日志把两者关联起来。
5. 跨语言调用与常见问题排查清单
大模型推理服务多数用 Python(vLLM、FastAPI、Triton)。Python 进程默认不会解析 `sw8`,链路到这里就会断开。有两种处理思路:
方案一:为 Python 推理服务也挂载 SkyWalking Python 探针(`sw-python`),它会自动解析 `sw8` 并延续链路。安装与启动示例如下:
pip install apache-skywalking
sw-python run python inference_server.py # 启动时挂载 agent
方案二:若无法挂载探针,则在 Java 侧手动把 `sw8` 头取出,转成 Python 可识别的字段(如 W3C `traceparent` 或直接透传 `X-Trace-Id`),由推理服务把该 ID 写入自己日志。下面演示如何手动读取 `sw8`:
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
public Map<String, String> buildInferenceHeaders() {
Map<String, String> headers = new HashMap<>();
// SkyWalking 自动透传 sw8,但如需要跨语言改造,可手动取出 traceId
headers.put("X-Trace-Id", TraceContext.traceId());
headers.put("X-Segment-Id", TraceContext.segmentId());
return headers;
}
最后,整理一份高频「链路断裂」排查清单:
现象 | 可能原因 | 排查与修复 |
--- | --- | --- |
网关这一跳在拓扑图中缺失 | 网关插件版本与 Spring Cloud Gateway 版本不匹配 | 核对 `apm-spring-cloud-gateway-x.x-plugin` 版本 |
业务服务到推理服务链路断开 | 使用了 WebClient 但未启用 webflux 插件 | 放入对应 `apm-spring-webflux-*` 插件 |
推理服务(Python)不在链路中 | 未挂载 Python 探针 | 安装 `apache-skywalking` 并用 `sw-python run` 启动 |
自定义业务字段下游取不到 | 字段写入了 tag 而非 correlation | 使用 `ActiveSpan.setCorrespondingTraceId` 或 `sw8-x` |
链路整体偶发断裂 | 网关对 sw8 头做了清洗/转发拦截 | 检查网关是否透传自定义 Header,必要时白名单放行 |
6. 消息队列与 gRPC 场景的透传
大模型应用里,除了 HTTP 同步调用,还常见两类异步跨进程通信,它们的透传方式略有不同:
**消息队列(Kafka / RocketMQ / RabbitMQ)**。当把长耗时生成任务丢进队列异步处理时,链路会从「生产者」经「消息」跳到「消费者」。SkyWalking 的 MQ 插件会在生产者侧创建一个 Exit Span,并把 `sw8` 写入消息的 Header / Properties;消费者侧从消息里解析 `sw8`,创建一个 Entry Span,从而延续链路。如果使用的是社区未覆盖的自研 MQ 客户端,就要手动处理:
// 生产者:把快照写入消息头
public void sendTask(InferenceTask task) {
ContextSnapshot snapshot = ContextManager.capture();
String sw8 = ContextManager.serializeContext(); // 序列化上下文
task.setHeader("sw8", sw8);
mqTemplate.send(task);
}
// 消费者:解析并续接上下文
@RabbitListener(queues = "inference")
public void onTask(InferenceTask task) {
ContextManager.deserializeContext(task.getHeader("sw8"));
try {
doInference(task);
} finally {
ContextManager.stopSpan();
}
}
**gRPC**。推理服务若以 gRPC 暴露(如 Triton),需启用 `apm-grpc-1.x-plugin`,它会自动在 gRPC 的 metadata 里传递上下文。需要注意双向流(bidi streaming)场景:流是长连接,一个 Stream 内可能承载多次推理请求,插件通常按每次 `onNext` 切分 Span,但要确认插件版本与 gRPC 版本匹配,否则可能出现一条流内所有请求被合并成一个 Span 的情况。
7. 端到端验证:怎么确认链路真的串起来了
配置完别急着上线,先做端到端验证,避免「以为通了其实断了」。三种验证手段:
**手段一:UI 比对 TraceId**。在 SkyWalking UI 的「Trace」里点开任意一条对话链路,展开 Span 树,确认网关、业务服务、推理服务的 Span 都在同一 traceId 下,且父子层级正确。如果推理服务的 Span 单独成一条、traceId 不同,说明透传失败。
**手段二:日志比对**。在网关、业务、推理三处分别打印 `TraceContext.traceId()` 到业务日志,用同一个用户请求触发一次对话,然后 grep 三个服务的日志,确认打印出的 traceId 完全一致:
log.info("service=gateway traceId={} uri={}", TraceContext.traceId(), uri);
**手段三:GraphQL 查询**。直接查 OAP 拿到某条 Trace 的所有 Span,脚本化校验跨服务一致性:
curl -X POST 'http://oap:12800/graphql' \
-H 'Content-Type: application/json' \
-d '{"query":"query { trace(traceId:\"7c8e1a2b3c4d5e6f\") { spans { serviceCode operationName peer } } }"}'
验证时还有一个隐蔽坑:**服务器时钟不同步**。如果网关、业务、推理三台机器时钟偏差几秒,SkyWalking 展示 Span 时序时会错乱,看起来像「先有子 Span 后有父 Span」。所有节点务必接入 NTP 时间同步,trace 的时序图才准确。
8. 网关侧的调试技巧
在排查透传问题时,网关是第一现场。可以在网关加一个调试过滤器,把进出请求的 `sw8` 头打印出来,确认它确实被注入与转发:
@Component
public class Sw8DebugFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String inSw8 = exchange.getRequest().getHeaders().getFirst("sw8");
log.info("incoming sw8={}", inSw8); // 入口应为空(首跳)
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 转发后,下游应在请求头里看到 sw8
log.info("outgoing sw8 present={}", exchange.getRequest().getHeaders().containsKey("sw8"));
}));
}
@Override
public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }
}
首跳(`incoming sw8` 为空)是正常的:请求从用户进来,网关创建 Entry Span 并生成 traceId;之后网关向业务服务转发时,插件自动写入 `sw8`。如果你在入口就看到 `sw8` 有值,反而说明上游(如前置 Nginx/Envoy)已经创建了链路,这时要确认不会和网关的插件重复创建 Segment。
9. 透传的安全边界:别让 Trace 上下文外泄
链路透传带来便利,也有安全边界要注意。`sw8` 头里携带了服务名、实例地址、端点等信息,这些属于内部架构细节。当你的 Java 业务服务需要出公网调用第三方大模型(如公网托管的 OpenAI 兼容接口)时,务必在网关或出口处把 `sw8` 头剥离,**不要**透传给外部:
- 一方面避免泄露内部服务拓扑与实例 IP,给攻击者提供侦察线索;
- 另一方面避免外部回传一个伪造/错误的 `sw8`,污染你本地链路的父子关系,造成监控错乱。
出口剥离的写法很简单,在转发到外部前移除该头即可:
@Component
public class ExternalCallFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest req = exchange.getRequest().mutate()
.headers(h -> h.remove("sw8")) // 出公网前剥离内部链路头
.build();
return chain.filter(exchange.mutate().request(req).build());
}
@Override
public int getOrder() { return Ordered.LOWEST_PRECEDENCE; }
}
同时,关联上下文(correlation,`sw8-x`)里只应放「维度与计数」(如 userId、modelName、token 数),**绝对不要**放大模型对话原文、用户隐私或密钥——这些信息会随 Trace 落库,既违反合规又推高存储成本。
10. 关键配置速查表
把本文涉及的核心配置汇总,方便上线前对照检查:
配置项 | 位置 | 推荐值 | 作用 |
--- | --- | --- | --- |
agent.service_name | agent.config | 服务唯一名 | 拓扑节点标识 |
collector.backend_service | agent.config | oap:11800 | 上报地址 |
agent.sample_rate | agent.config | 0.1(生产) | 探针采样率 |
agent.ignore_suffix | agent.config | 静态资源后缀 | 忽略无关请求 |
agent.force_sample_error_segment | agent.config | true | 错误强制采样 |
网关插件版本 | agent/plugins | 对齐 Gateway 大版本 | 拦截 WebFlux 链路 |
webflux / httpclient 插件 | agent/plugins | 按需放置 | 覆盖响应式与 HTTP 调用 |
sw-python 探针 | Python 推理服务 | 启用 | 跨语言延续 sw8 |
上线前对照这张表逐项确认,能避免绝大多数「链路断裂」「拓扑缺节点」的问题。
总结:跨进程链路透传是 SkyWalking 排查大模型性能问题的地基。只要保证 `sw8` 在每一跳正确注入与解析,你就能从网关一路追到推理服务,把每一段耗时钉在具体的进程与端点上。下一篇我们将讨论链路进入异步线程池后为什么会「断」,以及如何修复。