try ai
科普
编辑
分享
反馈
  • 数据持久性

数据持久性

SciencePedia玻尔百科
核心要点
  • 数据持久性是一个多层次的过程,涉及物理介质属性、操作系统缓存以及像 fsync 这样的显式命令来保证写入。
  • 文件系统通过元数据日志(预写式日志)或写时复制(CoW)等策略确保崩溃一致性,以防止因意外关机导致的数据损坏。
  • 高级持久性涉及端到端校验和以检测静默数据损坏,以及在分布式系统中使用副本来确保对硬件故障的弹性。
  • 在受监管领域,持久性的概念扩展到 ALCOA+ 框架,要求不可变的审计追踪以确保科学诚信和患者安全。

引言

数据持久性是一项根本性承诺:信息一经存储,便能在时间的考验下保持完整、正确和可访问,即使面对断电、系统崩溃和物理介质的缓慢衰变。然而,在现代计算中,这一承诺并非自动实现。许多开发者在一个危险的假设下工作,认为一个简单的 write 命令就足够了,他们没有意识到数据在到达其最终的非易失性目的地之前,会经历一个穿越易失性缓存的、充满风险的多阶段旅程。这种知识鸿沟可能导致难以察觉的错误、灾难性的数据丢失和系统性损坏。本文旨在通过全面概述数据持久性究竟是如何实现的,来弥合这一鸿沟。第一部分“​​原则与机制​​”将剖析持久性的分层技术栈,从磁存储的物理原理到像 [fsync](/sciencepedia/feynman/keyword/fsync) 这样的操作系统命令以及日志等一致性策略的关键作用。接下来的“​​应用与跨学科联系​​”部分将展示这些原则的深远影响,说明它们如何被应用于从软件架构、下一代持久性内存到高风险的受监管科学和医疗数据等各个领域。

原则与机制

谈论“数据持久性”,就是在谈论一个变动不居的世界中的“恒久性”。它是关于记忆的科学与工程,旨在确保写入的内容能被永久保存。但“写入”某样东西到底意味着什么?我们又如何能确信,在经历了无数次断电、系统崩溃以及熵的缓慢而无情的侵蚀之后,它明天或十年后依然存在?通往真正持久性的旅程是一场引人入胜的奥德赛,它带领我们从材料的量子行为一直探索到操作系统的宏伟架构。

让事物“固化”的艺术

让我们从最底层开始,从存储的物理现实谈起。想象一下,你想存储一个比特的信息——一个“1”或一个“0”。你需要一个可以被置于两种不同状态,并且至关重要地,会保持在该状态的物理系统。想想在布满灰尘的窗户上画画和在石头上雕刻的区别。石刻是持久的;而灰尘画则不然。

在计算历史的大部分时间里,这种“雕刻”是通过磁性来完成的。磁带和硬盘盘片上涂有一层薄薄的​​铁磁性​​材料。这些材料中的原子就像微小的磁铁,或者说自旋,它们可以被外部磁场朝某个方向排列。为了存储一个比特,我们使用一个写头来对齐这些自旋的一个微小区域。

但仅仅对齐它们还不够。该材料必须具备两个关键属性。第一个是高​​顽磁性​​(retentivity):一旦外部磁场消失,材料必须能强烈地保持其磁化状态。它必须“记住”自己被置于的状态。第二个是高​​矫顽力​​(coercivity):它必须能高度抵抗来自外部世界的杂散磁场的改变。一种高顽磁性但低矫顽力的材料,就像用一支优质的笔在容易弄脏的纸上书写。对于长期归档存储,你需要的是相当于在无孔表面上使用永久性记号笔的效果:一种具有宽而稳健的磁滞回线的材料,这表明它既有高顽磁性来维持状态,又有高矫顽力来保护状态不被改变。这种物理上的“顽固性”是所有数据持久性赖以建立的基石。

单次写入的危险之旅

要是存储数据像刻石头一样简单就好了。在现代计算机中,当你的应用程序想要写入数据时,这些数据在找到其最终的非易失性归宿之前,会踏上一段危险的、多阶段的旅程。认为一个 write() 命令能立即保存你的数据,就像认为把信投进邮箱就能立即送达一样。

