AI 工程实践

OTLP Partial Success 实验:Collector 显示 Sent 4,Jaeger 为什么只有 2

让后端只提交 4 个 spans 中的 2 个,并返回合法 HTTP 200 + rejected_spans=2;实际核对 Collector 不重试、sent counter 仍为 4、warn 日志记录 dropped 2,而 Jaeger 查询永久缺两条。

发布:2026/07/15更新:2026/07/15
四个 spans 进入 Collector,部分提交代理只转发两个并拒绝两个,返回 HTTP 200 partial success;Collector 不重试且显示 sent 四,日志 dropped 两,Jaeger 最终查询两条
图 1:2026-07-15 OTLP partial-success 实验。HTTP 200 与 Collector sent counter 表示协议请求完成,不证明每条 telemetry 都进入最终后端;本轮唯一直接丢弃信号来自 warn 日志。

时间与证据

上一篇 OpenTelemetry 提交后响应丢失实验验证了整批提交歧义:同一个 3-span payload 到达 Jaeger 两次,Jaeger receiver 计 6,最终 Badger query 仍为 3 unique。它说明传输重试可能重复,但稳定身份可以让固定后端的最终对象在本轮收敛。

另一种更隐蔽的情况是 partial success:后端接受一部分数据、明确拒绝其余数据,却仍按 OTLP 合同返回 HTTP 200。此时客户端不应重试整个请求,因为重试会把已经接受的部分再发一次;代价是被拒绝的数据必须被记录、告警或在更上层补偿。

2026 年 7 月 15 日,我新增 labs/otel-partial-success/。应用只向 Collector 发送一次包含 4 个 spans 的 trace;Collector persistent queue 再以未压缩 OTLP JSON 发给故障代理。代理只把 root 与第一个 child 转发给 Jaeger,另外两个不转发,然后返回:

{
  "partialSuccess": {
    "rejectedSpans": "2",
    "errorMessage": "lab rejected two spans after partial commit"
  }
}

HTTP status 是 200。runner 继续观察 2500 ms,记录代理 attempt、Collector metrics 与日志、Jaeger metrics 和最终 query。

固定结果位于 labs/otel-partial-success/results/2026-07-15-results.json,文件 SHA-256 为 ef3981e27f13ac26ec33dc4bbff5c498bffa4d0c7b23f0e7e9a6e43a8616d5c3,加入自引用字段前的 payload SHA-256 为 23c428a64b9313641ee1eb1d824b0a328303b511f55959d21275dd7d3219bf06。10/10 工件断言覆盖镜像与输入身份、单次应用请求、两条提交与两条拒绝、OTLP response、no-retry observation、Collector counter/log、Jaeger 缺失 identity、部分树和工件隐私。

证据等级保持 reproduced。真实运行的是 Collector v0.156.0 OTLP HTTP exporter、persistent queue、partial-success parser、Jaeger v2.19.0 Badger 与 query API;部分拒绝由实验代理控制,不是 Jaeger 自身的生产过滤策略。

上游协议到底要求什么

固定到 OpenTelemetry Proto v1.10.0 commit 的 OTLP specification对 HTTP partial success 有三条关键约束:

  1. 部分接受时 server 必须返回 HTTP 200 OK
  2. response 必须填 partial_success,并用 rejected_<signal> 给出拒绝数量。
  3. client 收到 populated partial success 后 MUST NOT retry 同一请求。

Trace service protobuf还说明 error_message 面向开发者,用于解释为何拒绝部分数据或给出修复建议。

因此“不重试”不是 Collector 漏配 retry。它是协议为了避免重复已接受部分而规定的行为。真正的工程问题是:被拒绝数量是否进入可告警、可归因、可补偿的证据面。

实验设计

四个稳定 Span 分成两组

固定 trace 为 f200...0001

span 角色 代理动作
9100...0001 root 转发到 Jaeger
9200...0001 accepted child 转发到 Jaeger
9300...0001 rejected child 1 不转发,计入 rejected
9400...0001 rejected child 2 不转发,计入 rejected

代理不是按随机比例丢弃,而是保存 input、forwarded、rejected 三组稳定 IDs。最终 query 不只检查 count=2,还必须等于前两个 IDs;missing 集合必须精确等于后两个 IDs。

为什么改用未压缩 JSON

