Finding Crash-Consistency Bugs with Bounded Black-Box Crash Testing

Abstract

我们提出了一种测试文件系统崩溃一致性的新方法:有界黑盒崩溃测试(bound black-box crash testing,简称B3)

B3用黑盒的方式测试文件系统,使用文件系统操作作为负载。由于可能的负载空间是无限的,B3限定了空间,基于参数,例如文件系统的操作数量或者包含什么操作,并且在有限的负载空间中进行穷尽。

每个工作负载都在目标文件系统上被测试,通过模拟断电崩溃,并检测文件系统在每次crash是否恢复到了一个正确的状态。

B3建立我们在过去五年,linux文件系统中报告的崩溃一致性错误的研究见解上。

我们发现大多数报告的错误报告,都可以在新创建的文件系统上使用三个或更少的文件系统操作、小工作负载来重现,而且所有报告的错误都是由于fysnc()相关系统调用后的崩溃造成的。

我们构建了两个工具:CrashMonkey和ACE,来证明这种方法的有效性。

我们的工具可以发现在过去五年中,26个崩溃一致性BUG中的24个。我们的工具也在广泛使用的成熟的linux文件系统中发现了10个新的崩溃一致性错误,其中7个自2014年以来就存在与内核之中。

新的错误会导致严重的后果,例如重命名原子性中断和持久文件丢失。

介绍

一个文件系统如果总是能够在一次由断电或者kernel panic导致的崩溃后恢复到一个正确的状态,那么他就是崩溃一致性(crash consistent)的。

文件系统的状态是正确的,如果文件系统的内部数据结构是一致的,并且在崩溃之前保留的文件没有丢失或者被破坏。开发者们在2009年向ext4文件系统中加入了延迟分配功能,他们引入了一个崩溃一致性错误,导致了广泛的数据丢失1

考虑到崩溃一致性错误的潜在后果和即使专业的数据中心也会偶尔遭受电力中断这样的事实,保持文件系统的崩溃一致性是非常重要的。

不幸的是,对于目前广泛使用的Linux文件系统,今天为止还几乎没有崩溃一致性测试。

当前linux社区的做法是不执行任何主动崩溃一致性测试。如果一个用户报告了一个崩溃一致性错误,开发者将被动的编写一个测试来捕获这个错误。

linux文件系统开发者使用xfstest,一个特定的正确集合,来执行回归测试。

xfstest包含了一共482个正确的测试,适用于所有的POSIX文件系统。这482个测试中,只有26个(5%)是崩溃一致性测试。因此,文件系统开发者没有一个简单的方法来系统性的测试他们的文件系统的崩溃一致性。

本文介绍了一种新的测试崩溃一致性的方法:有界黑盒崩溃测试。B3是一种黑盒测试方法:不需要修改文件系统的代码。B3通过穷尽生成工作负载在一个边界空间内,模拟一个在之执行例如fsync()操作后的崩溃,并且最终测试文件系统是否正确的恢复到了。

我们通过两个工具来验证B3:CRASHMONKEY和ACE。我们的工具可以发现过去五年中26个崩溃一致性错误中的24个,涵盖七个内核版本和三种文件系统。进一步的,B3的天然系统性让我们的工具发现了新的BUG:10个BUG在广泛使用的linux文件系统上,会导致严重后果,例如rename()不再是原子的,在fsync后文件消失。我们已经报告了所有的新bug,开发者已经提交补丁修复了四个,并且正在工作修复剩余的。

请注意如果没有这些工作负载的限制,B3是不可行的,因为你可以运行在无数的文件系统镜像,使用无数的工作负载。

选择崩溃系统,在持久点之后,是B3易于处理的关键之一

B3不探索由于一个文件系统操作中间产生的产生的bug,因为在这种情况下,文件系统保证未定义

此外,如果没有持久性点,B3不能可靠地假设存储上的文件系统状态已经被修改。

尽在持久性点后崩溃才会限制测试崩溃一致性所需的工作,并提供了明确的正确性标准:在崩溃之前成功持久化的文件和目录必须在崩溃中幸存下来,并且不会损坏。

B3通过一些其他的方式限制了工作负载空间。首先,B3限制了工作负载中文件系统操作的数量,并且仅在持久化点之后模拟崩溃。第二,B3限制了文件和目录,作为工作负载中文件系统操作的参数。最后,B3将系统的数值状态限制为一个小型的新文件系统。

同时,这些边界极大的缩小了可能的工作负载的空间,允许CRASHMONKEY和ACE穷尽的生成测试工作负载。

只有当我们能够自动的、高效的检测任意工作负载的崩溃一致性时,B3这样的方法才是可行的

我们构建了CRASHMONKEY,一个框架来模拟在工作负载执行中的崩溃并且测试恢复后的文件系统镜像上的一致性。CRASHMONKEY首先配置一个给定的工作负载,捕获工作负载上的所有IO结果。然后,它重播IO请求,知道持久性点创建新的文件系统映像,我们称之为崩溃状态。在每一个持久性点,CRASHMONKEY也会捕捉一份已经显式持久化的文件和目录的快照(因此应该在崩溃中幸存下来)

CRASHMONEK接着挂载一个文件系统,在每个崩溃状态,允许文件系统恢复,使用它自己的细粒度检查来验证是否持久化数据和元数据是可用并且正确的。

我们构建了自动崩溃器(Automatic Crash Explorer(ACE))来穷尽生成工作负载(给定的用户约束和文件系统语义)。ACE首先生成一个文件系统操作序列:例如,一个link()跟着一个rename()。下一步,ACE向每个文件系统操作中填充参数。

现在文件系统是复杂的,在内存中保留大量与元数据相关的数据结构。例如,btrfs使用B+树来组织它的元数据。对这些数据结构的修改积累在内存中,并且被写入存储不管是fsync()或者通过一个后台线程。

**
开发者可能犯两个普遍类型的错误,在持久化这些内存中结构的时候,这将直接导致崩溃一致性错误。
**

第一个是忽视更新数据结构的某些字段。例如,btrfs有个bug,文件inode中的决定它是否应该被持久化的字段没有更新。作为结果,fsync()对于这个文件没用,导致数据在crash时丢失2

第二个是持久化时,不当的排序数据和元数据。例如,在ext4中引入延迟分配时,用重命名来原子的更新文件的应用丢失了数据,因为rename可以持久化,在文件的新数据之前。1

