如何在运行时模拟 I/O 故障
本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

在生产环境中,文件系统故障可能由多种事件引发,例如磁盘故障或管理员操作失误。作为混沌工程平台,Chaos Mesh 从早期版本就支持模拟文件系统的 I/O 故障。只需添加一个 IOChaos 自定义资源定义(CRD),就能观察文件系统如何失效并返回错误。
但在 Chaos Mesh 1.0 之前,这类实验实施复杂且资源消耗较大。我们需要通过动态准入控制 Webhook 向 Pod 注入 Sidecar 容器,并重写 ENTRYPOINT 启动命令。即使未注入故障,这些 Sidecar 容器也会带来显著的系统开销。
Chaos Mesh 1.0 彻底改变了这一状况。现在我们可以使用 IOChaos 在运行时向文件系统注入故障,既简化了流程又大幅降低了系统开销。本文将介绍如何在不使用 Sidecar 的情况下实现 IOChaos 实验。
I/O 故障注入
要在运行时模拟 I/O 故障,我们需要在程序发起系统调用(如读写操作)后、请求抵达目标文件系统前注入故障。可通过两种方式实现:
-
使用伯克利数据包过滤器(BPF),但该方法无法注入延迟。
-
在目标文件系统前添加名为 ChaosFS 的文件系统层。ChaosFS 将目标文件系统作为后端,接收操作系统请求。完整调用链路为:目标程序系统调用 → Linux 内核 → ChaosFS → 目标文件系统。由于 ChaosFS 可自定义,我们能够按需注入延迟和错误,因此选择此方案。
但 ChaosFS 存在几个问题:
-
若 ChaosFS 需读写目标文件系统中的文件,必须将其挂载到与 Pod 配置中目标路径不同的位置。ChaosFS 无法直接挂载到目标目录路径。
-
必须在目标程序启动前挂载 ChaosFS。因为新挂载的 ChaosFS 仅对程序在目标文件系统中新打开的文件生效。
-
需要将 ChaosFS 挂载到目标容器的
mnt命名空间。详见 mount_namespaces(7) — Linux 手册页。
在 Chaos Mesh 1.0 之前,我们通过动态准入控制 Webhook 实现 IOChaos。该技术解决了上述三个问题,使我们能够:
-
在目标容器内运行脚本:将 ChaosFS 后端文件系统的目标目录变更(例如从
/mnt/a改为/mnt/a_bak),从而将 ChaosFS 挂载到原目标路径(/mnt/a)。同时修改 Pod 启动命令,例如将原始命令/app改为/waitfs.sh /app。 -
waitfs.sh脚本持续检查文件系统是否成功挂载,确认挂载后才会启动/app。 -
在 Pod 中添加新容器来运行 ChaosFS。该容器需与目标容器共享存储卷(例如
/mnt),然后将该存储卷挂载到目标目录(例如/mnt/a)。同时我们需为该存储卷正确配置挂载传播,使其穿透共享到宿主机,再以从属模式穿透到目标容器。
这三种方法虽能实现运行时 I/O 故障注入,但操作过程相当不便:
-
仅能对存储卷的子目录注入故障,无法覆盖整个存储卷。变通方案是用
mount move替代mv(重命名)命令来移动目标存储卷的挂载点。 -
必须在 Pod 中显式编写命令,不能隐式使用镜像预设命令。否则
/waitfs.sh脚本无法在文件系统挂载后正确启动程序。 -
相关容器需预先配置挂载传播参数。考虑到潜在的隐私和安全风险,我们无法通过可变准入 Webhook 修改该配置。
-
注入配置过程繁琐。更糟糕的是,配置生效后必须重建 Pod 才能注入故障。
-
程序运行时无法撤除 ChaosFS。即使未注入任何故障或错误,系统性能仍会大幅下降。
免 Webhook 实现 I/O 故障注入
能否不依赖可变准入 Webhook 解决这些难题?让我们回溯思考最初使用 Webhook 添加 ChaosFS 容器的根本原因——实则是为了将文件系统挂载到目标容器。
其实另有方案:无需向 Pod 添加容器,只需通过 Linux 系统调用 setns 修改当前进程的命名空间,再调用 mount 将 ChaosFS 挂载到目标容器。假设待注入文件系统为 /mnt,新注入流程如下:
-
当前进程使用
setns进入目标容器的 mnt 命名空间 -
执行
mount --move将/mnt移动到/mnt_bak -
将 ChaosFS 挂载到
/mnt并以/mnt_bak作为后端存储
流程完成后,目标容器将通过 ChaosFS 打开、读取和写入 /mnt 中的文件,从而更便捷地注入延迟或故障。但仍需解决两个关键问题:
-
如何处理目标进程已打开的文件?
-
在文件打开状态下无法卸载文件系统,如何实现恢复操作?
动态替换文件描述符
ptrace 可同时解决上述两个问题。我们能够通过 ptrace 在运行时替换已打开的文件描述符(FD),并替换当前工作目录(CWD)及内存映射(mmap)。
使用 ptrace 使被跟踪进程执行二进制程序
ptrace 是强大的工具,可使目标进程(tracee)执行任意系统调用或二进制程序。为让 tracee 执行程序,ptrace 将 RIP 寄存器指向的地址修改为目标进程地址,并添加 int3 指令触发断点。当二进制程序停止时,我们需要恢复寄存器和内存。
注意:
在 x86_64 架构中,RIP 寄存器(亦称指令指针)始终指向下一条待执行指令的内存地址。要将程序加载到目标进程内存空间:
-
通过 ptrace 在目标程序中调用 mmap 以分配所需内存
-
将二进制程序写入新分配的内存,并将 RIP 寄存器指向该内存地址。
-
二进制程序执行完毕后,调用 munmap 清理该内存区域。
最佳实践中,我们通常使用 process_vm_writev 替代 ptrace 的 POKE_TEXT 写入操作,因为当需要写入大量数据时,process_vm_writev 的执行效率更高。
通过 ptrace 机制,我们可以使进程自行替换其文件描述符(FD)。现在只需通过 dup2 系统调用来实现这个替换操作。
使用 dup2 替换文件描述符
dup2 函数的签名为 int dup2(int oldfd, int newfd);。它用于创建旧文件描述符(oldfd)的副本,新副本的文件描述符编号为 newfd。若 newfd 已对应某个已打开文件的 FD,系统会自动关闭该已打开文件的文件描述符。
例如:当前进程打开 /var/run/__chaosfs__test__/a 的文件描述符为 1。若需将其替换为 /var/run/test/a,该进程需执行以下操作:
-
通过
fcntl系统调用获取/var/run/__chaosfs__test__/a的OFlags(即open系统调用的参数,如O_WRONLY)。 -
使用
Iseek系统调用获取当前seek位置。 -
使用相同的
OFlags通过open系统调用打开/var/run/test/a(假设其文件描述符为2)。 -
使用
Iseek调整新打开文件描述符2的seek位置。 -
执行
dup2(2, 1)将/var/run/__chaosfs__test__/a的文件描述符1替换为新打开的2。 -
关闭文件描述符
2。
完成上述操作后,该进程的文件描述符 1 将指向 /var/run/test/a。为实现故障注入,所有后续对目标文件的操作都将经过用户空间文件系统(FUSE)。FUSE 是类 Unix 操作系统的软件接口,允许非特权用户在不修改内核代码的前提下创建自定义文件系统。
编写程序使目标进程自行替换文件描述符
结合 ptrace 和 dup2 的功能,跟踪进程(tracer)可使被跟踪进程(tracee)自行替换已打开的文件描述符。现在我们需要编写二进制程序并让目标进程执行:
注意:
上述实现基于以下前提:
- 目标进程的线程均为 POSIX 线程且共享已打开的文件
- 目标进程使用
clone函数创建线程时传递了CLONE_FILES参数因此 Chaos Mesh 仅替换线程组中首个线程的文件描述符
整体故障注入流程
以下示意图展示了完整的 I/O 故障注入流程:

在此图中,每条水平线代表一个沿箭头方向执行的线程。其中文件系统挂载/卸载与文件描述符替换任务经过精心编排形成顺序执行。结合前述流程,这种任务安排具有充分的合理性。
后续计划
本文探讨了运行时 I/O 故障注入的实现机制(详见 chaos-mesh/toda),但当前实现仍存在以下待优化点:
-
暂不支持世代编号(generation numbers)
-
尚未实现 ioctl 支持
-
文件系统挂载状态检测存在约 1 秒延迟
如果您对Chaos Mesh感兴趣并愿意参与改进,欢迎加入Slack交流频道,或通过GitHub仓库提交PR和issue
本文是 Chaos Mesh 实现原理系列的首篇,后续将持续解析其他类型故障注入的实现机制,敬请关注。