AI 工程实践

OpenTelemetry 运行期 EIO 实验:health 还是 200,为什么已经拒绝数据

把已打开的 ext4 persistent queue 从 device-mapper linear 在线切到 error target:核对 block EIO、Collector EROFS 503、health 200、queue gauge 多计,以及恢复 linear、fsck 和稳定 ID 对账。

发布:2026/07/16更新:2026/07/16
OpenTelemetry Collector 在 device-mapper linear target 下接受三批;在线切换 error target 后底层读取返回 EIO、health 仍为 200、第四批收到 EROFS 503;恢复 linear 和 clean fsck 后加载三批,只重放第四批,Jaeger 最终四个唯一 ID
图 1:2026-07-16 运行期 EIO 实验。底层 dm-error、ext4/Collector EROFS 与 health 200 同时成立;恢复前先核对数据库,恢复后再分开验证三批已接受数据和一批被拒绝补偿。

时间与证据

上一篇 只读队列启动实验从一开始就把同一 volume 挂为 ro。replacement Collector 在 health 与 OTLP 端口就绪前 exit 1,数据库哈希不变;恢复 rw 后才能加载三批旧数据。

它没有回答另一个更危险的问题:Collector 已经健康、bbolt 已经打开、上游正在发送时,底层块设备突然开始返回 I/O error 会发生什么?

本轮把 64 MiB file-backed loop device 通过 device-mapper 暴露为 ext4,Collector persistent queue 正常打开后,runner 将活动 table 从:

linear -> error -> linear

镜像、Collector 配置、bbolt 文件、queue capacity、单 consumer、fsync=true、四个稳定 span 和 Jaeger 后端不变。error target 只作用于专用测试设备,清理流程会删除 mapper、loop、backing volume 和所有实验资源。

固定结果位于 labs/otel-runtime-eio/results/2026-07-16-results.json

artifact bytes        12,053
artifact SHA-256      cbb2006ee1481ac7ce0437b804a19c9043fdfbd0463fdc75517839bd964b8a6b
payload SHA-256       59e030992bfe9f44612981567e4fb7cd1f1564c61e14dfeeed4d2d7949523100
artifact assertions   12 / 12

运行环境固定记录 Docker Desktop LinuxKit 6.12.76-linuxkit、device-mapper library 1.02.185mke2fs/e2fsck 1.47.0、Collector Contrib v0.156.0 与 Jaeger v2.19.0。五个实验输入文件都保存 bytes 与 SHA-256。

证据等级保持 reproduced。本轮真实触发 Linux device-mapper error target 和 ext4 读写失败;它不是物理 SSD 损坏、潜在扇区错误、NFS 中断、内核崩溃或生产 Agent 流量。

实验设计

为什么用 device-mapper,而不是改权限

运行中的 Collector 已经打开 bbolt 文件。此时执行 chmod 通常不会撤销已有 file descriptor 的写能力;把后续错误描述成“磁盘 I/O 失败”并不成立。

Linux dm-error 文档说明 error target 会让其映射范围内的 I/O 失败。runner 先创建 131,072 sectors 的 linear mapping:

0 131072 linear /dev/loopN 0

在三批队列完成 fsync 后 suspend device、加载:

0 131072 error

再 resume 同一个 mapper。Collector 容器、ext4 mount 和 bbolt 进程均不替换,因此第 4 批发生在同一个已经健康运行的进程中。

四个稳定 ID 与三层观察面

trace ID 固定为 e600...0001,span IDs 从 e600...001e600...004。sender 没有自动重试。

batch mapper target 入口用途
1 linear 已接受基线
2 linear 已接受基线
3 linear 已接受基线
4 error 运行期故障请求

每个阶段同时观察:

  1. 块设备层:dm table 是 linear 还是 error,独立读取是否返回 Input/output error
  2. Collector 层:health、OTLP HTTP、logs、accepted/refused、enqueue failed 与 queue gauge。
  3. 最终数据层:replacement loaded items、Jaeger stable IDs、补偿前 missing set 与最终 duplicate/missing。

如果只保存 HTTP body,就会把 EROFS 当成故障根因;如果只保存 dm table,又无法证明 Collector 实际拒绝了哪一个业务身份。

恢复顺序