这个旅程通常是这样的:

  1. ​​应用程序:​​ 你的程序在自己的内存中持有数据。它发出一个 write() 系统调用。

  2. ​​操作系统页缓存:​​ 操作系统为了追求速度,不会立即访问缓慢的机械硬盘。相反,它会将你的数据复制到一个名为​​页缓存​​(page cache)的快速内存缓冲区中。从应用程序的角度来看,write() 调用此时通常会返回“成功”。操作系统实质上是说:“我收到了,别担心。接下来交给我。” 这是为了性能而撒的一个善意的谎言。

  3. ​​磁盘控制器的缓存:​​ 在稍后的某个时间——当方便时或缓存已满时——操作系统将数据从页缓存发送到存储设备本身。但旅程还未结束!硬盘或固态硬盘上的磁盘控制器通常有其自己的小型易失性内存缓存。一旦数据进入这个缓存,它就会向操作系统报告“收到数据!”,这同样是为了显得速度快。

  4. ​​非易失性介质:​​ 最后,磁盘控制器会在自己方便的时候,将数据从其缓存写入物理磁性盘片或闪存单元——这才是真正的“石刻”。

只有当数据完成第四步后,它才真正具有持久性。在此之前的任何时刻发生断电——当数据还在页缓存或控制器缓存中时——都意味着数据将永久丢失。

船长的命令:[fsync](/sciencepedia/feynman/keyword/fsync) 及其同类

如果一个简单的 write 只是把信投进第一个邮箱,我们如何下达立即、有保证送达的命令呢?操作系统为此提供了专门的命令。最著名的是 [fsync](/sciencepedia/feynman/keyword/fsync) 系统调用。

对一个文件调用 [fsync](/sciencepedia/feynman/keyword/fsync) 就像向整个指挥链下达一个直接、明确的命令:“我不管你正在做什么。把这个特定的数据,从页缓存中推出去,发送到设备,并且在得到设备确认它已被写入非易失性介质之前,不要返回。”这是一个强大且昂贵的命令,因为它迫使快速、懒惰的系统去做一些缓慢而审慎的事情。

这揭示了一个关于硬件的微妙而深刻的观点。操作系统不能仅仅命令数据被写入;它还必须处理驱动器本身可能在进行缓存的可能性。一个正确的 [fsync](/sciencepedia/feynman/keyword/fsync) 不仅必须发送数据,还必须向设备发出一个特殊命令——​​缓存刷新​​(cache flush)或​​写屏障​​(write barrier)——告诉它将自己的易失性缓存提交到持久化存储中。没有这个操作,可能会发生一个危险的竞争条件:操作系统可能先发出数据写入,然后是元数据写入,而设备的内部调度器可能会发现先持久化小的元数据块比大的数据块更高效。如果恰好在错误的时机断电,你可能会得到持久化的元数据指向已丢失的数据,这是导致数据损坏的根源。一个能正确刷新设备缓存的 [fsync](/sciencepedia/feynman/keyword/fsync) 可以防止这种情况。

但即便如此,这也不是故事的全部。我们到底在让什么东西持久化?一个文件有它的​​数据​​(内容)和​​元数据​​(关于文件的信息,如大小、权限和修改时间)。甚至文件名本身也不属于文件;它是其父目录元数据中的一个条目。

[fsync](/sciencepedia/feynman/keyword/fsync) 调用是暴力方法:它试图使与文件相关的所有数据和元数据都持久化。但如果你只关心内容呢?POSIX 标准提供了一个更精细的命令,fdatasync。这个命令保证文件数据的持久性,但只保证访问该数据所需的最小元数据(比如文件大小)。它可能不会费心去持久化修改时间的变化。这给了程序员一个选择:用 [fsync](/sciencepedia/feynman/keyword/fsync) 获得最大安全性,或者通过放宽对非必要元数据的保证,用 fdatasync 获得更好的性能。

一刀切不可取:根据任务定制持久性

