请求结束后才知道它失败了或耗时过长;如果 Java 在创建 Span 时已经决定不记录,放在 Collector 的尾部采样器还能把它选回来吗?不能。这里有两个不同的决策位置:源端决定是否产生可导出的 Span,接收端只能从实际收到的 Span 中选择 Trace。

本篇固定 Java SDK 源码 SHA c25c0a0ee0da01ab2f74ba83052d1c249ed57020(SDK 1.31.0),完整 JDK 21.0.12。Collector Contrib 0.101.0 与 Jaeger all-in-one 1.57.0 是各自独立的 Go 进程,不属于 Java SDK;具体发行包、二进制 SHA-256 和下载信任限制记录在 writing-plans/opentelemetry-java/VERSIONS.md。实验是 SDK 直调 OTLP/HTTP exporter → Collector OTLP receiver/tail_sampling → OTLP/gRPC → Jaeger 内存后端,并非 Agent 自动插桩链路。

决策先于结果

SdkSpanBuilder.java L169–208 在 startSpan() 选择父 Context/Trace ID,调用 Sampler.shouldSample,按决策设置 sampled flag;若非 recording,返回只携带 SpanContext 的 Span。后续 setStatus(ERROR) 无法把这个 Span 转回已经启动的 SDK recording Span。SdkSpan.java L380–393 只处理已创建的 SDK Span 的状态;SdkSpan.java L439–458 在结束时通知 processor,不能用于证明被 DROP 的 Span 也触发 onEnd。

ParentBasedSampler.java L51–75 对无效父 Context 走 root sampler,对有效父 Context 按远端/本地及 sampled flag 分支;未另外配置时,非 sampled 的本地父节点默认走 alwaysOff。TraceIdRatioBasedSampler.java L36–51 为 0 设定不会命中的阈值;其 shouldSample L61–79 是在 Span 启动时判断,不读取结束时长或之后的异常。上游 ParentBasedSamplerTest.java、TraceIdRatioBasedSamplerTest.java 和 SdkSpanBuilderTest.java(同一 SHA,分别位于 sdk/trace/src/test/java/io/opentelemetry/sdk/trace/ 下的 samplers/ 和根目录)提供父分支、概率边界及 DROP/RECORD_ONLY/RECORD_AND_SAMPLE 对照;本次是静态阅读,没有运行上游测试。

尾采在接收端等待一个决策窗口再按所收到的 Span 评估:本次 Collector 配置 decision_wait: 1s、num_traces: 100,两条策略分别是 status_code: ERROR 和 latency: threshold_ms: 100。它们的逻辑是“已收到的错误或慢 Trace 保留”,不是“替源端生成缺失的错误 Span”。窗口结束之后才到达的数据、内存容量压力及进程重启另有边界,本实验没有给出这些条件下的完整性保证。

四个固定 Trace ID 的对照

Lab27Test 用固定 128-bit ID (…0001 到 …0004)、同步 exporter 和模拟开始/结束时间。写入 SDK 内存 exporter 后先断言状态及纳秒时长,再让 ID 2–4 逐个经 Collector 出口传输;ID 1 刻意用 parentBased(traceIdRatioBased(0)) 拒绝。延迟构造 ID 1 的错误子 Span 并显式指定未 sampled 的本地父 Context。不是用随机休眠“等待慢请求”:慢度由 Span 时间戳确定,查询和指标收敛用有期限轮询。

Trace ID 末位 Java 侧 Collector/Jaeger 实测 能证明什么
1 root DROP;稍后子 Span 标 ERROR,但两者均不 recording 内存出口 0,Jaeger 查询不命中 已拒绝的源端数据不因尾采复原
2 2 ms、UNSET,源端已发送 receiver 接收;查询不命中 正常 Trace 可以被本次尾采策略排除
3 250 ms、UNSET,源端已发送 Jaeger 查询到 slow 时长策略保留已接收的慢 Span
4 2 ms、ERROR,源端已发送 Jaeger 查询到 failure 与 ERROR 状态策略保留已接收的错误 Span

执行 JAVA_HOME=/tmp/otel-20260930/jdk-extract/usr/lib/jvm/java-21-openjdk-amd64 ./mvnw -pl sdk-labs -Dtest=Lab27Test test(在 examples/opentelemetry-java/ 目录),退出码 0;原始命令、查询 JSON 摘要、Collector 原始指标和判定见 examples/opentelemetry-java/evidence/27/RUN.md。原始指标显示 receiver accepted spans 为 3、exporter sent spans 为 2;Jaeger 的 ID 3/4 JSON 分别带 duration:250000 / 2000(单位微秒),错误 ID 带 otel.status_code:ERROR。Jaeger 对测试用 ID 的高位零做了显示规范化,查询路径仍使用完整 32 字符 ID。这里的两个“未命中”是已确认的本次本地查询结果,不能从单次查询推出任意生产环境的永久丢失率。

不要把两种采样当成互补备份

“把尾采打开,就能补救已经丢弃的错误请求”忽略了源端入口:Collector 根本收不到 ID 1;“头部拒绝后晚到的错误 child 会重新开启记录”忽略了有效的非 sampled 父 Context。另一方面,不能把 TraceIdRatioBasedSampler 单独当成全局父采样策略;本次明确套用 parentBased。Java 的 sampler、processor、exporter 分别发生在创建、结束、发送阶段,Collector 的策略在另一进程里。此次未测 Agent→Collector、Metric/Log OTLP、生产后端持久化或决策窗口之后补报的 Span。

两道练习:

  1. 修改 Lab27Test,先把 root 比例改成 1,随后让一个 child 在 decision_wait 之后才结束;分别记录 root/child 在 Jaeger 的命中集合,不把设置的延迟直接当作接收时间。检查 Collector 的迟到数据策略并补充实际断言后,才能回答是否完整。
  2. 将 head sampler 换为 Sampler.traceIdRatioBased(0),保留未 sampled 的父 Span,但自定义 child 的采样规则为 alwaysOn;用内存出口和 Collector 查询构造父缺失、子存在的反例,并说明该对照不能代表当前 parentBased 的默认行为。

参考资料:Java SDK Sampler 源码(固定 SHA)、Java SDK sampling 上游测试(固定 SHA)、Collector Contrib v0.101.0、examples/opentelemetry-java/evidence/27/RUN.md。

导航:26 Collector 接收与转发 · 当前篇:27 头采与尾采边界。