故障后不直接把 mapper 切回 linear 并继续使用原进程。固定流程是:

  1. 对故障 Collector 发送 SIGKILL,避免继续接受流量。
  2. mapper 恢复 linear
  3. 删除持有 queue mount 的容器,确保文件系统可离线检查。
  4. 执行 e2fsck -fy,只有 exit 0 或已修复 exit 1 才能继续。
  5. 重新计算 bbolt bytes 与 SHA-256。
  6. 启动 replacement Collector,让旧队列先独立排空。
  7. Jaeger 确认第 4 个 ID 仍 missing 后,只重放 batch 4。

e2fsck 是恢复门禁,不是“自动证明没有损坏”。文件系统 clean、数据库 bytes/hash 不变、replacement 能加载三项,以及 Jaeger 能查询三项,四个证据缺一不可。

结果

Linear 基线:三个 200 与三项队列闭合

Jaeger 停止后,前三批均返回 HTTP 200:

{ "partialSuccess": {} }

故障前 Collector 为:

accepted              3
refused               0
sent                  0
enqueue failed        0
queue size / capacity 3 / 20
mapper target         linear

bbolt 文件为 65,536 bytes、mode 0600、UID/GID 10001:10001,SHA-256 为:

fa557eba5d2e48ac4e3aa960e3b6c38e52e06a55c6aa493607a65592dd8dd8bd

三项已经被 Collector 接受并 fsync 到 queue,但尚未到 Jaeger。

Error target:底层 EIO,Collector 对外是 EROFS

table 切换后,dmsetup table 报告:

sectors 131072
target  error

独立 helper 尝试读取 queue file,exit code 为 1,并明确得到:

Input/output error

但 batch 4 从 Collector 收到的是 HTTP 503:

{
  "code": 14,
  "message": "read-only file system"
}

Collector log 同样记录 read-only file system,而不是原始 input/output error。这不是证据冲突:device-mapper 的块 I/O 失败触发了 ext4/文件层只读状态,bbolt 和 Collector 看到的是上层错误。生产错误分类不能只按最终字符串聚合;同一个事故需要同时保存 block、filesystem 和 application layer。

Health 仍为 200

最危险的观察值是:

health reachable true
health status    200

同一时刻 batch 4 已收到 503。health extension 证明 Collector 进程和 HTTP health handler 存活,并没有执行 persistent queue 写事务,因此无法证明遥测入口可写。

如果编排器只依据这个 health endpoint,它不会重启或摘除已经拒绝数据的实例。生产至少需要一个 bounded write-read-delete storage probe,或把持续增长的 receiver_refused / enqueue_failed 纳入 readiness 与流量摘除策略。

Counter 闭合,Gauge 再次多计一项

故障点 metrics 为:

accepted              3
refused               1
sent                  0
failed                0
enqueue failed        1
queue size / capacity 4 / 20

前三个 200 与 accepted=3 闭合;第 4 个 503 与 refused=1enqueue_failed=1 闭合。queue gauge 却是 4,比已接受的持久集合多 1。

这个现象与 max-size 和 ENOSPC 实验方向一致:当前进程的内存 metadata 在持久 transaction 成功前推进,失败路径不立即给出磁盘权威 item 数。故障时不能把 queue_size=4 写成“磁盘有四批待恢复”;replacement loaded items 才是重启后的权威恢复集合。

恢复 linear 后,文件系统和数据库都未改变

故障 Collector 被 SIGKILL,mapper 恢复为 linear。固定 e2fsck 1.47.0 结果为:

exit code          0
filesystem clean   true
filesystem modified false

恢复后 bbolt 仍为 65,536 bytes,SHA-256 仍是:

fa557eba5d2e48ac4e3aa960e3b6c38e52e06a55c6aa493607a65592dd8dd8bd

因此本轮固定 fault 没有产生可见文件系统修复或数据库字节变化。这不是对所有 EIO 时点的保证;如果故障落在已开始写 page 或 journal commit 的中间,结果可能不同。

Replacement 只恢复三批,再补偿第四批

replacement 启动日志报告:

Loaded queue metadata
itemsSize        3
bytesSize    4,254
dispatchedItems  1

没有新 ingress 时:

accepted 0
sent     3
queue    0 / 20

Jaeger 此时只包含 e600...001e600...003,第 4 个 ID 仍 missing。随后只重放 batch 4;最终 Collector 为 accepted 1、sent 4、queue 0,Jaeger 为:

4 total
4 unique
0 duplicate
0 missing

先恢复三项、再补一项,证明 batch 4 的 503 没有被错误写入旧 queue;如果全量重放四批,这个结论会被最终 4/4 掩盖。

生产监控与恢复合同

本轮至少需要以下信号联合判断:

信号 能证明什么 不能证明什么
health HTTP 200 进程与 health handler 存活 queue 可写、数据可接受
receiver refused 请求已被 receiver 明确拒绝 具体底层错误
exporter enqueue failed persistent enqueue 未成功 旧 queue 是否损坏
queue gauge 当前进程内存观察值 磁盘权威 item 数
block/filesystem error class EIO、EROFS、ENOSPC 等故障层级 哪些稳定 IDs 已接受
bbolt bytes/hash 数据库字节身份 语义可加载、后端已交付
replacement loaded items/bytes 重启后的恢复集合 backend 最终完整性
Jaeger stable ID set 本后端查询面的最终对象集合 其他后端 exactly-once 语义

推荐的自动动作顺序是:

  1. enqueue_failed 或 storage write probe 失败时停止新流量,而不是等待 health 失败。
  2. 记录明确拒绝与未确认 stable IDs,冻结无预算自动重试。
  3. 保护原设备,恢复 block path 后离线检查文件系统和数据库。
  4. replacement 先在无新 ingress 下排空并保存 loaded/sent/ID 集合。
  5. 对拒绝集合逐项补偿,最后从权威后端查询 duplicate/missing。

复现

npm run lab:otel-eio:test
npm run lab:otel-eio:run

第一条对冻结工件执行 12 项断言。第二条在 Docker Desktop LinuxKit 内创建专用 loop、device-mapper 与 ext4,候选结果写入 /tmp/younis-ai-lab-otel-runtime-eio.json,不会覆盖发布工件。

runner 的 finally 默认恢复 linear target,并删除:

Collector / Jaeger containers
Compose network and Jaeger volume
external ext4 queue volume
device-mapper target
loop device
64 MiB backing volume

只有显式 --keep-stack 才保留现场。正式 capture 后已经确认 mapper、loop、容器、network、volumes 与 3171831733317883388633888 端口无残留。

失败与边界

第一,dm-error 是确定性故障注入,不是物理介质模型。它让映射范围 I/O 失败,但没有模拟闪存磨损、超时分布、silent corruption、控制器 reset 或部分扇区故障。

第二,HTTP EROFS 不能反推所有底层原因都是 EIO。只读 mount、ext4 error policy、权限与设备故障都可能在应用层表现为类似错误;本轮依靠 dm table 与独立读取才确认底层 EIO。

第三,health 200 是固定 health extension 的行为。不同部署可能用自定义 readiness probe;本文不声称所有 Collector 健康检查都忽略 storage。

第四,fsck clean 与数据库哈希不变只覆盖本次故障时点。故障发生在新的 queue transaction 写入时,旧三项保持不变;更多 write ordering、journal mode 和 page 边界需要独立矩阵。

第五,四个 synthetic spans 不是可靠性概率或吞吐 Benchmark。结果证明分支与恢复集合,不推导生产故障率、P95 或 MTTR。

第六,Jaeger 四个唯一对象不等于通用 exactly-once。固定后端最终查询没有重复;其他 storage backend、payload 变化和乱序恢复仍需验证。

复盘

第一个结论是故障层级不能被一个字符串压平。device-mapper 明确返回 EIO,独立 queue read 也得到 Input/output error,但 Collector 对外暴露 EROFS。告警系统如果按 message 去重,会把同一事故拆成两个;如果只保留最上层 message,又会丢失根故障面。

第二个结论是 liveness 不能替代 capability readiness。health 200 与第 4 批 503 同时成立,证明“进程活着”不等于“持久队列能接受数据”。对于必须持久化才返回 200 的入口,readiness 应覆盖一个低成本可写 transaction 或等价的存储能力信号。

第三个结论是恢复前的 clean 证据与恢复后的业务集合都需要。fsck clean 和 SHA 不变说明旧数据库没有可见变化;loaded=3、sent=3、Jaeger 三个 IDs 说明它在应用语义上可恢复;第 4 个仍 missing 才确定补偿清单。任意单项都不足以决定重放。

商业价值