上一实验让代理把 protobuf 当 opaque bytes,只比较 SHA。本轮需要构造规范的部分提交,因此 Collector exporter 固定:

encoding: json
compression: none

代理解析 OTLP JSON,保留完整 resource 与 scope,只从 spans 取前两条后转发。原请求为 2059 bytes,SHA-256 084257640ecf85799e491e0327953187f0191088dae8d3cf7d4a6ae719136c3c;工件不保存 raw body。

这项配置只服务于故障注入。它没有比较 JSON 与 protobuf 的带宽、CPU 或吞吐,也不建议生产为了可观测代理而改用 JSON。

四层验收顺序

runner 每次从空 volumes 开始:

  1. 启动 host partial proxy、Jaeger Badger 与 Collector。
  2. 应用发送一次 4-span request,收到 Collector HTTP 200。
  3. 代理向 Jaeger 转发前两个,Jaeger 返回 200;代理向 Collector 返回 partial success。
  4. Jaeger query 必须出现 root 与 accepted child。
  5. 继续观察 2500 ms,代理 attempts 必须保持 1。
  6. Collector queue 必须为 0;读取 metrics 与 partial-success warn。
  7. Jaeger receiver/storage metrics 必须为 2;最终 missing IDs 必须是 93 与 94。
  8. finally 停止 proxy,删除容器、network 与 volumes。

2500 ms 不是“永远不会重试”的时间证明。更强依据来自协议 MUST NOT retry、Collector 已完成 queue item,以及本轮实际 attempt=1。本文把 2500 ms 只写成观察窗口。

结果

应用看见的是入口成功

应用只发送一次,automaticRetries=0,收到:

HTTP 200
{"partialSuccess":{}}

这是 Collector receiver 接受并入队的 response,不是下游代理稍后返回的 partial success。persistent queue 解耦了入口与出口,因此应用不会同步看到 rejectedSpans=2

如果应用把入口 200 记录成“Jaeger 已完整存储”,状态名称就错了。它最多能写 collector_accepted,不能写 trace_persistedaudit_complete

Collector 不重试,但 sent 仍为 4

观察窗口结束后只有一次 proxy attempt。Collector metrics:

accepted 4 · refused 0
sent 4 · failed 0
queue 0/10 · enqueue failed 0

Collector metrics 的单行摘要是 accepted 4 · sent 4,同时 failed=0

从 metrics 看,四条全部 sent、零失败、零积压。它没有从 sent_spans 中扣除后端报告的两条 rejected spans。

但 Collector 同时产生 warn:

Partial success response
message: "lab rejected two spans after partial commit"
dropped_spans: 2

本轮最关键的跨面差异就是:成功 counter 是 4,丢弃日志是 2,最终后端对象是 2。 只抓 Prometheus metrics 而不采 Collector logs,会把数据完整性缺口显示成全绿。

Jaeger 只有两条,另外两个 ID 不会自动恢复

Jaeger metrics:

receiver accepted 2
storage exporter sent 2

最终 query 返回:

集合 span IDs
present 9100...0001, 9200...0001
missing 9300...0001, 9400...0001

部分 trace 保持一根一子;两个 rejected children 不会因为 queue 排空或后端继续健康而出现。Collector 已按协议完成该 request,没有可供后续 drain 的 queue item。

这不是“静默丢失”:warn 明确报告 dropped 2。它也不是对应用可见的同步失败。信号位于 Collector 出口日志,必须进入告警和运营流程才能产生实际作用。

如何为 Partial Success 建立完整性 SLO

仅用 accepted - failed 估算存储量不够。至少需要:

ingress accepted
exporter logical sent
partial-success rejected/dropped
downstream receiver accepted
backend queryable

生产控制面可以定义:

信号 初始策略
partial-success count > 0 立即告警并按 exporter/tenant/signal 归因
dropped items > 0 保留 error message、版本和低基数原因
sent 与 downstream accepted 长期偏离 检查 partial success、采样、过滤和抓取缺口
audit trace 缺少必需 span type 触发 reconciliation,不用普通 trace count 掩盖
同类 bad data 持续出现 修复 producer/schema,不盲目重试同一 payload

OTLP 规定 client 不重试原请求,不等于系统永远不能补偿。更上层可以根据稳定业务 key 重新生成修复后的 telemetry 或审计事件,但必须避免重发已经接受的部分,并与原始 partial-success event 关联。