尽管事实上,这两个例子中,错误导致崩溃一致性错误时非常困难的,基本的问题的,一些内存中的状态需要被正确的恢复,没有写到磁盘中

1 create foo
2 link foo bar
3 sync
4 unlink bar
5 create bar
6 fsync bar
7 CRASH!

表1:一个崩溃一致性错误例子
这个表展示了工作负载暴露了一个崩溃一致性BUG,在2018年二月份在btrfs文件系统中被报告3。这个bug导致文件系统变成无法挂载了。

**Posix和文件系统保证。**名义上,Linux文件系统实现了POSIX API,提供POSIX标准中规定的保证。不幸的是,POSIX是机器模糊的。例如,在POSIX标准下,是合法的,fsync()不使数据耐用4

Mac OSX利用了这种合法性,要求用户采用fcntl(F FULLFSYNC)来使数据耐用。作为结果,文件系统通常提供超出POSIX标准要求的保证。

例如,在ext4上,持久化一个新文件,还想持久其目录条目。不幸的是,这些保证在不同的文件系统中各不相同,所以我们联系过各个文件系统的开发者,来保证我们测试的是他们寻求提供的保证。

一个崩溃一致性错误的例子。表1展示了一个btrfs中的一个崩溃一致性错误,导致文件系统在crash后变成不可挂载的(不可用)。解决这个bug需要文件系统使用btrfs-check进行修复。对于非专业用户来说,这需要开发者的指导。

这个bug导致了在btrfs上,因为unlink影响了两个不同的数据结构,如果发生崩溃,他们将变得不一致。

**为什么测试崩溃一致性是重要的。**文件系统研究者们这在研发新的崩溃一致性技术[13][14][46],设计新的文件系统来提升性能[1][5][21][23][50][54][68][^69]。同时,linux文件系统例如btrfs,包含了大量的优化,影响了IO请求的顺序,因此,崩溃一致性。然后,崩溃一致性是微妙的,并且难以做对,并且一个错误可能导致静默的数据损坏和数据丢失。因此,有机会影响崩溃一致性的地方应该被仔细的测试。

**当今的崩溃一致性测试状态。**xfstest[^16]是一个回归测试套,来检查文件系统的正确性,包含了一小部分(5%)崩溃一致性测试。这些测试是避免随着时间的推移复发bug,但是没有识别到这些bug的变体。此外,这些测试用例的每一个都要求开发者写一个检查器,来表述文件系统崩溃后正确的行为。给出无穷的工作负载空间,手工制作能够揭示bug的负载是及其苦难的。这些因素导致xfstests识别新的崩溃一致性错误是不足的。

3 学习崩溃一致性错误

我们展示了一个分析,26种不同的崩溃一致性错误,被用户报告,在过去五年被广泛使用的linux文件系统中[^58]。[-todo-]

由于崩溃一致性错误的特性(所有内存中的信息都在crash中被丢失),将他们绑定成一个特定的工作负载是困难的。作为结果,bug报告的数量是比较少的。我们相信有许多未报告的崩溃一致性错误。

我们分析这些报告中的bug,内核版本,文件系统,以及复现他们所需要的操作数量。有26个不同的bug,在ext4、F2FS和btrfs中传播。每个不同的bug需要一个单独的集合(文件系统操作)来复现。两个错误发生在两个文件系统(F2FS和ext4、F2FS和btrfs),导致了28个BUG。

表1显示了一些有关崩溃一致性错误的统计信息。这个表展示了报告BUG的内核版本。如果一个bug没有被报告版本,它将被呈现在最近的B3可以复现bug的内核版本。bugs有验证的结果,范围从文件系统崩溃到文件系统变得不可挂载。最常见的四个在崩溃一致性bug中出现的文件系统操作是write()link()unlink()rename()。大多数被报告的bug是由于在多个文件系统中重用文件名或者对文件区域进行重叠写入导致的。大多数的bug可以被3个或者更少数量的文件系统操作复现。

例如,表2展示了一些崩溃一致性bug。BUG #15涉及创建两个文件在一个目录中,仅持久化其中的一个。btrfs日志恢复错误地计算了目录大小,导致这个目录不能再移动。BUG #26涉及创建一个硬链接到一个已经存在的文件。一个崩溃导致btrfs恢复文件成0大小,从而导致数据不可得。一个类似的bug(#5 7)体现在ext4的直接写入路径中,写入成功并且block被分配了,但是文件大小被错误的更新为0,导致数据丢失。

复杂导致BUG。
ext4文件系统已经经历了15年的开发了,结果是只有2个BUG。btrfs和F2FS文件系统更新:btrfs在2007年被介绍,F2FS在2012年被介绍。被别是btrfs,是一个极度复杂的文件系统,提供快照、克隆、带外重复数据删除和压缩等功能。btrfs管理元数据(例如inides和bitmaps),以各种copy-on-write的B+树的形式。这使得实现崩溃一致性很棘手,因为更新必须传播到几个树。因此,btrfs报告了最多的崩溃一致性bug是不足为奇的。未来的文件系统会变得更加复杂,我们希望看到崩溃一致性错误相应增加。

111
表2:崩溃一致性错误的例子,表展示了过去五年间被报告的一些崩溃一致性错误。这些bug导致了严重的错误,从丢失用户数据到使得目录无法重新移动。

崩溃一致性错误是非常困难去找到的
尽管事实是,我们检查的文件系统时被广泛应用的,一个bug仍然隐藏了很多年。例如,btrfs有一个崩溃一致性问题,一直隐藏了7年。这个bug是由于错误的处理btrfs的数据结构硬链接导致的。当添加了一个硬链接时,目录项被添加到一个数据结构中,同时inode被添加到另外一个数据结构。当crash发生时,这些数据结构中只有一个可以被正确的恢复,导致目录包含一个不可移除的硬链接8。这个bug在2008年日志树被添加后展现出来,但是这个bug直到2015年才被发现。

系统性的测试是需要的
硬链接的bug一被发现,开发人员就迅速修复了它。但是,他们只修复了一个导致该BUG的代码路径。相同的bug可能被另外一个代码路径触发,在原始bug被报告四个月后,事实被发现。虽然原始的工作负载需要创建硬链接并虚体且在袁术文件和父目录调用fsync(),这个只需要调用fsync(),在硬链接被创建的兄弟目录9。对文件系统进行系统性的测试,可以发现bug可能在其他的备选路径。