这引导我们得出一个更深刻的见解:持久性不是一个单一的概念。合适的持久性级别完全取决于手头的任务。一位明智的软件架构师,就像厨师选择合适的食材一样,会选择他们所需要的精确保证,不多也不少。让我们考虑三种场景:

  • ​​临时缓存:​​ 想象一个应用程序生成一个大的临时文件来加速其工作。如果文件丢失,会很烦人但并非灾难性的;应用程序可以重新生成它。最首要的优先事项是,如果文件确实存在,它绝不能是损坏的或部分写入的(即“撕裂读”)。在这里,程序员可以耍个小聪明:将新缓存写入一个临时文件,对该文件调用 [fsync](/sciencepedia/feynman/keyword/fsync) 使其数据持久化,然后执行一个原子性的 rename 操作将其移动到最终的名称。他们不需要对父目录执行 [fsync](/sciencepedia/feynman/keyword/fsync),因为那会使 rename 操作本身永久化。如果发生崩溃并且 rename 操作丢失了,系统只需恢复到旧的缓存,这是一个完全可以接受的结果。我们获得了数据一致性,而没有支付持久化的全部成本。

  • ​​系统配置更新:​​ 现在考虑更新一组关键的系统配置文件。这里的要求是绝对的。更新必须是​​原子性的​​(所有文件要么全部更新,要么全不更新)和​​持久性的​​。新旧文件混杂将是灾难性的。正确的流程是 painstaking(煞费苦心)的:将所有新文件写入一个临时目录,对每个文件执行 [fsync](/sciencepedia/feynman/keyword/fsync) 使其数据持久化,然后原子性地 rename 该临时目录为最终配置名称,最后,对父目录执行 [fsync](/sciencepedia/feynman/keyword/fsync) 使 rename 操作本身永久化。只有在完成这最后、缓慢的一步之后,我们才能确信新状态已被提交。

  • ​​只追加审计日志:​​ 对于一个每一条记录都至关重要的日志文件,需求很简单:一旦一条记录被写入并确认,它就绝不能丢失。在追加每条记录后,对文件调用 [fsync](/sciencepedia/feynman/keyword/fsync) 是必要且充分的。这确保了新添加的数据和文件更新后的大小都已持久化。由于不涉及目录操作,因此不需要额外的元数据刷新。

这些例子表明,数据持久性是在性能和安全性之间进行权衡的光谱,而操作系统提供了驾驭它的工具。

建立堡垒:在混乱世界中保持一致性

到目前为止,我们都聚焦于单个文件。但文件系统是一个由相互关联的数据结构组成的复杂网络。如果在一个像移动文件这样的复杂操作中途断电,操作系统如何防止整个结构陷入混乱?它采用复杂的策略来提供​​崩溃一致性​​(crash consistency)。现代文件系统主要由两种哲学主导:

  1. ​​元数据日志(预写式日志):​​ 这就像会计的账本。在对主文件系统结构进行任何更改之前,操作系统首先将预期更改的描述写入一个称为​​日志​​(journal)的特殊日志文件中。一旦该日志条目安全地写入磁盘,它才会继续修改实际的文件系统。如果发生崩溃,操作系统会执行恢复检查。如果在日志中发现不完整的条目,它就知道操作被中断了,于是便什么也不做。如果发现完整的条目,它就可以“重放”该操作,以确保文件系统达到其预期的、一致的状态。这保证了元数据操作是原子性的:它们要么完全发生,要么完全不发生。

  2. ​​写时复制(Copy-on-Write, CoW):​​ 这种方法更加谨慎。它从不就地修改数据。当一个块需要被更改时,文件系统会将该块的新版本写入磁盘上的一个空闲位置。然后,它更新父指针以指向这个新块,这又需要写入父节点的新版本,以此类推,一直到文件系统树的根。最后一步是原子性地更新一个主“根指针”,使其指向整个文件系统的新的、一致的版本。如果在这个最终的原子性切换之前发生崩溃,旧的根指针仍然有效,文件系统保持在其先前的、完全一致的状态。那些新的、部分写入的数据只是稍后会被清理的垃圾。

这些技术构建了一座一致性的堡垒,在易出错的硬件之上提供了一个简单、可靠的存储系统的假象。这种保护非常有效,甚至可以在虚拟化中用来保护客户机操作系统免受廉价物理USB驱动器不可靠性的影响。通过在健壮的、带日志功能的主机文件系统上将磁盘模拟成一个文件,虚拟机监控器(hypervisor)为客户机提供了比直接“透传”访问可疑硬件远为稳定的基础。