对高价值 Agent,可把必须存在的 span/业务事件定义成完整性合同,例如:

tool.request
tool.authorization
tool.result
business.reconciliation

若 trace 只有 request 与 authorization,缺 result 和 reconciliation,系统应标记 incomplete,而不是因为 trace 可查询就算成功。

复现与验证

要求 Docker Engine 与 Compose 可用,并允许容器访问 host.docker.internal。本机端口 28318281332987828418295183068630788 未占用:

npm ci
npm run lab:otel-partial:run
npm run lab:otel-partial:test

默认 replay 输出到 /tmp/younis-ai-lab-otel-partial-success.json。固定工件保存 compose、两份配置、partial proxy、sender 与 runner 六个输入 SHA-256。raw request、Collector instance ID、container ID、mountpoint 与 host gateway 不进入报告。

runner 在成功或失败时都会停止 proxy 并删除实验容器、network 与 volumes。诊断期曾用 --keep-stack 读取 Collector 实际日志字段,确认 v0.156.0 使用 dropped_spans;诊断 stack 随后删除,最终冻结结果重新从空 volumes 运行。

失败与边界

第一,部分拒绝由实验代理制造。Jaeger 本身没有拒绝后两个 spans,本文验证的是 Collector 对合法 OTLP response 的行为。

第二,只有一批四条数据。没有并发、跨 resource、跨 tenant、多个 signal 或大 batch。

第三,观察窗口只有 2500 ms。no-retry 结论同时依赖 OTLP MUST NOT retry、queue item 已完成和 attempt=1,不把窗口时长当完整证明。

第四,sent counter 行为固定到 Collector v0.156.0。未来版本可能增加 partial-success metrics 或改变计数语义,升级时必须重放。

第五,只检查 warn 日志。没有把日志接入真实 Loki、SIEM、PagerDuty 或 on-call 流程,所以不声称告警已 field-tested。

第六,没有坏数据分类。代理 error message 是固定实验文本,没有 schema、size、rate limit、PII 或租户配额原因。

第七,没有补偿写入。missing IDs 被精确识别,但未实现只重建被拒绝 spans 的上层 reconciliation。

第八,JSON 无压缩不是性能配置。没有与 protobuf/gzip 比较成本。

第九,单 Collector、单 consumer、单 host。没有多副本、乱序或队列恢复组合。

第十,没有真实 Agent 业务。四条 synthetic spans 不能证明生产审计完整性。

商业价值

partial success 最容易破坏“看起来健康”的系统。HTTP 200、queue=0、failed=0、sent=accepted 会让普通可观测面板全绿,但实际审计链可能缺一半。对于允许 Agent 修改代码、工单、审批或客户数据的团队,缺失的恰好可能是授权或结果 span。

可销售的能力不是“支持 OTLP”,而是 telemetry completeness control:

partial-success ingestion
  -> dropped count 与 reason 归一化
  -> tenant / signal / producer 归因
  -> 必需事件完整性检查
  -> reconciliation 或人工处理
  -> 升级回归与趋势报告

成本包括日志采集、低基数原因模型、完整性规则、补偿存储、告警 owner 和故障演练。收益是避免用不完整 trace 支撑事故复盘、合规审计或 Agent 成功率计算。

从全栈工程迁移到 AI 系统工程

全栈基础 本实验对应能力
HTTP 200 full success 与 200 partial success 的结构化差异
消息队列 item 完成、no retry 与上层补偿边界
API 合同 rejected count、error message 与版本固定
可观测性 metrics、logs、downstream receiver、query 四面核对
数据质量 present/missing 稳定 identity 集合
SRE dropped 告警、完整性 SLO 与 owner
Agent 审计 必需 span/业务事件合同与 incomplete 状态
回归测试 规范 response、attempt count、counter/log/query 断言

Agent 系统增加了新的信号类型,却没有改变数据管线的基本事实:成功响应可以包含拒绝明细,聚合 counter 可能掩盖缺失对象,最终完整性必须在权威查询面验证。

面试表达