小工作负载就可以在空文件系统上复现bug
大多数被报告的bug不需要一个特殊的文件系统镜像或者巨量的文件系统操作来复现。26个被报告的bug中的25个bug只需要三个或者更少的文件系统操作来在一个空的文件系统上复现。数量很少因为我们没有计算以来的操作。例如,一个文件需要在rename之前存在、一个目录需要存在,在在它里面创建一个文件之前。给出核心文件操作系统,这样的以来型操作可以被推断出来。其余的两个bug,一个需要一个特殊的命令(dropcaches),在工作负载运行的过程中。另一个需要一个特殊的配置:3000个硬链接存在(强制外部引用)。

报告的错误涉及持久化之后的崩溃
所有被报告的错误涉及一次崩溃,刚好在持久化点之后:一次fsync()fdatasync()调用,或者一个全局的sync命令。这些命令是重要的因为文件系统操作只会默认修改内存中的元数据和数据。只有持久化点 才会真正改变存储中文件系统的状态。因此,除非一个文件或者目录被持久化了,否则不能期望它在一次crash中幸存下来。尽管crash在技术上可以被发生于任何一个点,但用户不能抱怨一个未持久化的文件在一次crash后丢失。因此每一个崩溃一致性错误涉及持久化的数据或者元数据在一次crash后被bug影响,一个负载不含有持久化点,不能复现bug。这同样是一个有效的方法来发现崩溃一致性bug:执行一个文件系统操作序列,通过fsync()或者类似的调用改变存储上文件系统状态,崩溃,然后检查之前已被持久化的文件和目录。

4 B3有界黑盒崩溃测试

基于我们研究崩溃一致性bug的观点,我们介绍了一种新的测试文件系统崩溃一致性的方法:有界黑盒崩溃测试(B3)。B3是一种黑盒测试方法,基于于大量崩溃一致性错误报告的见解,可以通过在新的文件系统上系统的测试一些小的文件系统操作序列来发现bug。B3通过system-call API来练习文件系统,观察文件系统行为,通过read和write IO。作为结果,B3不需要批注或者修改文件系统源代码。

4.1 概述

B3生成文件系统操作序列,叫做工作负载。由于可能的工作负载空间是无限的,B3限制了工作负载边界,基于研究的观点。在决定的边界之内,B3尽可能的生成,并且测试所有可能的工作负载。每个工作负载都被测试,通过模拟一个崩溃,在持久化点之后,并且检查文件系统是否恢复到了一个正确的状态。B3执行细粒度的正确检查,在被恢复的文件系统状态上,只有被明确持久化的文件和目录会被检查。B3检查文件和目录数据和元数据(大小,链接数量、块数量)的一致性。

崩溃点
研究中的主要观点是,B3这种方法的可行性是崩溃点的选择,一个崩溃被模拟,仅在工作负载中的持久化点之后,而不是在文件系统操作中。这种设计选择的因素有两个。首先,文件系统保证未定义的,如果崩溃发生在文件系统操作中间;仅有在之前被成功持久化的文件和目录会在crash中幸存,文件系统开发者负担过重,bug涉及数据或者元数据未被明确持久化的被给出了低优先级(有些时候不被承认是一个bug)。第二,如果我们在一个在操作中间crash,就会有很多个正确的状态可以被文件系统恢复。如果一个文件系统操作传输n个块IO请求,就会有2n个不同的磁盘崩溃状态如果我们在才做中的任何时刻crash。限制在持久点之后发生崩溃限制了工作负载于操作数量的线性关系。一小组的崩溃点和正确状态使得自动化测试更简单,我们对于崩溃点的选择自然会导致持久化的数据和元数据被破坏或者丢失,文件系统开发者有足够的动力来修复这样的bug。

4.2 B3中使用的边界

基于我们对于崩溃一致性bug的研究,B3限制了可能的工作负载空间用下面这些方法:

  1. **操作的数量。**B3限制了工作负载中文件系统操作的数量(称为队列长度)。一个seq-X工作负载拥有X个核心的文件系统操作。没有算上依赖操作,例如在重命名之前创建一个文件
  2. **工作负载中的文件和目录。**我们发现在报告的bug中,错误是由于一个小的文件元数据操作集引起的。因此,B3限制工作负载每个目录使用少量文件,并且目录深度较低。这个限制自动的减少了元数据相关操作的输入,例如rename()
  3. **数据操作。**研究表明,有关数据一致性的bug主要发生在对文件范围的覆盖写。在大多数情况下,bug不依赖于确切的offset和write中使用的length,但是关于写入重叠区域之间的相互作用。研究表明,写的广泛作用,例如追加到文件末尾,覆盖写文件的重叠区域,对于找出崩溃一致性错误是有效的。
  4. **初始化文件系统状态。**研究中分析的大多数bug不需要一个特定的初始文件系统状态(或者一个大文件系统)来复现。而且,大多数研究的bug可以被复现,在相同的、小的文件系统镜像上。因此B3可以测试从初始文件系统状态开始的所有工作负载。

4.3 细粒度的正确性检查

B3使用细粒度的正确性检查来验证被持久化的文件和目录在每一个崩溃状态的数据和元数据。由于fsck既耗时,又可能导致数据丢失/损坏,它不适用于B3检查器。

4.4 限制

B3方法有很多的限制:

  1. B3不保证发现所有的崩溃一致性bug。然而,因为B3详尽的测试,如果触发bug的工作负载落在了给定的工作负载区间内,B3会发现他。因此,B3的有效性取决于边界的选择和测试工作负载的数量
  2. B3集中于一类特定的bug。它不会再文件系统操作中间模拟崩溃丙炔不会调整IO请求的顺序来创建不同的崩溃状态。一个隐含的假设是,核心崩溃一致性机制,例如日志10或者写时拷贝[20][52],是正确工作的。相反,我们假设其余的文件系统都有bug。崩溃一致性错误的研究表示这个假设是有原因的
  3. B3专注于文件和目录被显式持久化的工作负载。如果我们创建了一个文件,等待一个小时,然后崩溃,发现文件消失,在文件系统恢复后,那么我们就发现了一个崩溃一致性错误。但是B3不探索此类工作负载,因为他们需要大量的运行时常,且不容易以确定性的方式复现
  4. 由于是天然的黑盒,B3不能无法确定导致观察到的错误的具体代码行。一旦B3发现了一个错误后,需要进一步确认根因。但是,B3有助于调查错误的根本原因,因为它提供了一种确定性方式重现错误的方法

