在这里插入图片描述

前置:篇(八)无状态(已验证 MCP Java SDK 2.0.0 的 _meta 注入路径)、篇(九)L6 审计、篇(十三)多租户
配套散点文:《MCP 可观测工具横评(Langfuse / Braintrust / Phoenix)》(待发布)
技术基线:MCP Java SDK 2.0.0;Micrometer + Prometheus + Grafana(指标),W3C traceparent(追踪)
实测环境:本地 Docker(完整可运行栈见 mcp-in-action/14-observability,tag v14:OTel Collector + Jaeger;网关 / 多实例 / 租户隔离 Redis 见篇十三)


引子:工具卡了,但没人知道卡在哪

篇(八)把无状态迁完了,多实例跑得欢。直到有天运营说:“点一下查销售,转了半分钟才出来,有时候还 500。”

我去查——日志是散的:请求跨了三个实例,每个实例只留半句;工具内部调了下游库存服务,慢的是那一跳还是 MCP 本身?不知道。最后靠 grep 时间窗硬拼,花了两小时。

这就是 MCP 上生产的第一个硬门槛:调用链看不见。本文给一套生产可行的方案——用 _meta 注入 W3C traceparent,把一次 MCP 调用的全链路串起来,再接指标和评测。

范围说明:本文解决"怎么埋 trace + 怎么量化 + 怎么评测"。三个工具的"埋完看什么"见配套散点文,本文不重复横评。


一、为什么 MCP 没有标准 trace 传播

MCP 协议设计上没有强制要求在请求间传播 trace 上下文——协议关心"调用什么工具、传什么参数",不规定"这条调用属于哪个分布式 trace"。这是协议取舍(保持轻量),但也留了个生产坑:原生 MCP 调用链是断的。

不自己补,就会回到引子的窘境。下面给补法。


二、可观测性三件套

件回答本文手段
日志发生了什么篇九 L6 结构化日志 + 篇十三 MDC 带 tenant
指标多频繁 / 多慢 / 错多少Micrometer + Prometheus
追踪一次调用经过哪、卡哪_meta 注入 traceparent → Jaeger / SkyWalking(后端可换,见 3.4)

监控 ≠ 评测(重要):监控看"快不快/通不通",评测看"结果对不对"。两者都做,见第六节。


三、独家方案:_meta 注入 W3C traceparent

3.1 为什么用 _meta

MCP 的请求体允许在 _meta 字段带实现自定义的元数据,且会随请求原样透传到对端——这是协议留的"侧信道"。把 W3C traceparent(00-{trace-id}-{parent-id}-{flags})塞进 _meta,就能在不改协议语义的前提下传播 trace 上下文。

关键事实(篇八已验证,MCP Java SDK 2.0.0):客户端有 CallToolRequest.Builder.meta(...),无状态服务端的 handler 签名是 BiFunction<McpTransportContext, CallToolRequest, ...>,直接能拿到 .meta()。也就是说原生路径就够用,不需要 ToolContext(无状态模式下本不适用)。本文复用该路径。

3.2 注入点:Agent → Client → Server → 下游

Agent ──(业务调用)──> MCP Client ──(注入 traceparent 到 _meta)──> MCP Server ──(提取并 new span)──> 下游服务(透传 header)
        trace-id 全程不变,每一跳是一个 child span

Client 侧注入(发起工具调用前):

// 生产接法:OpenTelemetry 的 SpanContext 按 W3C 规范拼成 traceparent,塞进 _meta —— 注入点①
// 注:配套 14-observability 为可零依赖跑通,用自实现 TracingUtil 生成等价 traceparent,逻辑一致
Span span = tracer.spanBuilder("mcp.call").startSpan();
SpanContext sc = span.getSpanContext();
String traceparent = "00-" + sc.getTraceId() + "-" + sc.getSpanId() + "-01"; // W3C traceparent
try (Scope ignored = span.makeCurrent()) {
    CallToolRequest req = CallToolRequest.builder()
            .name("get_store_sales")
            .arguments(args)
            .meta(Map.of("traceparent", traceparent,
                         "tenant", currentTenant()))   // 顺带带租户,呼应篇十三
            .build();
    McpClientResult r = client.callTool(req);
}

