使用 Fuse 来进行 I/O 错误注入

在之前介绍 SystemTap 的文章中,我提到了我们使用 SystemTap 做了很多 I/O 错误注入的工作,但也有一些局限,譬如:

  • Delay 的时间如果过长,就可能导致 SystemTap 出错。
  • 不支持动态调节,如果需要将 delay 的时间从 100 ms 调整到 200 ms,只能重新启动 SystemTap 脚本。
  • 不能很好的支持精确的控制,譬如对某一个文件进行限流控制,对另一个文件进行错误注入。虽然能做,但脚本写起来并不简单。

虽然 SystemTap 很简单,但有时候,我们需要另一套机制。这里,我们参考了 Namazu,使用了 Fuse 对 I/O 进行错误注入。

什么是 Fuse

Fuse 是一个用户态文件系统框架。得益于 Fuse 简单的 API,很多其他的文件系统都是基于 Fuse 来开发的,自然,我们也可以基于 Fuse 来开发一个自己的 I/O injection 文件系统。虽然 Fuse 能在很多操作系统上面使用,但这里我们仍然聚焦在 Linux。

下图是 Fuse 架构,主要参考 To FUSE or Not to FUSE: Performance of User-Space File Systems 这篇 Paper:

Fuse 包含两个部分 - kernel 和用户态 daemon。内核部分是一个 Linux 的内核模块,它会在 Linux 的 VFS 上面注册一个 Fuse 的文件系统驱动。这个 Fuse 驱动可以认为是一个 proxy,会将请求给转发到后面的用户态 daemon 上面。

Fuse 内核模块也会注册一个 /dev/fuse 的块设备,这个就是 kernel 和用户态 daemon 交互的接口。通常 Daemon 会从 /dev/fuse 上面读取到 Fuse 的请求,处理并且将数据写回到 /dev/fuse。一个简单的 Fuse 流程如下:

  • 应用程序在挂载的 Fuse 的文件系统上面进行操作。
  • VFS 会将操作转发到 Fuse 的 kernel driver 上面。
  • Fuse 的 kernel driver 分配一个 request,并且将这个 request 提交到 Fuse 的 queue 上面。
  • Fuse 的用户态 daemon 会从 queue 里面通过读取 /dev/fuse 将这个 request 取出来并且处理。这里需要注意,处理 request 的时候仍然可能进入 kernel,譬如可能将 request 发到 Ext4 去实际处理。
  • 当请求处理完毕,Daemon 会将结果写回到 /dev/fuse
  • Fuse 的 kernel 标记这个 request 结束,然后唤醒用户应用程序。

Go Fuse

上面可以看到,如果我们要实现自己的文件系统,主要就是实现我们自己的 daemon,而这个其实就是搞定 Fuse 的 User-Kernel 协议就可以。这里我不准备详细介绍 Fuse 的协议,以及 Fuse 的底层实现,后面有机会可以再写一篇。而是会直接切入,讲讲如何用 Fuse。

得益于 Fuse 的广泛使用,几乎所有的语言都有 Fuse 的支持了,在 Go 里面,两个比较知名的项目,一个是 go-fuse,另一个是 fuse,这里,我是用 go-fuse 来说明如何构建一个 zip 文件系统。

首先我们生成一个简单的 zip 文件,压缩之前目录如下:

a/
a/a.log
b.log

A.log 和 b.log 的内容都是 “123”。然后我们希望,将这个 zip 文件挂载到一个目录,能让我们按照普通的文件访问方式来访问这个 zip 文件。譬如,我们挂载到目录 m,可以进行如下操作:

➜  m ls
a     b.log
➜  m cat b.log
123
➜  m cat a/a.log
123

那么这个是如何做到的呢?使用 go-fuse,我们可以非常简单的做到。我们仅仅需要实现自己的一个文件系统,然后挂载到某一个路径下面就可以了,首先来看看挂载的代码:

root, _ := zipfs.NewArchiveFileSystem("test.zip")
opts := &nodefs.Options{
    AttrTimeout:  time.Second,
    EntryTimeout: time.Second,
}
state, _, _ := nodefs.MountRoot("m", root, opts)
state.Serve()