在这里插入图片描述
图2:系统架构。给出探索边界,ACE生成一个工作负载集。每个工作负载都会被喂给CRASHMONKEY,产生了一组崩溃状态和对应的预言。AutoChecker比较每个oracle/crash中的持久文件状态对,不匹配表示存在错误。

尽管有其缺点,但我们相信B3是对测试文件系统崩溃一致性错误的有用技术。

5 CrashMonkey and Ace

我们通过CrashMonkey 和 Ace搭建两个工具来验证B3。如图2所示,CrashMonkey负责在给定工作负载的不同点模拟崩溃,并测试文件系统在每次模拟崩溃后是否正确恢复。而自动崩溃浏览器(ACE)负责在有限的空间内最大限度地生成工作负载。

5.1 CrashMonkey

CrashMonkey使用record-and-replay技术在工作负载中间段模拟崩溃,并测试文件系统是否在崩溃后恢复到正确的状态。为了最大的可移植性,CrashMonkey像一个黑盒子一样处理文件系统,只要求文件系统实现POSIX API的功能。
在这里插入图片描述
图3:CRASHMONKEY操作。CM首先工作负载传来的记录块IO请求,在每个持久化点之后捕获称为oracles的镜像。CM然后生成崩溃状态,通过重放IO请求,并且测试与oracle的一致性。

**概述。**CrashMonkey的工作分为如图3所示的三个阶段。在第一个阶段,CrashMonkey通过收集工作负荷期间的所有文件系统操作量和IO请求量来配置工作负载。第二阶段,重放IO请求直到可以创建一个崩溃状态的持久点。这个崩溃状态是系统在持久性操作完成后崩溃的存储状态,CrashMonkey以崩溃状态运行文件系统并允许文件系统执行恢复。在每个持久点,CrashMonkey还捕获一个引用文件系统映像,称为oracle,通过安全的卸载它,以便于文件系统完成任何挂起操作和检查点。Oracle代表文件系统崩溃后的预期状态。从bug的角度来看,持久化文件在oracle中应该是一致的,并且回复后的崩溃状态也应该是相同的。在第三阶段,CrashMonkey的AutoChecker通过比较oracle中的持久化文件及目录与恢复后的崩溃状态来测试正确性。

CrashMonkey由两个内核模块和一组用户空间实用程序来实现。其核心模块由1300行可编译的C语言代码组成并在运行时插入内核,从而避免长时间重载(从新编译)。用户空间实用程序由4800行C++语言代码构成。CrashMonkey将内核模块和用户空间利用率分离,允许快速移植到不同的内核版本,只有内核模块需要移植到目标内核。这就允许我们移植CrashMonkey到7种内核并复现在$3中研究的bug。

配置工作负载。CrashMonkey在两级存储栈上配置工作负载:它记录IO块请求,并记录系统调用。它使用两个内核模块来记录IO块请求并创建崩溃状态与(oracles)。

第一个内核模块记录所有通过使用包装块设备工作负载生成的IO请求。包装块设备记录IO请求的数据和元数据(如扇区号、IO号和标志)。工作负载中的每个持久性点都会导致一个特殊的检查点请求被插入到记录的IO请求流中。检查点只是一个带有特殊标志的IO空载请求,用于关联持久性操作的完成与低级IO流块。包装设备记录的所有数据都通过ioctl调用与用户空间实用程序通信。

CrashMonkey中的第二个内核模块是一个内存中的、写时拷贝的块设备,它有助于快照。CrashMonkey创建一个文件系统的快照在开始分析阶段之前,它表示基本磁盘映像。CrashMonkey提供了快速的、可写的快照,它通过在基本磁盘映像上重放分析期间记录的IO来生成崩溃状态。快照还保存在工作负载中的每个持久性点,以创建oracle。更进一步的,由于快照是写时拷贝的,重置一个快照到基础镜像意味着删除修改后的数据块,很高效。

CrashMonkey还记录所有open()、close(),在工作负载中调用fsync()、fdatasync()、rename()、sync()和msync(),以便在工作负载执行fsync(fd)等持久性操作时,CrashMonkey能够将fd与先前打开的文件关联起来。这允许CrashMonkey跟踪在工作负载的任何一点显式持久化的文件和目录集。CrashMonkey的AutoChecker使用此信息来确保只比较在工作负载的给定点显式持久化的文件和目录。CrashMonkey使用自己的一组函数包装系统调用,这些调用操作文件来记录所需要的信息。

构建崩溃状态。要创建崩溃状态,CrashMonkey从文件系统的初始状态(在运行工作负载之前)开始,并使用类似于dd的实用程序重放从工作负载开始到IO流中下一个检查点的所有记录的IO请求。最终崩溃状态表示存储设备上与持久性相关的调用刚刚完成之后的存储状态。由于IO流重放直接在流中的下一个持久性点之后结束,因此生成的崩溃点表示一个文件系统状态,该状态被认为是不干净的卸载状态。因此,当文件系统再次装载时,内核可能会运行特定于文件系统的恢复代码。

自动测试正确性。CrashMonkey的AutoChecker能够自动测试正确性,因为它有三个关键信息:它知道哪些文件被持久化了,它在oracle中拥有这些文件的正确数据和元数据,它拥有恢复后处于崩溃状态的相应文件的实际数据和元数据。测试正确性只是比较oracle和崩溃状态中持久化文件的数据和元数据。

CrashMonkey避免使用fsck,因为它的运行时间与文件系统中的数据量成比例(而不是更改的数据量),并且它不会检测用户数据的丢失或损坏。相反,当一个崩溃状态被重新装载时,CrashMonkey允许文件系统运行它的恢复机制,比如journal replay,它通常比fsck更轻量级。仅当恢复的文件系统不可装载时,才会运行fsck。为了检查一致性,CrashMonkey在恢复后使用自己的读写检查。CrashMonkey使用的读取检查确认持久化的文件和目录被准确地恢复。如果一个bug导致无法修改文件或目录,则write检查测试。例如,btrfs bug由于文件句柄过时而使目录无法移动5