看不见的敌人:对抗静默损坏

我们已经构建了一个能够在崩溃中命令持久性并保持一致性的系统。还有什么可能出错呢?最阴险的威胁是:​​静默数据损坏​​(silent data corruption),或称“比特腐烂”(bit rot)。这是指磁盘上的一个比特在被正确写入很久之后自发地翻转了。存储设备并不知道这发生了。你那完美一致的文件系统现在包含了一个谎言。

像ECC这样的设备级检查可以捕获其中一些错误,但它们并非万无一失。更重要的是,损坏可能根本不是发生在磁盘上。它可能发生在计算机的内存、总线或驱动器的控制器中——从应用程序到存储介质路径上的任何地方。

为了对抗这种情况,我们需要“端到端论证”(end-to-end argument)。检查必须覆盖整个路径。这是通过​​端到端校验和​​(end-to-end checksums)实现的。当操作系统决定写入一个数据块时,它会计算该数据的数学指纹(一个强校验和,如SHA-256),并将该指纹与数据一同存储在磁盘上。当它稍后读取该块时,它会根据接收到的数据重新计算校验和,并与存储的指纹进行比较。如果它们不匹配,就说明检测到了损坏,无论损坏发生在哪里。

这引出了操作系统关于数据完整性的终极契约:

我,作为操作系统,保证当您读取一个文件时,我要么返回您最初写入的、逐比特完全正确的数据,要么返回一个错误。我绝不会在知情的情况下悄悄地向您返回损坏的数据。

为了使这个承诺真正稳固,操作系统将校验和与​​冗余​​(例如,RAID镜像,保存数据的多个副本)和​​后台刷洗​​(background scrubbing)结合起来。刷洗是一个过程,操作系统会定期读取磁盘上的所有数据,验证校验和,以便在比特腐烂造成永久性数据丢失之前主动发现并修复它。这是数据持久性的顶峰:一个能够主动对抗物理世界缓慢衰变的自愈系统。

超越比特:人的因素

最后,值得记住的是,我们的旅程并非始于一块磁盘,而是一个学生在一张纸巾上草草记下一个数字。这一个简单的动作违反了人类世界中关于持久性的每一条原则。这张便条是不可​​追溯的​​(谁写的?),非​​同时的​​(没有在正确的时间记录在正确的地方),并且与其​​上下文​​脱节(哪个样本?哪个仪器?)。它不是一个​​持久​​、​​可用​​的系统的一部分。

这向我们表明,数据持久性终究是人类关心的问题。缓存、文件系统和校验和这些复杂的层次,都是为了一个简单的目标服务:创建可靠的信息记录。无论这条记录是一次科学测量、一笔金融交易,还是一张家庭照片,使其长存的原则——确保其完整性、上下文和永久性——其适用范围从电子的自旋,一直延伸到执笔者自身的纪律。

应用与跨学科联系

既然我们已经探讨了确保数据在计算机动荡的生命周期中存活下来的复杂机制,让我们退后一步,问一个更深刻的问题:对持久性的追求到底在哪些地方至关重要?这似乎只是数据库工程师和操作系统设计师关心的小众问题,一个隐藏在机器深处的技术细节。但事实远非如此。持久性原则——即信息应能可靠地经受时间和混乱的考验这一简单而强大的理念——是一条贯穿现代技术几乎所有层面的线索,从平凡到神奇。这是一个关于信任的故事,是我们的数字造物所做出的“它们会记住”的承诺。

让我们开启一段旅程,从你屏幕上熟悉的浏览器到医学的前沿,看看这同一个概念是如何以截然不同却又紧密相连的方式体现出来的。

数字架构师的蓝图

想象一下,你正在用大量相互扣合的积木搭建某个东西。你完成了一部分,但在连接下一部分之前,桌子被撞了一下,你的作品散落一地。很沮丧,不是吗?应用程序开发者每时每刻都在面临同样的问题。计算机随时可能崩溃,如果数据以错误的顺序写入,整个结构可能会处于损坏、无意义的状态。

