文件系统天生就是不平等的 - 实现崩溃一致性应用的复杂性

最近看到一篇 Paper,觉得很有意思,Paper 的主题是 “All File Systems Are Not Created Equal”,看来文件系统也跟人一样,天生就是不平等的。这篇 Paper 主要是讨论如何找出应用程序对文件操作的有问题的地方,保证在发生崩溃的时候,也能保证数据的一致性,从而正常的恢复,论文里面作者叫做 “crash consistent”,也就是『崩溃一致性』。

要让应用在崩溃之后,能正常恢复,说起来容易,实际还是比较困难的,因为一方面应用程序会依赖底层的文件系统的实现,不同文件系统对一些保证是不一样的,譬如是否能支持原子 rename 这些。同时,很多文件系统,会提供丰富的配置供用户调优,譬如 etx4 就有 write back,ordered 这些挂载属性,这些就更加难判定文件系统在崩溃时的具体行为。

而另一方面,则是应用程序自己对数据一致性的处理,我们都知道,每次文件操作之后,都进行强制 sync,这样就是非常安全的(当然,这里排除了硬件故障),但大家知道这个性能就是非常的差了,所以为了性能,我们势必在文件操作上面做很多优化,这些就在崩溃的时候引入了不确定性了。

为了解决这两个问题,论文作者实现了两个工具,一个就是 Block Order Breaker (BOB),而另一个就是 Application-Level Intelligent Crash Explorer(ALICE)。

BOB

BOB 主要是为了测试文件系统的 Persistence Properties。对于一个给定的文件系统,当在发生崩溃之后,有哪些可能的状态,这个就是用 Persistence Properties 来确定的。

那这个 BOB 是怎么做的呢?首先 BOB 会一个简单的场景去压力测试 Persistence Properties(譬如,一批特定 size 的持续写入来验证 overwrite 的原子性)。BOB 会收集这个场景产生的 block I/O,然后重新排序这些收集的 blocks,选择性的将一些写入到磁盘去产生一个合法的磁盘 state。用这种方式,BOB 能产生一批在崩溃之后,多个对应不同 disk states 的唯一的磁盘 images 。然后 BOB 会在各自的 image 上面执行文件系统的 recovery ,并检查 Persistence Properites 是否继续满足(譬如 Write 是原子的)。

ALICE

ALICE 的原理其实比较简单,就是通过 trace 应用程序的 system call 来直接构建文件的 state。

使用 ALICE 的时候,用户首先需要提供一个初始化的文件 snapshot,譬如一个完整的文件目录,一个 workload 脚本(譬如执行一次事务),一个跟 workload 对应的 checker 脚本,用来检查 workload 的不变性(譬如事务是否是原子的)。Alice 会在不同的 crash states 上面去执行 checker。在执行 workload 的时候,Alice 会生成 update protocol 的逻辑表示,记录 protocol 里面的脆弱的地方以及对应的代码,以及 persistence properties。

ALICE 会将 trace 的 system call 转换成逻辑操作。逻辑操作会将当前的 system call,I/O 操作抽象成更精简的文件系统操作。譬如,write,pwrite,writev,pwritev,mmap 这些操作会转成 overwrite 或者 append。

对于不同的文件系统来说,crash state 是不一样的,ALICE 使用一个 Abstract Persistence Model (APM) 来抽象出不同文件系统的 crash state,如果想测试特定文件系统里面的易脆性,可以写一个特定的 APM。对于指定的文件系统,APM 会指定逻辑操作里面原子性和顺序性的所有约束,这样就能定义 crash state。APM 通过两个逻辑实体 - file inodes 包括 data 和 size,directories 包括 directory 的 entries。每个逻辑操作会操作一个或者多个这些实体。为了更好的抓住中间的 crash state,APM 会将逻辑操作拆分成多个微操作 - 也就是能用到逻辑实体的最小原子更新。主要包括:

  • write_block:也就是写到文件的一个 size 的 block。包含两个特定的参数,zeroesrandomzero 意味着文件系统初始化了一个新分配的全为 0 的 block,而 random 则是表示一个未初始化的 block。如果文件不是追加写,那么就不会更新文件的 size。
  • change_file_size:更新 file inode 的大小
  • create_dir_entry:在一个目录里面创建一个目录 entry,并且关联一个文件 inode 或者目录到它上面。
  • delete_dir_entry:删掉一个目录 entry。
  • stdout:在终端输出添加消息。

APM 定义了逻辑操作如何转换成微操作,譬如上面说的 overwrite 操作,就会转换成一个或者多个 write_block 操作。APM 同样也会定义不同微操作之间的顺序,譬如所有在 sync 之后的操作都必须排在 sync 的后面。