上面我们使用 nodefs 的 MountRoot 函数将一个 zip 文件系统挂载到了路径 “m” 上面。那么这个 zip 文件系统是如何实现的呢?这里,我们要做的就是使用 Go 自带的 archive/zip 包解析出 zip 里面的目录结构,一个简单的例子:

r, _ := zip.OpenReader("./test.zip")
defer r.Close()

for _, f := range r.File {
    log.Printf("name: %s, is dir %v", f.Name, f.FileInfo().IsDir())
}

然后我们根据 zip 包的目录结构,先用 nodefs.NewDefaultNode() 创建一个 root node,再依次递归,不断的调用 node 的 NewChild 函数生成一个文件目录树。具体的代码在 zipfs,代码比较简单,这里不再详细描述。

Hook I/O

可以看到,使用 go-fuse 我们可以非常方便的构建自己的文件系统,那么对于我们的 I/O failure injection 文件系统来说,要做什么事情呢?其实也就是非常简单,就是能 hook 到所有的 I/O 操作,然后在里面注入错误就可以了。这个,在 go-fuse 里面,我们可以创建一个 Loopback 的文件系统就可以了。Loopback 的文件系统,就是将我们所有的 I/O 请求给重新发送到实际底层的文件系统上面。这个比较类似于对一个目录进行 soft link,然后你再这个 soft 目录里面进行的任何操作也会影响到实际的原始目录。

Namazu 已经帮我们提供好了好了封装,就是 hookfs,在 hookfs 里面,它 hook 了大部分的 I/O 操作(后面我会逐渐添加完善),我们仅需要的是实现自己的接口,任何的 I/O 操作就能调用到我们自己的对应函数里面进行处理了。

Delay Example

下面,我们来使用 hookfs 做一个简单的 delay 功能,默认任何的 read,我们都会 delay 1s,当然,也可以动态调整。

因为我们是想 delay read,所以我们需要实现下面的接口:

type HookOnRead interface {
    // if hooked is true, the real read() would not be called   
    PreRead(path string, length int64, offset int64) (buf []byte, err error, hooked bool, ctx HookContext)
    PostRead(realRetCode int32, realBuf []byte, prehookCtx HookContext) (buf []byte, err error, hooked bool)
}

代码比较简单:

type MyHookContext struct{}
type MyHook struct {
    dur time.Duration
}

func (h *MyHook) PreRead(path string, length int64, offset int64) ([]byte, error, bool, hookfs.HookContext) {
    time.Sleep(h.dur)
    return nil, nil, false, MyHookContext{}
}

func (h *MyHook) PostRead(realRetCode int32, realBuf []byte, prehookCtx hookfs.HookContext) (buf []byte, err error, hooked bool) {
    return realBuf, nil, false
}

我们在 PreRead 里面 sleep 了特定的时间,然后我们启动 hookfs:

h := &MyHook{
    dur: time.Second,
}
fs, _ := hookfs.NewHookFs(originPath, mountPath, h)
fs.Serve()

上面默认的 delay 时间是 1s,然后我们可以添加一个 HTTP 服务来动态修改 delay 时间,当然这里没考虑 data race 问题:

go func() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        h.dur, _ := time.ParseDuration(r.FormValue("dur"))        
    })

    http.ListenAndServe("127.0.0.1:8080", nil)
}()

然后我们来看实际效果,我们将 /tmp/b 目录给挂载到 /tmp/a 目录,该目录里面有一个文件 “a.log”:

time cat /tmp/a/a.log
124
cat /tmp/a/a.log  0.00s user 0.00s system 0% cpu 1.002 total

可以看到,上面的 cat 耗时 1s。然后我们动态修改时间,在继续:

curl http://127.0.0.1:8080\?dur\=2s
time cat /tmp/a/a.log
124
cat /tmp/a/a.log  0.00s user 0.00s system 0% cpu 2.002 total

我们将耗时改成了 2s,实际 cat 也耗时 2s 了。

注意:如果我们不测试了,需要调用 fusermount -u /tmp/a

Epilogue

使用 Fuse,我们可以非常方便对 I/O 进行模拟。当然,上面仅仅是一个非常简单的例子,实际的模拟功能会非常的强大,譬如进行限流,注入错误,修改数据等,甚至集成 Lua 做更加复杂的控制。业内也有相关的一些开源实现,如果你对这块感兴趣,欢迎联系我 tl@pingcap.com

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容