深入 OpenTelemetry 28:Java 可观测性预算怎样测量
“接了 Agent 会慢多少”没有脱离负载、采样和出口的固定答案。同一条 Java HTTP 请求,若把启动 Agent 的耗时、网络响应时间、Span 创建和异步发送混成一个数,甚至把尚未发送的排队 Span 当成后端已经保存,就算拿到漂亮的平均值也无法确定该调哪里。本篇把正确性计数和性能测量分开,公开两轮短负载的原始读数及不能外推的部分。
本次只用已有的完整 JDK 21.0.12、SDK/Agent 1.31.0、JDK HTTP 客户端与本机教学 OTLP/HTTP protobuf 接收端。Agent 发行 JAR 的 SHA-256 与 SDK SHA c25c0a0ee0da01ab2f74ba83052d1c249ed57020、instrumentation SHA 97d87f3f3b2bc61aa396d71a98387a1e857618af 都在 writing-plans/opentelemetry-java/VERSIONS.md;本文未引入 Collector 或第二套全局 SDK。
热路径在哪,后台线程又在哪
固定 SDK SHA 中,SdkSpanBuilder.java L169–225 在请求线程处理父 Context、Trace ID、采样判断、recording 决策及 Span 创建;SdkSpan.java L439–451 结束 Span 时调用 processor。BatchSpanProcessor 的 onEnd L98–103 只对 sampled Span 入队;Worker.addSpan L219–230 的 queue.offer 失败会计入 dropped,但不让出口 HTTP 发送变成请求线程上的同步成本。队列和出口仍能消耗同一 JVM 的 CPU/内存,进程关停也有自己的等待和失败边界。
SpanLimitsBuilder.java L13–25 定义默认属性、事件与 Link 数量限制;SdkTracerProviderBuilder.java L129–152 明确选择 sampler 和 processor。不能把“默认有限制”当成“属性成本为零”或“采样为 0 时 Agent 无成本”。Agent 在固定 instrumentation SHA 的 OpenTelemetryAgent.java L42–57 由 JVM premain 进入;HttpUrlConnectionInstrumentation.java L30–50 匹配目标类和方法。这两处并不能单独推出 Agent 的 CPU 开销,只给出成本可能出现的位置。上游 BatchSpanProcessorTest、SdkSpanBuilderTest 与 HttpUrlConnectionTest 已在固定 SHA 下静态阅读,本机并未运行上游测试。
固定同负载,分开报告指标口径
Lab28Test 同一个 localhost JDK HTTP 端点、同一个单线程 HttpURLConnection GET:每个独立 JVM 预热 60 次,再测 120 次;每轮依次 none(无 SDK/Agent)、manual(请求外包一个 CLIENT Span)、agent(仅挂 1.31.0 Java Agent),共两轮。采样固定 alwaysOn;manual/agent 的 trace exporter 均是 OTLP/HTTP protobuf、本机同一接收端,Batch 队列 2048、批量上限 512、调度 50 ms;Agent Metrics/Logs exporter 关掉。manual 自己构建 provider,agent 不初始化第二套 SDK;none 不发 Span。接收端解析 protobuf,逐进程等退出并断言预热+测量的 180 个 Span(none 为 0),此接收口径下每组 dropped=0。不把教学端点描述成 Collector 或存储后端。
manual 模式的 Span 紧贴 HTTP 请求;none 和 agent 模式不执行显式建 Span 代码,三个模式发送相同的业务 HTTP 请求。实验中 request 方法的边界简化如下:
1 | |
为突出观察边界,这里省略真实方法中的超时、200 状态校验和 disconnect 的 finally;运行和复验以 Lab28Probe.java 为准。
| 轮次/模式 | 吞吐 (req/s) | p95 / p99 (ms) | 进程 CPU (ms) | 结束时已用堆 (MiB) | 主线程分配 (MiB) | VmHWM (MiB) | 接收/预期 |
|---|---|---|---|---|---|---|---|
| 1 none | 512.1 | 3.99 / 6.93 | 250 | 2.51 | 6.67 | 67.9 | 0/0 |
| 1 manual | 430.5 | 5.09 / 8.59 | 470 | 8.05 | 6.73 | 96.4 | 180/180 |
| 1 agent | 359.5 | 7.11 / 16.15 | 600 | 42.67 | 7.19 | 215.3 | 180/180 |
| 2 none | 758.0 | 2.69 / 7.93 | 210 | 2.51 | 6.67 | 67.7 | 0/0 |
| 2 manual | 756.9 | 2.73 / 8.22 | 280 | 6.90 | 6.73 | 95.1 | 180/180 |
| 2 agent | 558.8 | 3.49 / 13.26 | 540 | 43.87 | 7.19 | 214.0 | 180/180 |
命令、机器规格、准确纳秒/字节计数、退出码及六条未经换算的输出在 examples/opentelemetry-java/evidence/28/RUN.md。p95/p99 是 120 次单请求墙钟延迟的最近秩;吞吐由 120/测量窗秒数得到。CPU 是全 JVM 在测量窗的进程 CPU 时间(含其他线程,可能大于墙钟),堆是结束时快照,VmHWM 是进程生命周期最高 RSS,主线程分配不包含 exporter/Agent 线程。没有每次请求的真实全进程分配,也没有在预热后重置 VmHWM;两轮顺序固定、时间短且可能受 JIT、缓存和调度影响,不应把任一行当作生产税率或 Agent 相对 manual 的因果效应。manual 人工包一个 CLIENT Span 与 Agent 的 JDK 方法织入也不是同一实现;同负载/采样/出口不等于完全隔离两者开销。
预算不是单一开关
先确定的是需要多少个可接收 Span、可承受多少丢弃,再选择采样率、属性限制、Batch 队列大小与出口时限;同时约束请求 p99、JVM CPU、内存高水位和主线程分配。接收 180/180 只说明本地教学端点这一跳在这两轮没发现缺口,不能证明后端可查、峰值流量不丢或 GC 压力可接受。把 CPU 与请求延迟差别解释为“肯定由 exporter 同步阻塞请求”也和 Batch 的后台出口路径不符。另一常见误解是把 VmHWM 的 Agent 启动/预热开销直接除以 120 当每请求字节数;它是峰值常驻集,不是分配率。
两道练习:
- 修改
Lab28Test/Lab28Probe:把请求并发提高到 4,保持总量和接收端配置,逐次重复并给每组报告吞吐、p99 和收数;再将队列压到 1 并让端点通过闩锁阻塞导出,构造受控丢弃,区分 SDKqueue.offer失败与 HTTP 响应失败。不能用任意 sleep 当同步屏障。 - 让同样的三组在轮次之间交错不同顺序,每组至少跑多轮;单独采集 Agent 启动期和测量窗的内存/线程分配,并标记不可能由本测试识别的服务端网络噪声。给出你会用哪项置信区间而不是直接用两轮最大差值设置生产告警阈值。
参考资料:Java SDK BatchSpanProcessor(固定 SHA)、Java Agent HTTP URLConnection 插桩(固定 SHA)、examples/opentelemetry-java/evidence/28/RUN.md。
导航:27 头采与尾采边界 · 当前篇:28 可观测性成本测量。