由于每个文件系统的一致性保证略有不同,我们联系了测试每个文件系统的开发人员,以了解该文件系统提供的保证。在某些情况下,我们的谈话促使开发人员第一次明确地写下文件系统的持久性保证[57]。在这个过程中,我们确认大多数文件系统(如ext4和btrfs)实现了比POSIX标准更强大的保证。例如,虽然POSIX要求在新创建的文件及其父目录上都要有fsync(),以确保崩溃后文件的存在,但许多Linux文件系统不需要父目录的fsync()。根据开发人员的反馈,我们报告了违反每个文件系统旨在提供的保证的错误。

5.2 Automatic Crash Explorer (Ace) 自动崩溃浏览器

ACE生成满足给定边界的工作负载。ACE有两个组件,工作负载合成器和CrashMonke适配器。

工作负载合成器。工作负载合成器在由用户指定的边界定义的状态空间内以穷尽方式生成工作负载。此阶段中生成的工作负载用高级语言表示,类似于图4所示的语言。
在这里插入图片描述
图4:ACE工作负载的生成。展示了不同阶段。给出序列长度,ACE首先选择操作,然后选择参数给每个操作,接着选择性的在每次操作后添加持久点,最后满足文件系统的依赖关系。最终的工作负载长度比初始序列长度要长很多。

**CM适配器。**自定义生成器将工作负载转化为CM可以使用的等效C++测试文件。适配器处理CM跟踪的包装的文件系统操作的插入。此外,他在每一个持久点插入一个特殊的系统调用,转换为检查点IO。很容易扩展ACE和其他记录/重放工具一起所能够来构建适配器,例如dm-log-writes11.

表格3展示了如何使用我们的研究,在运行ACE时来为B3的边界分配特定的值。鉴于这些界限,ACE使用了多阶段过程来生辰工作负载,然后将其输入CM。图4说明了ACE的四个工作阶段,通过生成seq-2工作负载。

**阶段1:选择操作并生成工作负载。**ACE首选为给定的序列长度选择文件系统操作,我们称之为骨架。默认情况下,文件系统操作可以在工作负载种被重复,用户还可以提供边界,例如只需要使用文件系统操作的子集(例如,集中在测试新的操作)。然后ACE详尽的生成满足给定界限的工作负载。例如,如果用户指定了seq-2工作负载,只能包含6个文件系统操作,ACE将在第一阶段生成6*6=36个骨架。

**阶段2:选择参数。**对于每个被在阶段一中生成的骨架,ACE为每个文件系统操作选择参数(系统调用参数)。默认的,ACE使用两个顶层文件和两个子目录,每个子目录有两个文件作为参数,来进行元数据相关操作。ACE也了解文件系统操作的语义,并利用它消除对工作负载的产生。例如,考虑两个操作link(foo,bar)和link(bar,foo)。这个想法时在同一个目录下链接两个文件,但是文件名字的顺序无关紧要,在这个例子中,一个文件操作将被舍弃,来减少测序序列的工作负载。

对于数据操作,ACE的写操作在文件开头、中间还是结尾、覆盖,还是简单的追加操作之间进行选择。此外,由于我们的研究表明当数据操作重叠时会出现崩溃一致性错误,因此ACE尝试重叠第二阶段的数据操作。

每个在第一阶段被生成的骨架,都会导致第二阶段的多个工作负载(基于不同的参数)。但是,在此阶段结束,工作负载具有了一系列文件系统操作确定的所有点。

阶段3:添加持久化点。 ACE选择性的在每个文件系统操作之后添加持久点,但是ACE并不要求每个操作都是持久点。但是,ACE确保始终遵循工作负载中的最后一个操作有一个持久化点,这样它就不会被截断到较低序列长度的工作负载。

**阶段4:添加依赖。**最后,ACE满足各种依赖关系以确保工作负载可以在posix文件系统工作。例如,一个被重命名或者写入的文件必须存在。同样的,如果涉及对其文件的任何操作,目录必须被创建,图4显示了A、B和A/foo是如何创建的。作为结果,一个seq-2工作负载可以有两个以上的文件系统操作,在最终的工作负载中。在这个阶段结束时,ACE将其转换为一个CM可以用的C++程序。

执行。 ACE 由 2500 行 Python 组成代码,目前支持 14 种文件系统操作。我们研究中分析的所有错误都使用了这 14 个文件系统操作之一。未来将 ACE 扩展到支持更多操作。

**以轻松的边界运行 Ace。**很容易放宽 ACE 使用的边界以生成更多工作负载。这是以用于测试的计算时间为代价的额外工作负载。放宽界限时应注意,工作负载的数量增加以很快的速度。例如ACE生成大约1.5M具有三个核心文件系统操作的工作负载。放宽文件和目录集来增加一个深度,将导致工作负载增长至3.7M。这个简单更改导致 2.5 倍多的工作负载。

5.3 测试和错误分析

测试策略

给定一个目标文件系统,我们首先使用 CRASHMONKEY。详尽地生成 seq-1 工作负载并对其进行测试。然后我们继续 seq-2,然后然后是 seq-3 工作负载。按此顺序生成和测试工作负载,CRASHMONKEY 只需要在每个工作负载的一个点模拟崩溃。例如,如果 seq-2 工作负载有两个持久点,在第一个持久点之后崩溃将是等效的,因为已经探索过的 seq-1 工作负载。

分析错误报告

像 B3 这样的黑盒方法的挑战之一是,一个错误可能会导致许多不同的工作负载未能通过正确性测试。我们介绍了工作负载中多次测试失败的两种情况,以及我们如何缓解它们。

首先,不同顺序的工作负载可能会因为相同的错误而失败。我们设计的测试策略为了减轻这种情况:如果单个文件系统操作的错误导致不正确的行为,它应该被一个seq-1 工作负载捕捉。因此,如果我们只在 seq-2 工作负载中捕获一个错误,这意味着错误的结果来自两个文件系统操作的交互。理想情况下,我们将运行 seq-1,报告任何错误,并在运行 seq-2 之前应用开发人员提供的错误修复补丁。然而,为了更快的测试,ACE 维护一个所有以前发现的错误的数据库,包括产生了每个错误和错误的后果的核心文件系统操作。对于所有由生成的新错误报告CRASHMONKEY 和 ACE,它首先比较工作量以及已知错误数据库的后果。如果有匹配项,ACE 不会向用户报告错误。