以一个简单的网络浏览器缓存为例,这是一个你最近访问过的网页的本地库。为了加快速度,浏览器维护着一个索引——一个卡片目录——告诉它在哪里可以找到每个页面的实际数据。那么,如果你在完成保存页面数据之前就在索引中写入了一个新条目,会发生什么?如果此时电源中断,你就会留下一个指向虚无的指针,或者更糟,指向一堆不完整的数据。目录条目承诺了一本不存在的书。

对此,优雅的解决方案是数字架构的一条基本规则:​​先建桥,后通路。​​你必须首先确保数据已完整、正确地写入其位置。只有在那之后,在一个独立的、原子性的步骤中,你才能更新索引以指向它。这个协议通常涉及一些巧妙的技巧,比如写入数据时将一个临时的“提交标志”设为假,只有在数据安全写入后才将其翻转为真。在崩溃后恢复时,系统只需扫描那些可验证为完整且已提交的记录,并从这个“地面实况”中重建其索引,丢弃任何部分或未提交的片段。这种简单的两阶段提交,是一场谨慎与确认之舞,一种在可靠软件中反复出现的模式。

硬件基础:当硅与持久性相遇

但是,这种“持久性”究竟存在于何处?在计算历史的大部分时间里,它存在于旋转的盘片或闪存芯片中,通过一条缓慢的总线与处理器隔开。现在,我们正在进入​​持久性内存(PMem)​​的时代,这是一种革命性的技术,其中内存本身就是非易失性的。它像RAM一样快,但断电后能记住一切。

你可能认为这解决了我们所有的问题。如果内存是持久的,我们不就可以直接写入数据然后就完事了吗?自然,一如既往地,更为微妙。CPU并不直接写入内存;它写入其自己的、私有的、易失性的缓存——可以把它们想象成临时的草稿纸。如果电源故障,这些草稿纸会被擦除,上面的任何数据都会丢失。

因此,即使有了这种神奇的新硬件,软件也必须是显式的。要对一个数据结构做一个简单的更改,比如向链表中添加一个新节点,需要一系列精心编排的操作。首先,程序写入新数据。然后,它必须使用特殊指令,如 CLWB (Cache Line Write Back),来告诉CPU:“请将这块特定的数据从你的草稿纸刷新到永久内存中。”最后,也是至关重要的一步,它必须使用一个“屏障”指令,如 SFENCE,它起到了一个障碍的作用。程序会在此屏障处暂停,直到CPU确认所有之前的刷新操作都已完成。只有在新节点的数据被认证为持久化之后,程序才能安全地更新前一个节点的指针,使其成为列表的一部分。

这场 写入 (write)、刷新 (flush) 和 屏障 (fence) 之舞,是现代数据持久性的微观基础。并且,当我们增加抽象层时,这份责任并不会消失。一个使用PMem作为其内部缓存的操作系统,仍然需要执行这种精心的编排,以响应应用程序对持久性的请求,比如调用 [fsync](/sciencepedia/feynman/keyword/fsync)。即使在虚拟化世界中,客户机操作系统在虚拟机监控器(hypervisor)内部运行,这份注意义务也被传递下去。虚拟机监控器可以向客户机呈现一个“虚拟”的持久性内存设备,但客户机仍然有责任发出所需的刷新和屏障指令,以使其自己的数据持久化。看来,责任总是落在写入数据的那一方。

向上扩展:数据中心中的持久性

当我们将规模从一台计算机扩展到数据中心中协同工作的数千台计算机时,会发生什么?持久性的概念也随之扩展。它不再仅仅是关于在一次电源闪烁中幸存下来;而是关于在整台机器甚至整个机架的死亡中幸存下来。在这里,持久性与​​弹性​​(resilience)同义。

实现这一目标的唯一方法是通过​​复制​​(replication)。关键数据和服务不能只存在于一个地方;它们必须被复制到多个独立的主机上。设计这样一个系统涉及到一种优美的责任层级。在最底层,每台机器的本地操作系统处理着纳秒级的线程调度事务。但在集群层面,一个全局的“编排器”做出粗粒度的决策:在哪里放置新工作,如何平衡负载,以及最重要的是,将数据的副本存储在哪里,以确保整个系统对故障具有持久性。这种局部自治和全局协调之间的相互作用是现代分布式系统的核心,也是我们在单台机器上看到的同一持久性问题的大规模版本。

