暗无天日

=============>DarkSun的个人博客

文件充满了危险——Dan Luu 谈文件系统的可靠性陷阱

2019 年,Dropbox 宣布 Linux 版只支持 ext4 文件系统。Reddit r/programming(最大的英文编程论坛之一)上最高赞的评论是:

我不太理解,为什么应用程序要直接支持这些文件系统?内核不是已经抽象了底层存储细节吗?

这不应该是操作系统的工作吗?

大多数人觉得文件很简单——打开、写入、关闭,能出什么问题?Dan Luu 在 Deconstruct 2019 的演讲 "Files are fraught with peril" 给出了令人不安的答案:从 API 到文件系统到物理磁盘, 每一层都有可能导致数据丢失或损坏的陷阱

三层风险速览

文件 API:安全写一个文件有多难

假设我们要把文件内容从 a foo 改成 a bar 。最直觉的做法是调用 pwrite 系统调用:

pwrite([file], "bar", 3, 2)  // 在偏移 2 处写入 3 字节

问题在于:如果在写入过程中崩溃或断电,你可能得到 a booa bor 这样的半写入状态——数据损坏。

要安全地写文件,需要实现一个"撤销日志"(undo log),并且逐步保证每个步骤的持久性和顺序:

creat(/d/log)                        // 1. 创建日志文件
write(/d/log, "...[checksum],foo", 7) // 2. 写入原始数据和校验和
fsync(/d/log)                        // 3. 确保日志落盘
fsync(/d)                            // 4. 确保父目录记录了日志文件
pwrite(/d/orig, "bar", 3, 2)         // 5. 修改原始文件
fsync(/d/orig)                       // 6. 确保修改落盘
unlink(/d/log)                       // 7. 删除日志

为什么每一步都不可省略:

  • 不加 checksum(步骤 2): data=writeback 模式下,日志文件可能包含垃圾数据,"恢复"时反而把垃圾写入原始文件
  • 不 fsync 父目录(步骤 4):崩溃后日志文件可能不存在,等于没有保护
  • 不 fsync(步骤 3、6): data=ordered 模式下,操作可能被重排序,导致文件先被修改、日志还没写完

而这一切只是在 ext3/ext4 一个文件系统家族内部的差异。换成 btrfs、ZFS、XFS,行为又有不同。

2014 年的一项研究对 Leveldb、LMDB、GDBM、HSQLDB、SQLite、PostgreSQL、Git、Mercurial、HDFS、Zookeeper 做了静态分析,发现 除 SQLite 的特定模式外,所有软件都有文件 API 使用方面的 bug 。这些软件的开发者已经比绝大多数程序员更了解文件系统了。

为什么 API 这么难用?本质原因是四个难题叠在一起:

  1. 安全写文件面临的挑战和并发编程一模一样 ——你需要保证原子性、保证操作顺序
  2. 文件 API 在不同文件系统、不同挂载模式下的行为还不一致
  3. 文档不清楚
  4. 性能与正确性有固有冲突。 fsync 同时做了两件事:barrier(防止操作重排序)和 cache flush(强制数据写入磁盘)。很多时候你只需要 barrier 来保证顺序,但 fsync 强迫你连带执行代价极高的 cache flush。2013 年的一项研究修改 ext4 加了一个只做 barrier 不做 flush 的操作,发现性能接近完全禁用 cache flush(即 unsafe 模式)的水平——说明 fsync 接口捆绑导致的性能惩罚几乎等于安全与不安全之间的全部差距。但大多数人无法自己改文件系统,只能在性能和安全之间妥协,而妥协往往意味着不安全

文件系统:fsync 的错误处理几乎不可能做对

文件系统的错误处理经历了漫长的改进。2005 年的一项研究发现,大多数文件系统几乎丢弃了所有写错误。到 2017 年,Wesley Aptekar-Cassels 和 Dan Luu 重新测试,发现基本错误处理已有显著改善。

fsync 的错误处理依然是个灾难。在 2018 年 Q2 之前的 Linux 内核上,=fsync= 遇到错误(比如磁盘写入失败)时,错误很可能会被直接丢弃,甚至报告给错误的进程。即使在较新的内核上,=fsync= 报错之后的局面也很棘手:

  • XFS 和 btrfs :已修改但未持久化的数据会被直接丢弃,无法恢复
  • ext4 :数据不会立刻丢弃,但被标记为"未修改",文件系统不会再尝试写入;内存紧张时随时可能被清除
  • ZFS on Linux :CPU 使用率飙升,系统可能挂起或不可用