其次,相同顺序的相似工作负载可以由于相同的错误,导致测试失败。为了高效分析,我们根据后果(例如,文件丢失)将错误报告组合在一起,和骨架(序列构成工作负载的核心文件系统操作的数量)触发了错误,如图 5 所示。
在这里插入图片描述
图5:后期处理。该图显示了如何处理生成的错误报告以消除重复项。

使用骨架而不是完全充实的工作负载使我们能够识别类似的错误。例如,导致附加数据丢失的错误将重复四次,对文件集中的每个文件进行一次。我们可以将这些错误报告组合在一起,只检查一个来自每个组的错误报告。在验证了每个错误之后,我们将其报告给开发人员。

6 评估

我们通过回答以下问题来评估 B3 方法的效用和性能:

  • CRASHMONKEY 和 ACE 是否找到已知的错误和在合理的时间内 Linux 文件系统中的新错误? (x6.2)
  • CRASHMONKEY的性能如何? (x6.3)
  • ACE的性能如何? (x6.4)
  • CRASHMONKEY 需要多少内存和CPU消耗? (x6.5)

6.1 实验装置

B3 需要以系统的方式测试大量工作负载。为了完成这个测试,我们在Chameleon Cloud12上部署CM,一个用于大规模计算的实验测试平台。

我们在 Chameleon Cloud 上使用了一个由 65 个节点组成的集群。每个节点有 40 个核、48 GB RAM 和 128 GB固态硬盘。我们安装了 12 个 VirtualBox 虚拟机运行每个节点上的 Ubuntu 16.04 LTS,每个节点具有 2 GB RAM和 10 GB 存储空间。每个虚拟机运行一个 CRASHMONKEY 实例。因此,我们总共有 780使用 CRASHMONKEY 测试工作负载的虚拟机在平行下。我们发现我们被每个物理节点可用的存储限制为 780 个虚拟机。

在本地服务器上,我们使用 ACE 生成工作负载并将它们分成要测试的工作负载集在每个虚拟机。然后我们复制工作负载到每个物理Chameleon Cloud节点的网络,并且,从每个节点,将它们复制到虚拟机。

6.2 缺陷发现

确定工作量。我们的目标是测试B^3方法是否有用和实用,而不是彻底地找到每个崩溃一致性bug。因此,我们希望将用于测试的计算时间限制为几天。因此,我们需要用计算机计算的预算来确定要测试的工作量。

我们对崩溃一致性bug的研究表明,测试长度为1、2和3的小工作负载是有用的。然而,我们估计在我们的目标时间框架内测试2500万个长度为3的可能工作负载是不可行的。我们必须进一步限制测试的工作负载集。我们用我们的研究来指导我们完成这项任务。至少,我们希望选择能够生成重现报告的bug的工作负载的边界。以此为指导,我们提出了一组工作负载,这些工作负载足够广泛,可以重现现有的bug(并可能发现新的bug),但是足够小,我们可以在几天内于我们的研究集群上测试工作负载。

工作负载。我们测试长度为1(seq-1)、2(seq-2)和3(seq-3)的工作负载。我们进一步将长度为3的工作负载分为三组:一组侧重于数据操作(seq-3-data),一组侧重于元数据操作(seq-3-metadata),另一组侧重于涉及深度为3的文件的元数据操作(seq-3-nested)(默认情况下,我们使用深度2)。

seq-1和seq-2工作负载使用14个文件系统为一组操作。对于seq-3工作负载,我们根据工作负载所属的类别缩小了操作列表。在每个类别中测试的文件系统操作的完整列表如表4所示。
在这里插入图片描述
表4:测试的工作负载。下表显示了每组测试的工作负载数,以及在65台物理机器上并行测试这些工作负载所需的时间,以及在每个类别中测试的文件系统操作。总的来说,我们在两天内测试了337万个工作负载,重现了24个已知的bug,并发现了10个新的崩溃一致性bug。

测试策略。我们在ext4、xfs、F2FS和btrfs上测试了seq-1和seq-2工作负载,但在ext4或xfs中没有发现任何新的bug。我们将重点放在更大的seq-3工作负载的F2FS和btrfs上。我们总共花了48小时在前面描述的65节点研究集群上测试每个文件系统的337万个工作负载。表4显示了每组中的工作负载数量,以及测试它们所用的时间(对于每个文件系统)。所有测试都只在4.16内核上运行。为了重现报告的bug,我们采用以下策略。我们对触发ACE中报告的工作负载进行编码。在工作负载生成过程中,当ACE生成与编码的工作负载相同的工作负载时,它被添加到一个列表中。此工作负载列表在表1中报告的内核版本上运行,以验证ACE生成的工作负载是否确实可以重现bug。

计算成本。我们相信找到崩溃一致性bug所需的计算工作量用CrashMonkey和ACE是合理的。例如,如果我们要在Amazon上租用780个t2.small实例来运行ACE和CrashMonkey 48小时,按目前的按需服务费率0.023美元/小时[2],则成本将为780480.023=861.12美元。对于完整的2500万工作量集,计算成本将增加7.5倍,总计640万美元。因此,我们可以用不到7000美元的价格测试每个文件系统。或者,公司可以提供物理节点来运行测试;我们相信这对一家大公司来说并不难。

结果。CrashMonkey和ACE在btrfs和F2FS中发现了10个新的crash-consistency bug[59],此外还复制了过去5年报告的26个bug中的24个。我们研究了新bug的bug报告,以确保它们是唯一的,而不是同一个底层bug的不同表现。我们验证了每个唯一的bug都会在内核中触发不同的代码路径,这表明每个bug的根本原因不是相同的底层代码。

在这里插入图片描述

表格5:新发现的buG。下表显示了CRASHMONKEY和ACE发现的新bug。这些错误会造成严重后果,从丢失分配的块到整个文件和目录消失。这些缺陷在内核中已经存在了好几年,这表明需要进行系统测试。请注意,即使是单文件系统操作的工作负载也会导致bug。开发人员已经提交了一个标记为*的bug补丁。