当持久性事关生死

到目前为止,我们的例子都是关于性能和便利性的。但如果被记录的数据可能意味着一种救命药物被批准还是被拒绝呢?或者,如果它是一种个性化癌症疗法的生产记录,其中“批次”是单个、不可替代的人类患者呢?

在受监管的科学领域——良好实验室规范(GLP)和良好生产规范(GMP)——数据持久性不是一个技术特性。它是一种道德和法律上的强制要求。在这里,这些原则被编纂在一个称为 ​​ALCOA+​​ 的框架中:数据必须是可追溯的(Attributable)、清晰易读的(Legible)、同步的(Contemporaneous)、原始的(Original)和准确的(Accurate),此外还要完整(Complete)、一致(Consistent)、持久(Enduring)和可用(Available)。

这不仅仅是行话;它是创建可信知识的蓝图。考虑一个用于记录毒理学测试结果 或细胞治疗产品生产过程 的电子实验记录本。

  • ​​可追溯性(Attributable):​​ 每一个条目都必须与一个唯一的、经过验证的用户和一个安全的、同步的时间戳相关联。不存在匿名性。
  • ​​原始性与完整性(Original & Complete):​​ 必须保存来自仪器的原始数据——而不是方便的PDF摘要。所有数据,包括来自失败实验和被取代结果的数据,都必须保留。删除失败记录不是清理;而是销毁知识。
  • ​​持久性与可用性(Enduring & Available):​​ 数据必须以一种不仅能经受服务器故障,还能经受整个设施范围灾难的方式存储,并且必须在几十年后仍能读取。这意味着异地备份和经过测试的恢复计划是不可协商的。

也许最关键的元素是​​不可变的审计追踪​​。对任何记录的任何更改都必须被自动记录:谁做的更改,何时做的,更改前的值是什么,现在的值是什么,以及他们为什么更改。这个日志不能被编辑或删除。这是一个系统的宣誓证词。它防止了那种不合格的结果被悄悄地“修正”为合格而没有任何理由的阴险情况,这种行为可能掩盖有缺陷的药物或受污染的医疗产品。在这个高风险的领域,持久性是科学诚信和患者安全的最终保障。

抽象视角:思想领域中的持久性

最后,让我们上升到一个更抽象的层面。在函数式编程的世界里,有一个深刻而优美的思想,叫做​​持久性数据结构​​。其核心原则很简单:你永远不改变任何东西。当你想“更新”结构时,你会创建一个新版本,它重用旧结构中所有未改变的部分,只为被修改的路径创建新的部分。

这给了你一种非凡的持久性形式:数据结构曾经存在过的每一个状态的完整、可访问的历史。这就像为你的数据拥有一个完美的录像机。这种方法有一个惊人地实用的好处。因为结构是不可变的,并且更改是局部化的,所以管理计算机的内存变得更加高效。垃圾收集器可以使用一种简单、快速的技术,如引用计数,将其工作仅集中在版本之间的微小差异上,而无需扫描整个共享结构 [@problem_gpid:3258614]。通过在其设计的最基本层面上拥抱持久性,系统变得更加优雅和高效。

要真正领会持久性的作用,思考它的缺席是很有启发性的。想象一下一个为没有任何持久性存储(没有硬盘,没有闪存,只有易失性内存)的简单设备设计的操作系统。 “文件系统”这个概念会发生什么变化?它会消失吗?不会。分层命名空间和统一接口(open, read, write)的核心抽象仍然极其有用。你仍然可以有一个代表传感器、执行器或临时内存块的“文件”。所失去的仅仅是持久性的保证。通过移除持久性,我们更清晰地看到了这些抽象所扮演的其他重要角色。

从浏览器缓存到其运行的硬件,从单个服务器到全球数据中心,从科学仪器到我们算法的数学本身,持久性原则是一个永恒的伴侣。它是确保我们的数字世界拥有一个我们能信任的记忆的挑战性、迷人且最终至关重要的艺术。