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 查询永久缺两条。
时间与证据
上一篇 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 有三条关键约束:
- 部分接受时 server 必须返回
HTTP 200 OK。 - response 必须填
partial_success,并用rejected_<signal>给出拒绝数量。 - 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 开始:
- 启动 host partial proxy、Jaeger Badger 与 Collector。
- 应用发送一次 4-span request,收到 Collector HTTP 200。
- 代理向 Jaeger 转发前两个,Jaeger 返回 200;代理向 Collector 返回 partial success。
- Jaeger query 必须出现 root 与 accepted child。
- 继续观察 2500 ms,代理 attempts 必须保持 1。
- Collector queue 必须为 0;读取 metrics 与 partial-success warn。
- Jaeger receiver/storage metrics 必须为 2;最终 missing IDs 必须是 93 与 94。
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_persisted 或 audit_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。本机端口 28318、28133、29878、28418、29518、30686、30788 未占用:
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
关联可复用项目
本文已经进入以下工程项目;项目页提供固定版本、运行命令和结果工件。
- 已独立复现
本文验证 HTTP 200 partial success 下 Collector sent=4 仍可能只有 2 个 span 到达 Jaeger,缺失必须由响应与终态共同识别。
Source ledger
来源账本
以下来源用于核对事实、日期与当时可用范围。厂商自报性能不视为本站独立复现。
- OTLP v1.10.0 protocol specification: HTTP partial success and no-retry contract
- OTLP v1.10.0 trace service partial-success protobuf definition
- Collector OTLP HTTP exporter partial-success handling at v0.156.0 commit
- OpenTelemetry Collector exporter helper retry and persistent queue documentation at v0.156.0 commit
- OpenTelemetry Collector internal telemetry documentation at v0.156.0 commit
- Jaeger Badger storage data model at v2.19.0 commit
- Jaeger v2.19.0 query extension documentation
讨论