所有新的bug都向文件系统开发人员报告并确认四个新bug[11、12、43、44]。开发人员已经提交了四个bug的补丁[32、35、66、67],并且正在为其他bug开发补丁[34]。表5展示了CrashMonkey和ACE发现的新bug。我们根据这些结果进行了一些观察。

被发现的bug会带来严重的后果。新发现的bug会导致数据丢失(由于缺少文件或目录)或文件系统损坏。更重要的是,丢失的文件和目录已通过fsync()调用显式持久化,因此应该能够在崩溃中幸存下来。

小工作负载足以暴露新的bug。人们可能只期望具有两个或更多文件系统操作的工作负载暴露bug。然而,结果表明,即使是由单个文件系统操作组成的工作负载,如果进行系统测试,也会暴露出bug。例如,seq-1工作负载发现了三个bug,CrashMonkey和ACE只以系统方式测试了300个工作负载。有趣的是,这些bug的变体以前已经修补过了,只需将参数更改为文件系统操作,即可通过不同的代码路径触发相同的bug。

CrashMonkey和ACE发现的一个F2FS bug就是一个很好的例子,可以找到以前修补过的bug的变体。当fallocate()与KEEP SIZE标志一起使用时,先前修补的bug出现了;这会将块分配给一个文件,但不会增加文件大小。通过使用KEEP SIZE标志调用fallocate(),开发人员发现只有F2FS仅仅检查文件大小以查看一个文件是否已更新。由此得出,文件上的fdatasync()将没有结果。崩溃后,文件恢复到正确大小,因此不遵守KEEP size标志。此错误于2017年11月修补[65];但是,fallocate()系统调用还有几个标志,如零范围(ZERO RANGE)、穿孔(PUNCH HOLE)等,开发人员未能系统地测试系统调用的所有可能参数选项。因此,我们的工具发现并报告,当使用ZERO RANGE时,同样的bug也会出现。尽管开发人员最近修补了这个bug,但它提供了更多的证据表明当前崩溃一致性测试的状态是不够的,需要进行系统的测试。

崩溃一致性bug很难手动找到。CrashMonkey和ACE在4.16内核中的btrfs中发现了8个新的bug。有趣的是,自2014年发布的3.13内核以来,已经出现了7个这样的bug。我们的工具能够在两天内的一个中等规模的研究集群上进行测试后找到4年前的崩溃一致性 bug,这说明了手动查找这些bug的难度,以及像B^3的系统测试方法的好处。

破坏重命名原子性bug。ACE生成了几个工作负载,打破了btrfs的重命名原子性。工作负载包括首次创建和持久化一个文件,例如A/bar。接下来,工作负载创建另一个文件B/bar,并尝试用新文件替换原始文件A/bar。我们希望能够读取原始文件A/bar或新文件B/bar。但是,如果btrfs在错误的时间崩溃,它可能同时丢失A/bar和B/bar。虽然失去重命名原子性是不好的,但这个bug最有趣的部分是,在崩溃之前,必须对不相关的同级文件(如A/foo)调用fsync()。这表明,对于开发人员来说,揭示崩溃一致性bug的工作负载很难手动找到,因为它们并不总是涉及明显的操作序列。

6.3 CrashMonkey性能
CrashMonkey有三个操作阶段:分析给定的工作负载、构建崩溃状态和测试崩溃一致性。给定一个工作负载,生成bug报告的端到端延迟为4.6秒。主要的瓶颈是内核本身:装载文件系统需要一秒钟的延迟(如果CrashMonkey过早地检查文件系统状态,它有时会出错)。同样,一旦工作负载完成,我们还要等待两秒钟,以确保存储子系统已经处理了写入操作,并且可以在不影响写入操作的情况下卸载文件系统。这些延迟占分析所用时间的84%。

分析之后,构建崩溃状态的速度相对较快:CrashMonkey只需要20毫秒就可以构建每个崩溃状态。此外,由于CrashMonkey使用细粒度的正确性测试,因此检查读写测试的崩溃一致性仅需要20毫秒。

6.4 Ace性能
ACE在374分钟内生成了所有被测试(3.37米)的工作负载 (约等于每秒生成150个工作负载)。尽管成本很高,但值得注意的是,生成工作负载是一次性成本。一旦生成了工作负载,CrashMonkey就可以在不同的文件系统上测试这些工作负载,而无需重新进行任何配置。

将这些工作负载部署到Chameleon上的780个虚拟机需要237分钟:按虚拟机分组工作负载需要34分钟,将工作负载复制到Chameleon节点需要199分钟,将工作负载复制到每个节点上的虚拟机需要4分钟。
这些数字反映了单个本地服务器生成工作负载并将其推送到Chameleon所需的时间。通过使用更多服务器和采用更复杂的策略生成工作负载,我们可以减少生成和推送工作负载所需的时间。

6.5 资源消耗
CrashMonkey在10个随机选择的工作负载和三个序列长度上的平均总内存消耗为20.12MB。低内存消耗源于包装块设备的写时拷贝特性。由于ACE的工作负载通常会修改少量的数据或元数据,因此修改的页面数量很少,导致内存消耗较低。此外,CrashMonkey仅将持久性存储用于存储工作负载(每个工作负载480 KB)。最后,top报告的CrashMonkey的CPU消耗可以忽略不计(不到1%)

7 相关工作
B3在解决文件系统崩溃一致性的技术中提供了一个新的方案,同时还提供了经过验证的文件系统和检查模型。我们现在将B^3放在先前方法的上下文中。
已验证的文件系统。最近的工作聚焦于根据[8,9,53]规范创建新的、经过验证的文件系统。这些文件系统被证明具有强大的崩溃一致性保证。然而,文件系统所采用的技术对于测试现有的、广泛使用的Linux文件系统的崩溃一致性并没有用,这些文件系统是用C等低级语言编写的。B^3方法的适用目标是类似的不适合验证的文件系统。

标准碰撞一致性模型。铁氧体[6]将崩溃一致性模型形式化,并可用于测试文件系统中是否存在给定的排序关系;然而,很难确定要测试哪些关系。作者用铁氧体测试了一些简单的关系,比如前缀附加。另一方面,ACE和CrashMonkey探索更广泛的工作负载,并使用oracle和开发商提供的保证自动测试在崩溃后的正确性。