在 AI 平台中,trace、evaluation event、Agent checkpoint、tool outbox 与模型请求审计常被视为辅助数据,但它们决定事故归因、成本核算、合规证明和未知终态恢复。health dashboard 全绿而入口持续拒绝数据,会产生最昂贵的一类盲区:业务仍运行,解释它的证据正在丢失。

可交付的生产能力包括 storage-aware readiness、跨层 error taxonomy、稳定 ID delivery ledger、隔离恢复、数据库身份检查和 backend reconciliation。它把“Collector 重启成功”提升为“哪些已接受数据恢复、哪些拒绝数据需要补偿、最终是否重复或缺失”的可审计结论。

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

传统全栈系统中的数据库事务、readiness、WAL 恢复、幂等键和业务对账,在 AI 系统里对应 telemetry queue、Agent checkpoint、异步工具回执和评测事件流。本轮体现的能力包括:

  • 用 Linux block target 构造真实 failure primitive,而不是 mock 一个异常字符串。
  • 区分 block EIO、filesystem EROFS、Collector 503 和业务 stable ID。
  • 发现 health 200 与写能力失效的 readiness gap。
  • 用文件系统、数据库、进程 metrics 与 backend query 形成恢复证据链。
  • 让补偿只覆盖被拒绝身份,避免全量重放制造重复。

面试表达

**30 秒版本:**我把 OpenTelemetry Collector 的 bbolt persistent queue 放在 64 MiB ext4 device-mapper 上。前三批在 linear target 下返回 200 并形成 queue 3;在线切到 error target 后,独立 block read 返回 EIO,但 Collector health 仍为 200,第 4 批收到 EROFS 503。metrics 是 accepted/refused 3/1、queue gauge 4。恢复 linear 后 fsck clean、数据库 hash 不变,replacement loaded=3、sent=3;只重放第 4 批后 Jaeger 4 unique、0 missing。

**3 分钟版本:**实验固定 Collector 0.156.0、Jaeger 2.19.0、单 consumer、fsync 和四个稳定 IDs,只把已经挂载的 131,072-sector device-mapper table 从 linear 切 error 再切回 linear。底层 helper 读 queue 得到 Input/output error,Collector 则返回 read-only file system,说明 errno 在 block、ext4 和应用层发生了转换。health handler 没有触碰 storage,故障时仍 200;真正闭合入口的是 refused=1 与 enqueue_failed=1。故障进程 SIGKILL 后恢复 mapper,离线 e2fsck exit 0,bbolt bytes/hash 不变。replacement 无新 ingress 时 sent=3,Jaeger 只有前三个 ID,第 4 个仍 missing;定向补偿后终态无重复无缺失。生产上我会用 storage-aware readiness、跨层错误分类和 stable-ID ledger,不能只看 health 或 queue gauge。

方法披露

正文与图表由 AI 辅助整理;实验设计、device-mapper 探针、两次正式 Docker capture、工件冻结、哈希复核、12 项断言和结论核对均在本地完成。公开工件不保存 synthetic padding 原文、完整日志、容器 ID、loop device、Docker 宿主 mountpoint、数据库凭证或个人路径。

修订记录

  • 2026-07-16:初版发布;完成 linear/error/linear 运行期故障、block EIO 与 Collector EROFS 分层、health 200 差异、queue gauge 多计、clean fsck、数据库身份、三项恢复、单项补偿和 Jaeger 四身份终态验证。

Reusable projects

关联可复用项目

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

Source ledger

来源账本

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

  1. Linux v6.12 device-mapper error target documentation
    Linux Kernel官方文档文章资料来源发布:2024/11/17本站核验:2026/07/16
  2. e2fsprogs v1.47.0 e2fsck manual source
    e2fsprogs官方文档文章资料来源发布:2023/02/05本站核验:2026/07/16
  3. Collector Contrib file storage extension README at v0.156.0 commit
    OpenTelemetry官方文档文章资料来源发布:2026/07/07本站核验:2026/07/16
  4. Collector exporter helper persistent queue documentation at v0.156.0 commit
    OpenTelemetry官方文档文章资料来源发布:2026/07/06本站核验:2026/07/16
  5. Jaeger v2.19.0 query extension documentation
    Jaeger官方文档文章资料来源发布:2026/06/03本站核验:2026/07/16

讨论

正在加载评论...