跳至主内容

如何在运行时模拟 I/O 故障

· 1 分钟阅读
Keao Yang
Maintainer of Chaos Mesh
非官方测试版翻译

本页面由 PageTurner AI 翻译(测试版)。未经项目官方认可。 发现错误? 报告问题 →

混沌工程 - 如何在运行时模拟 I/O 故障
混沌工程 - 如何在运行时模拟 I/O 故障

在生产环境中,文件系统故障可能由多种事件引发,例如磁盘故障或管理员操作失误。作为混沌工程平台,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,新注入流程如下:

  1. 当前进程使用 setns 进入目标容器的 mnt 命名空间

  2. 执行 mount --move/mnt 移动到 /mnt_bak

  3. 将 ChaosFS 挂载到 /mnt 并以 /mnt_bak 作为后端存储

流程完成后,目标容器将通过 ChaosFS 打开、读取和写入 /mnt 中的文件,从而更便捷地注入延迟或故障。但仍需解决两个关键问题:

  • 如何处理目标进程已打开的文件?

  • 在文件打开状态下无法卸载文件系统,如何实现恢复操作?

动态替换文件描述符

ptrace 可同时解决上述两个问题。我们能够通过 ptrace 在运行时替换已打开的文件描述符(FD),并替换当前工作目录(CWD)及内存映射(mmap)。

使用 ptrace 使被跟踪进程执行二进制程序

ptrace 是强大的工具,可使目标进程(tracee)执行任意系统调用或二进制程序。为让 tracee 执行程序,ptrace 将 RIP 寄存器指向的地址修改为目标进程地址,并添加 int3 指令触发断点。当二进制程序停止时,我们需要恢复寄存器和内存。

注意:

x86_64 架构中,RIP 寄存器(亦称指令指针)始终指向下一条待执行指令的内存地址。要将程序加载到目标进程内存空间:

  1. 通过 ptrace 在目标程序中调用 mmap 以分配所需内存

  2. 将二进制程序写入新分配的内存,并将 RIP 寄存器指向该内存地址。

  3. 二进制程序执行完毕后,调用 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,该进程需执行以下操作:

  1. 通过 fcntl 系统调用获取 /var/run/__chaosfs__test__/aOFlags(即 open 系统调用的参数,如 O_WRONLY)。

  2. 使用 Iseek 系统调用获取当前 seek 位置。

  3. 使用相同的 OFlags 通过 open 系统调用打开 /var/run/test/a(假设其文件描述符为 2)。

  4. 使用 Iseek 调整新打开文件描述符 2seek 位置。

  5. 执行 dup2(2, 1)/var/run/__chaosfs__test__/a 的文件描述符 1 替换为新打开的 2

  6. 关闭文件描述符 2

完成上述操作后,该进程的文件描述符 1 将指向 /var/run/test/a。为实现故障注入,所有后续对目标文件的操作都将经过用户空间文件系统(FUSE)。FUSE 是类 Unix 操作系统的软件接口,允许非特权用户在不修改内核代码的前提下创建自定义文件系统。

编写程序使目标进程自行替换文件描述符

结合 ptrace 和 dup2 的功能,跟踪进程(tracer)可使被跟踪进程(tracee)自行替换已打开的文件描述符。现在我们需要编写二进制程序并让目标进程执行:

注意:

上述实现基于以下前提:

  • 目标进程的线程均为 POSIX 线程且共享已打开的文件
  • 目标进程使用 clone 函数创建线程时传递了 CLONE_FILES 参数

因此 Chaos Mesh 仅替换线程组中首个线程的文件描述符

  1. 根据前述章节和系统调用指令用法编写汇编代码,此处提供示例

  2. 使用汇编器将代码编译为二进制程序(本项目采用 dynasm-rs

  3. 通过 ptrace 使目标进程运行该程序,实现运行时文件描述符替换

整体故障注入流程

以下示意图展示了完整的 I/O 故障注入流程:

故障注入流程
故障注入流程

Fault injection process

在此图中,每条水平线代表一个沿箭头方向执行的线程。其中文件系统挂载/卸载文件描述符替换任务经过精心编排形成顺序执行。结合前述流程,这种任务安排具有充分的合理性。

后续计划

本文探讨了运行时 I/O 故障注入的实现机制(详见 chaos-mesh/toda),但当前实现仍存在以下待优化点:

  • 暂不支持世代编号(generation numbers)

  • 尚未实现 ioctl 支持

  • 文件系统挂载状态检测存在约 1 秒延迟

如果您对Chaos Mesh感兴趣并愿意参与改进,欢迎加入Slack交流频道,或通过GitHub仓库提交PR和issue

本文是 Chaos Mesh 实现原理系列的首篇,后续将持续解析其他类型故障注入的实现机制,敬请关注。