检查模型。B3与现场检查模型密切相关,如EXPLODE[63]和FiSC[64]。但是,B3与EXPLODE和FiSC不同,FiSC需要修改缓冲区缓存(以查看IO请求的所有顺序)并更改文件系统代码暴露选择点进行高效检查,这是一项复杂而耗时的任务。B3不需要更改任何文件系统代码,而且它在概念上比现场检查模型方法更简单,同时在查找方面仍然有效。

虽然B^3方法不具备验证的保证或检查模型的能力,但它的优点是易于使用(由于其黑盒特性),能够系统地测试文件系统(由于其穷尽特性),并且能够捕获成熟文件系统上发生的崩溃一致性bug。

Fuzz。B3 方法与模糊测试技术有一些相似之处,模糊测试技术探索的输入将暴露目标系统中的bug。模糊测试的有效性取决于对不常见的会触发异常行为的输入的仔细选择。但是,B3不会随机化输入选择。它也没有使用任何复杂的策略来选择要测试的工作负载。相反,B^3在我们研究的或用户提供的边界内穷尽地生成工作负载。虽然有模糊程序来测试系统调用[17,22,45]的正确性,但似乎没有模糊技术来暴露崩溃一致性bug。Nossum和Casasnovas[45]的工作与我们的工作最为接近,它们生成的文件系统映像可能会在文件系统的正常操作过程中暴露bug(非崩溃一致性bug)。
录制和回放框架。CrashMonkey类似于以前的录制和回放框架,如dm-log-writes[4]、Block Order Breaker[47]和work by Zheng et al [70]。与dm-log-writes不同,它需要手动地进行正确性测试或运行fsck,而CrashMonkey能够以高效的方式自动测试崩溃一致性。

与CrashMonkey类似,Block Order Breaker(BOB)[47]也从记录的IO请求创建崩溃状态。然而,BOB仅用于显示不同的文件系统用明显不同的方式做持久化文件系统操作结果。应用程序级智能崩溃浏览器(ALICE)探索数据库、键值存储等中的应用程序级崩溃漏洞。ALICE和BOB的主要缺点是需要用户手动处理工作负载,并为每个工作负载提供适当的检查器。它们缺乏对工作负载空间的系统探索,并且不了解持久性点,这使得用户很难手动编写触发bug的工作负载。
Zheng等人[70]提出的logging and replay框架专注于测试数据库是否提供ACID保证、是否只在iSCSI磁盘上工作以及是否只使用四个工作负载。CrashMonkey能够测试数百万个工作负载,并且ACE也允许我们生成更大范围的工作负载进行测试。
我们在研讨会论文[36]中概述了CrashMonkey后续的想法。从那时起,CrashMonkey增加了几个特性,其中最突出的特性是自动碰撞一致性测试。

8结论
本文提出了一种测试文件系统崩溃一致性的新方法:有界黑盒崩溃测试(B^3)。我们研究了过去五年中Linux文件系统中报告的26个崩溃一致性bug,发现大多数报告的bug都可以通过系统地测试小工作负载而暴露出来。我们利用这一洞察力构建两个工具,CrashMonkey和ACE,他们可以系统地测试崩溃一致性。CrashMonkey和ACE在一个由65台机器组成的研究集群上运行了两天,重现了24个已知的bug,并在广泛使用的Linux文件系统中发现了10个新bug。
我们在https://github.com/utsaslab/crashmonkey.已经提供了CrashMonkey和ACE一些资料(带有演示文档,文件和运行seq-1工作负载的单行命令)我们鼓励开发人员和研究人员根据存储库中包含的工作负载测试他们的文件系统。

致谢
我们要感谢我们的组织者安吉拉·德克布朗(Angela Demke Brown)、匿名评审员、系统和存储实验室以及LASR小组的成员,感谢他们的反馈和指导。我们要感谢Sonika Garg、Subrat Mainali和Fabio Domingues对CrashMonkey代码库的贡献。这项工作得到了VMware、Google和Facebook的慷慨捐助。本文所表达的任何意见、发现和结论或建议均为作者的意见、发现和结论或建议,并不反映其他机构的意见。


  1. U. B. LaunchPad. Bug #317781: Ext4 Data
    Loss. https://bugs.launchpad.net/
    ubuntu/+source/linux/+bug/317781?
    comments=all. ↩︎ ↩︎

  2. F. Manana. btrfs: add missing inode updat
    when punching hole. https://patchwork.
    kernel.org/patch/5830801/, Feb 2015 ↩︎

  3. F. Manana. btrfs: fix log replay failure after unlink and link combination. https:
    //www.spinics.net/lists/linuxbtrfs/msg75204.html, Feb 2018. ↩︎

  4. ↩︎
  5. ↩︎ ↩︎
  6. ↩︎
  7. ↩︎
  8. ↩︎
  9. ↩︎
  10. ↩︎
  11. ↩︎
  12. ↩︎
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Finding bugs(寻找错误)是指在软件开发过程中,为了保证软件的质量和稳定性,通过一系列的测试和调试过程,找出软件中存在的错误和缺陷,并进行修复的活动。 寻找错误是软件开发过程中必不可少的一步。在软件开发过程中,无论是编写代码、设计界面还是实施功能,都可能出现各种各样的错误。这些错误可能导致软件无法正常运行、功能异常或者性能低下。为了及时发现和修复这些错误,需要进行系统而全面的错误寻找工作。 寻找错误的方法和技巧有很多种。其中一种常用的方法是黑盒测试。黑盒测试是指在不了解软件内部结构和具体实现的情况下,通过输入一些指定的测试用例,观察软件的输出结果,并与预期结果进行对比,从而判断软件是否存在错误。另外一种方法是白盒测试。白盒测试是指在了解软件内部结构和具体实现的情况下,通过对代码进行逐行逐句的检查,发现其中潜在的错误。 除了以上的方法,还可以使用自动化的测试工具来辅助寻找错误。这些工具能够模拟用户的操作,快速地执行大量的测试用例,并生成详细的测试报告,帮助开发人员准确定位和修复错误。 在寻找错误的过程中,要保持耐心和专注。有时候错误可能隐藏得很深,需要仔细地分析和调试。同时,还要注重记录和总结错误,以便后续的修复工作。 总之,寻找错误是软件开发过程中不可或缺的一环。通过系统而全面的测试和调试工作,可以及时发现和修复软件中存在的错误和缺陷,提高软件的质量和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值