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 对账。
时间与证据
上一篇 只读队列启动实验从一开始就把同一 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.185、mke2fs/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...001 到 e600...004。sender 没有自动重试。
| batch | mapper target | 入口用途 |
|---|---|---|
| 1 | linear |
已接受基线 |
| 2 | linear |
已接受基线 |
| 3 | linear |
已接受基线 |
| 4 | error |
运行期故障请求 |
每个阶段同时观察:
- 块设备层:dm table 是
linear还是error,独立读取是否返回Input/output error。 - Collector 层:health、OTLP HTTP、logs、accepted/refused、enqueue failed 与 queue gauge。
- 最终数据层:replacement loaded items、Jaeger stable IDs、补偿前 missing set 与最终 duplicate/missing。
如果只保存 HTTP body,就会把 EROFS 当成故障根因;如果只保存 dm table,又无法证明 Collector 实际拒绝了哪一个业务身份。
恢复顺序
故障后不直接把 mapper 切回 linear 并继续使用原进程。固定流程是:
- 对故障 Collector 发送
SIGKILL,避免继续接受流量。 - mapper 恢复
linear。 - 删除持有 queue mount 的容器,确保文件系统可离线检查。
- 执行
e2fsck -fy,只有 exit 0 或已修复 exit 1 才能继续。 - 重新计算 bbolt bytes 与 SHA-256。
- 启动 replacement Collector,让旧队列先独立排空。
- 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=1、enqueue_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...001 至 e600...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 语义 |
推荐的自动动作顺序是:
enqueue_failed或 storage write probe 失败时停止新流量,而不是等待 health 失败。- 记录明确拒绝与未确认 stable IDs,冻结无预算自动重试。
- 保护原设备,恢复 block path 后离线检查文件系统和数据库。
- replacement 先在无新 ingress 下排空并保存 loaded/sent/ID 集合。
- 对拒绝集合逐项补偿,最后从权威后端查询 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 与 31718、31733、31788、33886、33888 端口无残留。
失败与边界
第一,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
关联可复用项目
本文已经进入以下工程项目;项目页提供固定版本、运行命令和结果工件。
- 已独立复现
本文在线切换 device-mapper error target,证明 health 仍为 200 时 OTLP 已 503;修复、fsck、重启与定向重放后终态完整。
Source ledger
来源账本
以下来源用于核对事实、日期与当时可用范围。厂商自报性能不视为本站独立复现。
讨论