Server 侧提取并续接 span(无状态 handler,SDK 2.0.0 已验证路径):

// 从 _meta 取出的 traceparent 解析出 parent SpanContext(W3C 格式,配套 demo 用 TracingUtil.parse)
// 再 setParent 续接同一 trace,不新建 —— 这才是"续接"而非"另起炉灶"
BiFunction<McpTransportContext, CallToolRequest, CallToolResult> handler =
    (ctx, req) -> {
        String tp = (String) req.meta().get("traceparent");
        SpanContext parent = parseTraceparent(tp);
        Span span = tracer.spanBuilder("server.handle")
                .setParent(Context.current().with(Span.wrap(parent)))
                .startSpan();
        try (Scope ignored = span.makeCurrent()) {
            return doTool(req);                          // 工具内部调用自动挂在这条 span 下
        }
    };

下游透传:Server 调下游(库存/ERP)时,把 tp 作为 traceparent header 带出去,下游 OTel 自动接上——全链路打通。

3.3 实现细节

  • trace-id 设计:traceId = 业务会话Id + 实例标识低几位,保证跨实例唯一且可回溯;parentId 每跳重新生成。
  • 上下文传递:异步工具里用 OTel 的 Context 传播(context.wrap() / Scope),优先于裸 ThreadLocal——后者在虚拟线程/线程池切换时易丢 span。
  • 降级:traceparent 缺失时 Server 自建 trace(保证总有链),不阻断业务。

3.4 后端选型:Jaeger 还是 SkyWalking?(核心是 _meta 注入,后端可换)

_meta 注入的 traceparent 是 W3C 标准格式,跟后端无关。本文演示用 OpenTelemetry + Jaeger:OTel SDK 生成 traceparent → OTLP 发到 OTel Collector → 转 Jaeger 存与看(见 mcp-in-action/14-observability/otel-collector-config.yaml,tag v14)。

但后端是可插拔的,两种主流接法都行:

  • Jaeger / OTel(本文演示):云原生标准、厂商中立(CNCF 毕业项目);多语言、上 K8s 的团队默认选择,国际主流。
  • SkyWalking:在中文 Java 企业圈(Spring Cloud / 阿里系 / 传统政企)更普及——Java agent 无侵入自动埋点、对 Spring Boot 开箱即用。而且 SkyWalking 已支持接收 OTLP 数据(需在其侧开启 OTLP 接收端口,gRPC :11800):把 OTel Collector 的 export 端点指到 SkyWalking OAP 即可,核心的 _meta 注入代码一行都不用改。

关键认知:无论选谁,SkyWalking 还是 OTel 的 Java agent 都认不得 MCP 的 _meta 通道——它们只能自动抓到 HTTP/gRPC/DB 那几跳,MCP Client→Server 这一跳的 trace 上下文,必须靠本文手搓的 _meta 注入来传。所以选后端是细枝末节,手搓注入才是"第一公里"、才是本文差异点。给国产 Java 团队的落地建议:用 OTel 注入 + SkyWalking 兜底看板,两者通过 OTLP 直连,既有标准底座又有熟悉的 UI。

✅ 已实测核对(MCP Java SDK 2.0.0,真实 MCP Server + Client):起 14-observability 模块,客户端注入 _meta.traceparent、服务端取出续接、下游透传,跑通全链路。某次实测真实输出:

[client] 注入 _meta.traceparent = 00-941ef914904aa5cc5ece82d7db173fcb-e5a2c8686f2c8dbe-01
[server] 下游调用透传 traceparent header: 00-941ef914904aa5cc5ece82d7db173fcb-8af71717c3de7344-01 | tenant=store-a
[client] 服务端回传: [... receivedTraceparent(from _meta)=00-941ef914904aa5cc5ece82d7db173fcb-e5a2c8686f2c8dbe-01 ...]
# 结构化 span 日志(mcp.spans),三段共享同一 traceId:
{"traceId":"941ef914904aa5cc5ece82d7db173fcb","spanId":"8af71717c3de7344","parentSpanId":"e5a2c8686f2c8dbe","name":"server.handle","tenant":"store-a"}

