Java 程序员第 46 阶段12:大模型调用链路追踪,SkyWalking 排查线上性能,自定义标签与日志联动Trace与Log关联排查上下文
上篇我们解决了大模型调用慢在哪一段的问题。但慢只是表象真正排障时你更常遇到的是这条慢链路对应的业务上下文是什么 —— 比如用户问了什么、命中了哪个知识库、模型返回了什么敏感内容、当时的租户 ID 是多少。这些信息不可能全塞进 SkyWalking 的 TagTag 要求低基数它们天然属于日志。本篇讲两件联动的事第一如何用自定义标签把业务维度打在 Trace 上让链路可按租户、按模型、按意图筛选第二如何把 traceId / spanId 注入到应用日志并在 SkyWalking UI 里实现从一条 Trace 直接跳到它产生的日志。掌握这两点你就能把链路和上下文拼成完整证据链。为什么要把 Trace 和 Log 关联起来SkyWalking 的日志插件与 TraceContext自定义标签从业务维度给链路打标代码实战在日志中注入 traceId / spanId通过 gRPC 上报日志到 OAPUI 中联动排查从 Trace 跳日志最佳实践与踩坑1. 为什么要把 Trace 和 Log 关联起来在微服务架构里日志和链路长期是两套体系日志管发生了什么链路管花了多久、调了谁。但线上排障需要的是二者的交集。举个大模型场景的真实案例某个租户反馈回答里出现了不该出现的价格。你用 SkyWalking 找到那条慢 Trace看到 llm.inference 正常但仅凭链路你不知道它问了什么、RAG 召回了哪段知识。此时若日志里带着同一个 traceId你就能用 traceId 搜到这次请求打印的 userQuery...、kbHit...、modelOutput...三秒钟还原现场。没有关联时排查路径是先看监控发现慢 → 再去日志系统按时间戳 关键字盲搜 → 还要对齐线程和时间窗口极易找错日志。有了关联路径变成点开 Trace → 一键跳到关联日志效率提升一个数量级。SkyWalking 提供了两条关联路径日志里带 traceId被动关联靠搜索以及日志直接上报到 OAP 并和 Trace 绑定主动关联可点击。2. SkyWalking 的日志插件与 TraceContextSkyWalking 的日志关联依赖 TraceContext。只要在挂载了 Agent 的 JVM 内任何地方都能拿到当前链路的 IDString traceId TraceContext.traceId();String segmentId TraceContext.segmentId();String spanId String.valueOf(TraceContext.spanId());SkyWalking 提供了多种日志框架的桥接插件位于 agent/optional-pluginsapm-toolkit-logback-1.x-activation桥接 Logback自动把 traceId 写进 MDC。apm-toolkit-log4j-2.x-activation桥接 Log4j2。apm-toolkit-log4j-1.x-activation桥接 Log4j 1.x。要使用把对应 jar 从 optional-plugins 复制到 plugins 目录然后在日志 Pattern 里引用 %tid 即可输出 traceId。注意 TraceContext.traceId() 返回空字符串时代表当前不在链路上下文中如后台定时线程需要做判空兜底。除日志桥接外SkyWalking 还支持把日志通过 gRPC 直接上报 OAP见第 5 节这样日志会作为 Trace 的附属数据存储在 OAP 中实现 UI 内联动。3. 自定义标签从业务维度给链路打标Tag 是 SkyWalking 里给 Span 附加键值对的能力。和日志不同Tag 会被索引、可用于查询和拓扑筛选因此必须低基数。大模型业务里推荐把以下业务维度做成 Tag而非写日志Tag 名取值示例用途---------llm.tenantt_1001 / t_1002按租户隔离分析性能llm.modelgpt-4o / qwen-max对比不同模型耗时llm.intent问答 / 摘要 / 翻译按意图聚合llm.isStreamtrue / false区分流式与非流式llm.errorTypetimeout / 429 / null错误归类注意不要把 userQuery 全文、userId 原文高基数作为 Tag。它们应当进日志。Tag 是分桶用的日志才是明细用的。ActiveSpan.tag(key, value) 即可打标value 必须是字符串。若 value 为 null 会抛异常需判空。4. 代码实战在日志中注入 traceId / spanId以 Logback 为例。首先激活插件复制 jar 到 plugins 目录然后修改 logback-spring.xml 的 Pattern加入 %tidappender nameCONSOLE classch.qos.logback.core.ConsoleAppenderencoderpattern%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} %tid - %msg%n/pattern/encoder/appender%tid 会渲染成 [TRACEID,SEGMENTID,SPANID] 形式无链路时为空。这样每条业务日志都自带链路坐标。接着在业务代码里除了打印明细也把关键上下文打上 Tagpackage com.demo.llm.service;import org.apache.skywalking.apm.toolkit.trace.ActiveSpan;import org.apache.skywalking.apm.toolkit.trace.TraceContext;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Service;Servicepublic class ChatService {private static final Logger log LoggerFactory.getLogger(ChatService.class);public String chat(String tenant, String intent, String model, String userQuery) {// 业务维度打标低基数ActiveSpan.tag(llm.tenant, tenant);ActiveSpan.tag(llm.intent, intent);ActiveSpan.tag(llm.model, model);// 明细进日志高基数带 traceId 自动注入log.info(用户提问 tenant{} intent{} query{}, tenant, intent, userQuery);String output doInference(model, userQuery);// 输出也可能很长只记录长度与风险标记正文进日志log.info(模型返回 tenant{} outputLen{} output{}, tenant, output.length(), output);return output;}private String doInference(String model, String q) { return ...; }}运行后控制台日志会出现类似... [TID: 3a9f... , 2b1c... , 0] - 用户提问 ...这就是关联键。5. 通过 gRPC 上报日志到 OAP仅把 traceId 打进本地日志仍需去 ELK/Loki 里搜。若希望在 SkyWalking UI 里直接看日志需要把日志上报到 OAP。SkyWalking 提供 apm-toolkit-logback-1.x 的 gRPC 上报 Appender。引入依赖provideddependencygroupIdorg.apache.skywalking/groupIdartifactIdapm-toolkit-logback-1.x/artifactIdversion9.7.0/versionscopeprovided/scope/dependency在 logback-spring.xml 增加 gRPC Appenderappender nameSW_GRPC classorg.apache.skywalking.apm.toolkit.log.logback.v1.x.log.GRPCLogClientAppenderencoder classch.qos.logback.core.encoder.LayoutWrappingEncoderlayout classorg.apache.skywalking.apm.toolkit.log.logback.v1.x.mdc.TraceIdMDCEncoder//encoder/appenderroot levelINFOappender-ref refCONSOLE/appender-ref refSW_GRPC//rootOAP 侧需在 application.yml 开启日志接收receiver-banyandb:selector: ${SW_RECEIVER_BANYANDB:-}logging:selector: ${SW_LOGGING_RECEIVER_ENABLED:default}日志上报走 11800 端口与 Trace 同一 gRPC 通道。上报后每条日志会带上 traceId并在 OAP 内部与 Trace 绑定UI 中即可联动。6. UI 中联动排查从 Trace 跳日志配置就绪后排查流程变成SkyWalking UI → 追踪按 llm.tenant t_1001 过滤找到慢 Trace。点开该 Trace在详情面板右侧可见关联日志标签页列出这次请求产生的全部日志条目。点某条日志能看到完整 userQuery、kbHit、modelOutput无需切换系统。反向也可在日志菜单按关键字搜到异常日志点 traceId 跳回对应 Trace看当时的链路拓扑与耗时。这种双向联动对大模型排障尤其重要因为模型问题的原因往往在输入现象在输出二者分处链路两端只有联动视图才能一次看清。下表对比两种关联方式的取舍方式实现成本联动体验适用场景------------日志带 traceIdELK/Loki低仅改 Pattern需跨系统搜已有成熟日志平台日志上报 OAP中加 AppenderUI 内一键跳转希望统一观测面标签筛选低代码打标按维度聚合多租户/多模型对比7. 最佳实践与踩坑第一Tag 与日志职责分清。再强调一次Tag 负责分桶筛选低基数、可枚举日志负责明细排查高基数、可长文本。把 userId 当 TagOAP 索引会膨胀到不可用。第二异步线程会丢上下文。大模型回调常切到线程池子线程里 TraceContext.traceId() 可能为空。解决用 SkyWalking 的 TraceCrossThread 或 RunnableWrapper/CallableWrapper 包装任务把上下文传递过去否则日志里的 traceId 会断开。第三批量上报注意采样。全量日志上报 OAP 在高峰压力很大。可对 INFO 走本地日志、仅把 WARN/ERROR 上报 OAP或用 log4j2 的异步 Appender 削峰。第四MDC 清理。Logback 的 %tid 基于 MDC线程复用如 Tomcat 线程池时若上下文未清除可能串号。SkyWalking 插件默认会清理但自写跨线程包装时要手动 MDC.clear()。第五敏感信息脱敏。大模型日志常含用户隐私与模型输出上报前务必做脱敏如手机号、身份证掩码避免把 PII 写进 OAP 存储。第六traceId 空值兜底。TraceContext.traceId() 在非链路线程返回空串日志里会显示空打印关联键前先判断避免误导。做好标签 日志联动你手里就同时有了按维度筛查的望远镜Tag和还原现场的显微镜Log大模型线上问题不再是黑盒。