**30 秒版本:**我构造了一个合法 OTLP partial-success response。应用只发送一次 4-span trace,代理只把 root 与一个 child 写入 Jaeger,再返回 HTTP 200、rejected_spans=2。Collector 按规范不重试,2500 ms 内 attempt 仍为 1;但 metrics 显示 accepted 4、sent 4、failed 0,只有 warn 日志记录 dropped_spans=2。Jaeger 最终正好 2 present、2 missing,说明 sent counter 不能单独证明 telemetry 完整交付。

**3 分钟版本:**实验固定 OTLP Proto v1.10.0、Collector v0.156.0 与 Jaeger v2.19.0。为了精确拆 batch,Collector exporter 使用未压缩 JSON;代理保存四个 input IDs,只转发前两个,并返回规范 partialSuccess。协议要求 client MUST NOT retry,因此 queue 正常完成。跨面结果是 Collector 4/4、warn dropped 2、Jaeger 2/2、query 缺 93/94。生产应把 partial-success 日志转成可告警事件,按 producer 与原因归因,并为高价值 Agent 定义必需审计事件与 reconciliation。

复盘

本轮最重要的发现是 HTTP 200 仍需要解析 response body。若代理返回 full success,四条 sent 与两条 query 的差异只能被视为故障;partial success 则把拒绝数量变成协议内的显式结果,并要求 client 不重试。

第二个发现是 Collector metrics 与日志表达不同层次。metrics 把逻辑 request 计为 sent 4,日志明确写 dropped 2。丢弃告警若只基于 send_failed_spans,本轮会完全漏报。

第三个发现是最终 trace “存在”不等于完整。root 与一个 child 可查,页面看起来有 trace,但另外两个稳定 IDs 永久缺失。下一阶段应把必需 span 类型、业务终态和 reconciliation 水位加入完整性检查,而不是只统计 trace 数。

方法披露

本文由 AI 工具协助设计 partial-commit 代理、审查脚本、组织文字与绘制原创 SVG。OTLP 固定规范、镜像 digest、输入 IDs、HTTP response、attempt count、Collector metrics/log、Jaeger metrics/query、工件哈希和 Docker 清理均由本站对照实际运行逐项核验。

实验没有调用模型、向量数据库、外部 Agent、客户系统或生产后端。四条 deterministic spans 不含 prompt、用户信息、凭据或真实 transaction。代理不保存 raw OTLP body,固定工件只保留 SHA、长度、结构化 IDs、counter 与 canonical query。

上游 commit 用于解释 partial-success 与 no-retry 合同;Collector sent 4、warn dropped 2、Jaeger 2、missing 93/94 只依据本仓库实际 capture。桌面 SVG、独立移动 SVG 与 PNG 不伪造生产丢弃率或告警能力。

修订记录

  • 2026-07-15:初版发布;完成 4-span 部分提交、OTLP HTTP 200 partial success、no-retry observation、Collector counter/log 与 Jaeger identity 终态核对。

Reusable projects

关联可复用项目

本文已经进入以下工程项目;项目页提供固定版本、运行命令和结果工件。

Source ledger

来源账本

以下来源用于核对事实、日期与当时可用范围。厂商自报性能不视为本站独立复现。

  1. OTLP v1.10.0 protocol specification: HTTP partial success and no-retry contract
    OpenTelemetry官方文档文章资料来源发布:2026/03/09本站核验:2026/07/15
  2. OTLP v1.10.0 trace service partial-success protobuf definition
    OpenTelemetry官方仓库文章资料来源发布:2026/03/09本站核验:2026/07/15
  3. Collector OTLP HTTP exporter partial-success handling at v0.156.0 commit
    OpenTelemetry官方仓库文章资料来源发布:2026/07/06本站核验:2026/07/15
  4. OpenTelemetry Collector exporter helper retry and persistent queue documentation at v0.156.0 commit
    OpenTelemetry官方文档文章资料来源发布:2026/07/06本站核验:2026/07/15
  5. OpenTelemetry Collector internal telemetry documentation at v0.156.0 commit
    OpenTelemetry官方文档文章资料来源发布:2026/07/06本站核验:2026/07/15
  6. Jaeger Badger storage data model at v2.19.0 commit
    Jaeger官方文档文章资料来源发布:2026/06/03本站核验:2026/07/15
  7. Jaeger v2.19.0 query extension documentation
    Jaeger官方文档文章资料来源发布:2026/06/03本站核验:2026/07/15

讨论

正在加载评论...