有了微操作以及顺序,ALICE 会先将应用程序的初始化 snapshot 表示成逻辑实体,然后会选择满足顺序关系的不同集合的微操作对初始状态进行应用,这样就会构造出一个 crash state。对于每个 crash state,ALICE 会将逻辑实体重新转换成对应的实际文件,然后使用 checker 进行检查。

例子

这里说一下 ALICE 提供的一个 toy 例子,比较简单,去掉了 assert 判断:

int fd = open("tmp", O_CREAT | O_RDWR, 0666);
int ret = write(fd, "world", 5);
ret = close(fd);
ret = rename("tmp", "file1");
printf("Updated\n");
ret = link("file1", "link1");
ret = link("file1", "link2");

这个例子会做几件事情:

  1. 打开 tmp 文件,会写入 “world”,然后将 tmp 文件改名。当打印 “Updated” 之后,这个操作就完成,文件内容就必须是 “world”。
  2. 设置 link,link1 或者 link2 要不存在,要不不存在。

首先我们执行 workload,也就是 toy_workload.sh 脚本,该脚本会编译 toy 测试用例,创建两个目录,一个是 workload_dir,用来放置操作的文件,我们会创建一个 file1 文件,写入 "hello"。另一个就是 traces_dir,用来保存 ALICE trace 的信息。然后执行 alice-record --workload_dir . \ --traces_dir ../traces_dir \ ../a.out

然后我们就执行 check,对应的 check 主要判断逻辑如下:

if 'Updated' in stdout:
    # Check durability
    assert open('file1').read() == 'world'
else:
    # Check atomicity
    assert open('file1').read() in ['hello', 'world']

# Check whether link1 and link2 were created together as a single atomic unit
dirlist = os.listdir('.')
assert ('link1' in dirlist) == ('link2' in dirlist)

如果控制台打印出来 “Updated”,那么文件一定有 “world”,否则就可能是原来的文件内容 “hello”,或者新的 “world”。Link1 和 Link2 要不都存在,要不都不在。

然后我们执行 check,alice-check --traces_dir=traces_dir --checker=./toy_checker.py,首先会将应用对应的逻辑操作打印出来:

Parsing traces to determine logical operations ...
Logical operations:
0   creat("tmp", parent=200440197, mode='0666', inode=200446770)
1   append("tmp", offset=0, count=5, inode=200446770)
2   rename(dest="file1", source="tmp", source_hardlinks=1, source_parent=200440197, dest_size=5, dest_hardlinks=1, source_size=5, dest_parent=200440197, source_inode=200446770, dest_inode=200440198)
3   stdout("'Updated\n'")
4   link(dest="link1", source="file1", source_parent=200440197, dest_parent=200440197, source_inode=200446770)
5   link(dest="link2", source="file1", source_parent=200440197, dest_parent=200440197, source_inode=200446770)

然后根据 check 来检查易脆性:

Finding vulnerabilities...
(Dynamic vulnerability) Across-syscall atomicity, sometimes concerning durability: Operations 4 until 5 need to be atomically persisted
(Static vulnerability) Across-syscall atomicity: Operation toy.c:19[main] until toy.c:21[main]
(Dynamic vulnerability) Ordering: Operation 1 needs to be persisted before 2
(Dynamic vulnerability) Ordering: Operation 2 needs to be persisted before 3
(Static vulnerability) Ordering: Operation toy.c:12[main] needed before toy.c:16[main]
(Static vulnerability) Ordering: Operation toy.c:16[main] needed before toy.c:19[main]
(Dynamic vulnerability) Atomicity: Operation 2(destination unlinked fully, source untouched, destination and source unlinked fully, destination unlinking partial  semi-truncated (3 count splits), destination unlinking partial , destination unlinking partial fully truncated)
(Static vulnerability) Atomicity: Operation toy.c:16[main] (destination unlinked fully, source untouched,destination unlinking partial fully truncated,destination and source unlinked fully,destination unlinking partial ,destination unlinking partial  semi-truncated (3 count splits))
Done finding vulnerabilities.

可以看到,在第二个操作 rename 之前,操作 1 必须持久化。

小结

上面只是对 ALICE 的一个简单介绍,当然ALICE 还有很多限制,但对于一些常用的操作,还是能检查出来了。后面还是需要再抽时间详细研究一下。

下一步自然就是想到将 ALICE 应用到我们自己的系统中来,无论是 RocksDB,还是我们其他很多的文件操作,我们都可以使用 ALICE 来看系统的 crash consistent。如果你对这块感兴趣,欢迎联系我,[email protected]

你可能感兴趣的:(文件系统天生就是不平等的 - 实现崩溃一致性应用的复杂性)