如果你订阅了 Linux Kernel 的 maillist,你一定发现最近 Linus 又爆粗口了,而这次的对象是 ext4 文件系统。
On Sun, Aug 6, 2017 at 12:27 PM, Theodore Ts'o wrote:
>
> A large number of ext4 bug fixes and cleanups for v4.13
A couple of these appear to be neither cleanups nor fixes. And a lot
of them appear to be very recent.
I've pulled this, but if I hear about problems, ext4 is going to be on
my shit-list, and you'd better be a *lot* more careful about pull
requests. Because this is not ok.
Linus
而这已经不是 Linus 第一次对 ext4 文件系统表达不满了。
尽管 ext4 文件系统已经发布了多年,也被广泛应用于桌面及服务器,但关于 ext4 存在可能丢数据的 Bug 报告就一直没有中断过。例如在 2012 年的一封邮件中,Theodore Ts'o 报告了一次严重的 Bug,已经影响了部分 Linux 稳定版本的内核。
如果你持续关注文件系统或内核技术,你一定注意过这样一篇文章:Fuzzing filesystem with AFL。Vegard Nossum 和 Quentin Casasnovas 在 2016 年将用户态的 Fuzzing 工具 AFL(American Fuzzing Lop)迁移到内核态,并针对文件系统进行了测试。
结果是相当惊人的。Btrfs,作为 SLES(SUSE Linux Enterprise Server)的默认文件系统,仅在测试中坚持了 5 秒钟就挂了。而 ext4 坚持时间最长,但也仅有 2 个小时而已。
这个结果给我们敲响了警钟,Linux 文件系统并没有我们想象中的那么稳定。而事实上,在 Fuzz 测试下坚持时间长短仅仅体现出文件系统稳定性的一部分。数据可靠性,才是文件系统中最核心的属性。然而 Linux 文件系统社区的开发者往往都把注意力放在了性能,以及高级功能的开发上,而忽略了可靠性。
今天,我们就带大家回顾一下 Linux 文件系统的黑历史,希望能够警醒大家,不要过分相信和依赖文件系统。同时,在使用文件系统构建应用时,也需要采用正确的“姿势”。
谈到 Linux 文件系统,不得不提到 POSIX(Portable Operating System Interface),这样一个奇葩的标准。而开发者对于 POSIX 的抱怨,可谓是罄竹难书。
作为一个先有实现,后有标准的 POSIX,在文件系统接口上的定义,可谓是相当的“简洁”。尤其当系统发生 crash 后,对于文件系统应有的行为,更是完全空白,这留给了文件系统开发者足够大的“想象空间”。也就是说,如果一个 Linux 文件系统在系统发生崩溃重启后,整个文件系统的内容都不见了,也是“符合标准”的。
而事实上,类似的事情确实发生过:在 2015 年,ChromeOS 的开发者曾报告了一个 ext4 的问题,有可能导致 Chrome 发生崩溃。而来自 ext4 开发者的回答是,“Working As Intended”。
在历史上,不断有人尝试给文件系统提供更加严谨的 Consistency(一致性)定义,尤其是 Crash-Consistency(故障后的一致性)。到目前为止,尽管 POSIX 也经历了几个版本,但关于文件系统接口的定义,还是那个老样子。而 POSIX 标准,也是造成了文件系统各种问题的一个很重要的因素。关于各种一致性的定义,我们后面也会有文章专门进行介绍。
文件系统一直有着光辉的发展历史,也孕育了许多伟大的 Linux 内核贡献者。从最早的 FFS,到经典的 ext2/ext3/ext4,再到拥有黑科技的 Btrfs,XFS,BCacheFS 等。
然而软件开发的过程,当然不是一帆风顺的。威斯康辛大学麦迪逊分校的研究者曾在 FAST '13 上发表过一篇著名的论文《A Study of Linux File System Evolution》。文章对 8 年中,Linux 社区与文件系统相关的 5079 个 Patch 进行了统计和分析。从其数据中可以看出,有将近 40% 的文件系统相关的 Patch 属于 Bugfix 类型。换句话说,每提交两个 Patch,就有可能需要一个 Patch 用于 Bugfix。
而文件系统的 Bug 数量并没有随着时间的推移而逐渐收敛,随着新功能不断的加入,Bug 还在持续不断的产生。而 Bug 的集中爆发也往往源于大的功能演进。
而从上图中可以看出,在所有的 Bug 中,有接近 40% 的 Bug 可能导致数据损坏,这还是相当惊人的。
可以想象,在 Linux 文件系统的代码库中,还隐藏着许多 Bug,在等待着被人们发现。
哥伦比亚大学文件系统领域著名的专家 Junfeng Yang,曾经在 OSDI '04 上发表了一篇论文,该论文也是当年 OSDI 的最佳论文。在这篇论文中,Junfeng Yang 通过 FiSC,一种针对文件系统的 Model Checking 工具,对 ext3,JFS,ReiserFS 都进行了检查,结果共发现了 32 个 Bug。而不同于 AFL,FiSC 发现的 Bug 大部分都会导致数据丢失,而不仅仅是程序崩溃。例如文章中指出了一处 ext3 文件系统的 Bug,该 Bug 的触发原因是在通过 fsck 进行数据恢复时,使用了错误的写入顺序,在 journal replay 的过程中,journal 中的数据还没有持久化到磁盘上之前,就清理了 journal,如果此时发生断电故障,则导致数据永久性丢失。
对于大部分应用程序开发者来说,并不会直接使用文件系统。很多程序员都是面向数据库进行编程,他们的数据大多是存在数据库中的。我们经常想当然的认为,数据库的开发者理应会理解文件系统可能存在的问题,并绕过文件系统的 Bug,帮助我们解决各种问题。然而这只是一种侥幸心理罢了,由于文件系统过于复杂,标准不清晰,即使是专业的数据库的开发人员,也往往无法避开文件系统中所有的问题。
以 LevelDB,我们最常用的一种单机 Key-Value Store 举例。研究人员分别对 LevelDB 的两个版本,1.10 和 1.15 进行了测试,分别发现了 10 个和 6 个不同程度的漏洞。其中 1.10 版本有 1 个漏洞可能导致数据丢失,5 个漏洞导致数据库无法打开,4 个漏洞导致数据库读写错误。而 1.15 版本分别有 2 个漏洞导致数据库无法打开,2 个漏洞导致数据库读写错误。
这些问题,大部分源自应用开发者对文件系统错误的假设。也就是说,他们以为文件系统可以保证的特性,而事实上并不能得到保证。而这些特性,也都是 POSIX 标准中未曾明确定义的。
这里我们举个例子:
Append atomicity,追加写原子性。
向文件中追加写入,并不意味着是原子性的。如前文 ChromeOS 开发者遇到的 ext4 的问题,其根本原因,就是假设 ext4 文件系统是保证追加写原子性的。在这封邮件中,开发者提供了一个可以复现问题的步骤。假设文件中已经有 2522 字节的数据,再追加写入 2500 字节的数据,文件大小本应为 5022 字节。而如果在追加写的过程中,遇到系统崩溃,在系统恢复后,文件的大小可能是 4096 字节,而非 5022 字节,而文件的内容,也可能是垃圾数据,无法被程序正确识别。
LevelDB 同样也假设了文件系统具有追加写的原子性,前面提到的一些漏洞就源于此。
而这仅仅是冰山一角。单单关于文件系统写入数据的原子性,就有包括:单 sector 覆盖写,单 sector 追加写,单 block 覆盖写,单 block 追加写,多 block 追加写等等。而对于不同类型的文件系统,甚至同一个文件系统的使用不同参数,对于原子性都可能具有不同范围的支持。再考虑到 POSIX 提供的其他接口,包括 creat,rename,unlink,truncate 等等(关于这些接口背后的坑,我们后续将单独写文章介绍)。这使得开发应用系统,尤其是数据库系统,变得非常复杂。
这里我们提供一些建议,希望能够帮助大家尽量少的踩坑。
首先,对于大部分应用程序员来说,应尽可能选择使用成熟的数据库,而非直接操作文件。尽管如前文所说,在复杂的文件系统面前,数据库也无法幸免于难,但数据库开发者掌握的关于文件系统的知识,还是远远强于普通开发者的。数据库也通常提供了数据恢复工具,以及备份工具。这避免了开发者重新造轮子,也极大的减轻了灾难发生后可能带来的影响。
而对于单机数据库,分布式数据库,以及分布式存储的开发者来说,我们的建议是尽量避免直接使用文件系统,尽可能多的直接使用裸设备,这避免了很多可能引起问题的接口,例如 creat,rename,truncate 等。例如 SmartX 在设计和实现分布式存储时,就直接使用裸设备。
如果必须要使用文件系统,也要使用尽量简单的 IO 模型,避免多线程,异步的操作。同时,一定要在设计的过程中,把对于文件系统操作的模型抽象出来,并画成步骤图,这里我们推荐 draw.io,一个非常不错的免费画图工具。要假设每一个步骤都可能失败,每一个步骤失败后,都可能产生垃圾数据,要提前设计好数据校验以及处理垃圾数据的方式。如果步骤之间有存在依赖关系,一定要在执行下一步之前,调用 fsync(),以保证数据被持久化到磁盘中。
最后,设计和实现完成后,在单元测试和集成测试的过程中,也一定要增加故障测试。例如在单元测试中,通过 mock 的方式模拟 IO 故障,在集成测试中,可以加入随机 kill 进程,随机重启服务器的测试用例,也可以通过 dm-delay,dm-flakey 等工具进行磁盘故障模拟。
看了这么多黑历史,真的是三观都毁掉了。而事实上,我们每天确实都生活在这些危机中。
这里要强调的是,我们并不是想诋毁 Linux 文件系统,相反,我们非常感谢 Linux 内核开发者在文件系统方面做出的贡献。但同时,由于系统的复杂度所带来的严重问题也是无法回避的。在 Linux 文件系统的代码中,必然还存在着很多未被发现的严重 Bug,开发者和研究人员也从来没有停止过寻找 Bug 的努力。而随着新功能不断地加入,新的 Bug 也在不断的产生。我们多一些这方面的思考和谨慎,并不是什么坏事。
感谢大家的观看!
作者:张凯(Kyle Zhang) ,SmartX 联合创始人 & CTO。SmartX 拥有国内最顶尖的分布式存储和超融合架构研发团队,是国内超融合领域的技术领导者。
来源:SmartX知乎账号
链接:https://zhuanlan.zhihu.com/p/28828826
————广告时间————
《马哥Linux云计算及架构师》课程,由知名Linux布道师马哥创立,经历了8年的发展,联合阿里巴巴、唯品会、大众点评、腾讯、陆金所等大型互联网一线公司的马哥课程团队的工程师进行深度定制开发,课程采用 Centos7.2系统教学,加入了大量实战案例,授课案例均来自于一线的技术案例。
开课时间:11月06号
课程咨询请长按即可咨询