使用 SystemTap 进行 I/O 错误注入

最近一直沉迷于折腾各种错误注入,总想着把我们的系统给搞挂,有一天,突发奇想,是不是能通过 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 来说:

  1. 系统发起一次 I/O request,进入 SCSI layer。
  2. 进行 scsi_dispatch_cmd,调用到对应的 SCSI command
  3. 使用 SystemTap 注入,将数据的 length 改成 0,并更改 SCSI 的 command。
  4. 将 command 发到 SCSI 的设备执行。
  5. 收到 SCSI 设备的执行结果。
  6. 使用一个 fake 的结果替换,生成一个 SCSI Error。
  7. 调用 scsi_decide_disposition,返回一个 I/O Error。

可以看到,只要我们理解了整个 SCSI 的流程,用 SystemTap 进行注入是非常简单的,具体到作者的代码,原来的已经不能跑了,我找到了一个 Github 的,但也没法运行,于是稍微改了一下,放在我的 fault injection 下面,主要有几个改动:

  1. 原来的 SCSI driver 是在 module 里面,但 CentOS 7 是编译到了 kernel 里面。
  2. 原来 Embedded C 代码使用的是 THIS 来访问参数和返回值,但最新的改成了 STAP_ARG_STAP_RETURN
  3. 原来的 requestbuf_len 没有了,改成了从 request 里面获取 __data_len
  4. 原来 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

点赞