要点(证明三件事):

  1. 服务端从 _meta 取出的 receivedTraceparent 与客户端注入的逐字符一致 → _meta 在 JSON-RPC 里完整透传;
  2. 子 span 的 parentSpanId 正好是客户端 spanId → 续接而非新建 trace;
  3. 下游 header 用服务端子 span 的 traceparent → 链路继续往下织。

即 MCP Java SDK 2.0.0 支持 _meta 注入且服务端可提取,本方案生产可用(这是规划期"储备验证项#1"的核心命题,已坐实)。Jaeger 里"Client/Server/下游三段 span"是上面结构化日志的等价渲染——docker-compose.yml 已附 OTel Collector + Jaeger,把 ObservabilityTracer 的 spanLog 换成 OTLP exporter 即上看板,业务代码零改动;./verify-tracing.sh 一键复现上述全部输出。


四、关键指标:QPS / 成功率 / P95 / 错误分布

trace 让你"看链路",指标让你"量化健康"。用 Micrometer 埋四类:

// 延迟(P95 重点)
Timer.builder("mcp.tool.latency")
    .tag("tool", name).tag("tenant", tenant)
    .register(registry).record(duration);
// 调用量 + 成功率
Counter.builder("mcp.tool.calls")
    .tag("tool", name).tag("tenant", tenant).tag("result", ok?"ok":"err")
    .register(registry).increment();
  • QPS:rate(mcp_tool_calls);成功率:ok/(ok+err);P95 延迟:histogram_quantile(0.95, ...);错误分布:按 result=err + 错误码 tag 分组。
  • 对接 Prometheus 抓取 + Grafana 看板 + ELK 存日志(篇九 L6)。

与篇十三呼应:指标 tag 带 tenant,多租户下能分租户看健康度——一个网关服务多团队时,谁的接口慢一目了然。


五、告警配置

触发阈值建议动作
P95 延迟突增较基线 +50% 持续 5m告警 + 自动抓最近 trace
成功率跌< 99% 持续 3m告警 + 拉错误分布
某租户配额异常错误率突增联动篇十三配额

六、在线评测:监控之外还要知道"对不对"

监控告诉你"快不快",评测告诉你"结果对不对"——两者正交,缺一不可。

  • 离线评测集:攒一批带标准答案的用例(如"查 A 店上周销售"应返回数值 X),每次工具/ Prompt 改动跑一遍。
  • 在线抽样评测:生产流量抽样,用裁判模型打分,跟踪效果漂移。
  • 指标对齐:把评测分数和上面的监控指标挂同一看板——"这周 P95 没变,但评测分掉了"才是真问题。
  • ♻️ 可复用 rag-lab 的评测经验(评测集设计、裁判模型、抽样统计),两条线在此缝合。

配套散点文把这套评测接到 Langfuse/Braintrust/Phoenix;本文只负责"埋"和"量"。


七、与全季的关系 + 诚实边界

互链(点击跳转对应篇章):

诚实边界:

  • 本方案是协议过渡方案——等 MCP 出标准 trace 传播后,应平滑迁移到官方机制,到时 _meta 注入可退役。
  • 真实 Jaeger/Grafana 截图、P95 基线数字为待实测项:trace 传播已实测核对(见第四节,_meta 端到端透传跑通);图谱/基线需接 docker-compose.yml 的 OTel+Jaeger 栈、跑真实负载后补,属"渲染层"非"方案层"缺口。
  • 评测集与裁判模型为方法论复用,具体用例需按你的业务沉淀。

八、可观测自查清单

项必做?
_meta 注入 traceparent(Client→Server)必做
Server 提取并续接 span(不新建 trace)必做
下游调用透传 traceparent header必做
指标:QPS/成功率/P95/错误分布必做
trace/指标带 tenant(多租户)必做(多团队)
告警:延迟/成功率/租户异常建议
在线评测集 + 抽样评测建议

一句话收尾

MCP 协议不留 trace,但 _meta 留了侧信道——把 W3C traceparent 塞进去,调用链就接上了。trace 看链路、指标看健康、评测看对错,三件齐了,Agent 才真正"生产可用"。至于埋完用什么看,那是配套散点文的事。

Logo

更多推荐