最近一直沉迷于折腾各种错误注入, 总想着把我们的系统给搞挂, 有一天, 突发奇想, 是不是能通过 SystemTap 来捣乱, 因为 SystemTap 能 probe 相关的函数, 那么我们就一定能在这些 probe 里面干一些事情, 想到这里, 立刻激动不已, 直接 Google 了一下, 发现业界早就有这么做的先例, 这能说自己太 out 了
SCSI Fault Injection
Google 搜出来提到最多的就是 SCSI Fault Injection Test 这篇 Paper, 里面提到了用 SystemTap 在 SCSI 这一层对 I/O 进行注入, 当即我就非常的兴奋, 毕竟我一直在思考一个好的办法对 TiKV 存储的数据进行干扰, 譬如写一块数据进去的时候, 把这些数据直接改掉, 或者读文件的时候随机的报错这些但悲催的是, 这篇文件使用的 kernel 已经非常的老了, 在 CentOS 7 下面根本就跑不了幸好, 原理还是很好理解, 所以对着新的内核稍微调整一下就行了在继续之前, 先来聊聊, 为什么这篇文章的作者想用 SystemTap 在 SCSI 这层注入错误
大家都知道, 当我们在处理 I/O 的时候, 会面临各种这样的错误, 但如何在程序里面模拟这些错误, 其实是一件比较困难的事情通常的做法就是写一个 mock IO, 但这个仅仅只能用于 unit test, 而且完全模拟所有的错误其实也不现实, 过度追求 coverage, 会导致整个的 test 代码极度的膨胀我们其实更希望的是, 在程序正常运行的过程中, 模拟很多错误, 来观察程序在这些错误情况下面的反映因为我们要注入 I/O 错误, 在 Linux 系统里面, SCSI 作为一个通用的 driver, 对其进行注入就非常的合适了
对于一个 SCSI 故障来说, 通常有两种, 一个就是 SCSI 的设备返回了一个错误, 而另一个就是 SCSI 的设备没有任何返回, 超时了对于底层硬件设备 (譬如 HDD) 来说, 通常也是两种故障, 临时的 (可恢复) 和永久的 (不可恢复) 根据上面的情况, 我们有多种组合:
临时的读错误
临时的写错误
能被后续的写覆盖修复的读错误, 这种的本来读是错了, 后面的写能够覆盖, 这样后面的读还是能读到最新的写入数据
永久的读错误, 不同于上面可被写覆盖修复的读错误, 这里的任何读都会失败, 但写有可能不会, 因为一些硬件不会去进行写错误检查
永久的读写错误
临时的读超时
临时的写超时
永久的读写超时
可以看到, 如果我们需要模拟上面这么多种组合, 其实是比较困难的, 业界有一些办法, 但都有局限性, 譬如:
Linux 的 scsi_debug driver, 它提供了一个模拟的 SCSI 设备, 但还是有一些局限性, 譬如对于一些访问注入错误, 只能通过 sector 0x1234 来进行
Linux 的 Fault Injection 框架, 这个是 kernel 原生支持的, 但也有一些局限, 譬如不能提供 SCSI 的设备 timeout, 另外, 这个功能需要重新编译 kernel 去支持, 并不通用
使用特定的设备, 这个就不说了, 更加不通用了
所以, 论文的作者使用了 SystemTap 来进行这里我不详细的说明论文里面 SCSI 的整个工作流程, 主要是因为论文里面使用的内核版本比较低, 我现在也不知道最新的内核的工作流程了但对于 SystemTap 注入方法, 其实应该差不多的譬如对于 I/O error 来说:
系统发起一次 I/O request, 进入 SCSI layer
进行 scsi_dispatch_cmd, 调用到对应的 SCSI command
使用 SystemTap 注入, 将数据的 length 改成 0, 并更改 SCSI 的 command
将 command 发到 SCSI 的设备执行
收到 SCSI 设备的执行结果
使用一个 fake 的结果替换, 生成一个 SCSI Error
调用
scsi_decide_disposition
, 返回一个 I/O Error
可以看到, 只要我们理解了整个 SCSI 的流程, 用 SystemTap 进行注入是非常简单的, 具体到作者的代码, 原来的已经不能跑了, 我找到了一个 Github 的, 但也没法运行, 于是稍微改了一下, 放在我的 fault injection 下面, 主要有几个改动:
原来的 SCSI driver 是在 module 里面, 但 CentOS 7 是编译到了 kernel 里面
原来 Embedded C 代码使用的是 THIS 来访问参数和返回值, 但最新的改成了 STAP_ARG_ 和 STAP_RETURN
原来的 requestbuf_len 没有了, 改成了从 request 里面获取 __data_len
原来 request 的 sector 变量改成了 __sector
但这个只能搞定 I/O error 的, 对于 I/O timeout,CentOS 7 的内核已经没有了 SCSI 相关的 timer 函数, 所以就没有折腾了
VFS Fault Injection
上面使用的是 SCSI 的方式, 但并没有很好的处理 timeout, 于是我想, 是不是能够更简单一点, 直接在 VFS 或者 syscall 这层上面处理, 于是立马开始弄, 首先就是 timeout, 这个比较难模拟, 但可以模拟 delay, 于是开干:
- probe vfs.read.return
- {
- udelay(300)
- }
上面的脚本就是在 read 返回的时候 delay 300 us, 模拟 I/O 的延迟, 实际测试也发现读取速度慢了下来
然后就是 I/O error, 想到应该也能直接改掉 return 的返回值, 于是开干:
- probe vfs.write.return
- {
- $return = -28
- }
上面在 write 返回的时候, 将结果改成了 -28, 也就是 no space 的错误
我们还可以做的更多, 譬如在 write 的时候, 直接将 buf 里面的数据给改掉, 或者在 read 的时候也改掉对应的数据
小结
可以看到, 使用 SystemTap 能非常方便的进行 fault injection, 这也会成为我后面一个重点比较关注的地方, 因为我们不光要给 I/O 注入错误, 还可以处理 CPU,Scheduler,Memory 这些总之, 我们就是需要尽量的将系统搞坏, 然后看我们整个系统在这些极端情况下会是怎样的反映
如果有谁对这块感兴趣, 喜欢折腾 kernel, 喜欢用 SystemTap 来进行动态追踪, 错误注入, 欢迎联系我: tl@pingcap.com
来源: http://www.jianshu.com/p/3f9881ca1d30