PostgreSQL、MySQL、MongoDB 的应对策略是: 遇到 fsync 错误直接崩溃进程,让用户从最近的检查点恢复 。这不是过度谨慎,而是因为在 Linux 上确实没有更好的恢复方法。

更糟糕的是 syncfs ——它 根本不返回错误 ,意味着静默数据丢失。

这个问题也不限于 Linux。当 PostgreSQL 团队首次提出"fsync 错误时应该崩溃"时,开发者 mailing list 上的第一反应是:

你一定在开玩笑……如果 [fsync 的当前行为] 确实如此,我们需要反击这种内核脑残行为。

但讨论完细节后,所有人都同意崩溃是唯一正确的选择。

磁盘:厂商声称的和实际的内容

磁盘厂商在数据手册中声称的不可纠正错误率(UBER):

类型 厂商声称的 UBER
消费级 HDD 1e-14
企业级 HDD 1e-15
消费级 SSD 1e-15
企业级 SSD 1e-16

看起来很可靠?1e-14 意味着每读 1e14 bit 才会出一个错。但 10TB HDD 全量读一次就是 8e13 bit——差不多就该出一个错了。读十几遍就几乎必然会遇到错误。

而实际观测值远比厂商声称的糟糕:

  • Microsoft 的研究观测到 SSD 错误率 1e-11 到 6e-14
  • Facebook 的研究观测到 SSD 错误率 2e-9 到 6e-11

2e-9 意味着每 250MB 就可能出现一个不可纠正错误——比声称的值差了 50 万到 500 万倍。

SSD 的数据保持能力也不像想象中那么可靠。一块剩余 90% 写入寿命的 SSD 厂商声称数据可保持约 10 年,但接近寿命终点的 SSD 可能只能保持数据 3 个月到 1 年。Luke Leighton 测试了 6 款声称有断电保护的 SSD,其中 4 款在断电测试中失败——凡是 Intel 以外的品牌都失败了。

那些不管用的偏方

网上讨论"如何安全写文件"时,总有人提出各种"一个技巧就够了"的说法。Dan Luu 阅读了两千多条互联网评论后,总结了最常见的两种:

"用 rename 代替覆盖写入"

做法是:先写入临时文件,再用 rename 覆盖原文件。理由是 POSIX 规范说 rename 是原子的。

问题在于两层:

第一,POSIX 说 rename 是原子的,指的是在 正常操作 下原子——要么成功要么失败,不会出现"重命名了一半"的状态。但崩溃时并非如此。崩溃可能发生在文件系统已经把新文件名写入了目录、但还没来得及删除旧文件名指向的数据的时刻。结果是:新文件指向的是不完整的内容,旧文件的数据也丢失了。大多数主流 Linux 文件系统至少有一种挂载模式会出现这种情况。

第二,=rename= 不保证按程序顺序执行。你可能先写入临时文件,再 rename 覆盖原文件——程序逻辑上是这个顺序。但文件系统可能先执行 rename ,再写入临时文件的内容。崩溃后你会发现:新文件存在,但内容是空的或旧的。

btrfs 是主流文件系统中唯一明确保证 rename 在崩溃时也原子的(仅限覆盖已有文件的场景)。但 2018 年的一项测试发现了大量原子性 bug——btrfs *声称*有这个保证,*实现*上却经常做不到。也就是说,即使你为了 rename 的原子性专门选用 btrfs,也得不到可靠的安全保证。

"只追加不覆盖"

追加写入也不保证顺序或原子性(已有多项研究证实)。相信追加是安全的是很多实际 bug 的来源。

实用建议

用数据库代替直接文件操作

如果你需要可靠地存储数据,SQLite 是一个很好的选择。它是在前述研究中 唯一 没有文件 API bug 的软件(在特定模式下)。相比自己处理 fsync、undo log、checksum,让成熟的数据库来处理这些复杂性要可靠得多。

这不是说绝对不能用文件——但如果你在写一个需要降低数据损坏率的应用,优先考虑数据库。

备份策略

鉴于磁盘错误率远高于厂商声称的数值、SSD 断电保护经常失效、文件系统 fsync 错误处理有缺陷, 定期备份是不可替代的 。不要依赖任何单层保护。

理解文件系统选择的代价

Dropbox 只支持 ext4 不是偷懒。支持多种文件系统意味着要针对每种文件系统的 bug 和特殊行为做适配,测试矩阵指数级增长。对于一家需要可靠同步数据到磁盘的公司来说,限制支持的文件系统范围是务实的工程选择。

如果有人告诉你"文件系统都一样,内核已经抽象了",让他们看看 ext4 三种 data 模式下完全不同的崩溃行为。

文件系统 : 可靠性 : fsync : 数据安全 : DanLuu