Li Sheng | Backend / Distributed Storage Engineer Li Sheng | Backend / Distributed Storage Engineer
Home
Resume
Projects
Topics
Notes
GitHub (opens new window)
Home
Resume
Projects
Topics
Notes
GitHub (opens new window)
  • Go语言

  • C++

  • 算法题

  • 存储系统

    • 导航

    • 单机IO基础

    • IO语义与持久化

      • VFS层
      • Page Cache总览
      • fsync、写回与刷盘语义
      • 文件系统日志与崩溃恢复
    • 块层与高速路径

    • Ceph

    • DAOS

    • 归档

  • CephFS

  • 分布式系统

  • 计算机网络

  • Redis与缓存

  • Kubernetes

  • 技术笔记
  • 存储系统
  • IO语义与持久化
lisheng
2026-04-27

fsync、写回与刷盘语义

# fsync、写回与刷盘语义

存储系统里最容易被说得过于轻巧的一句话,就是“调用了 write,数据就写进去了”。这句话之所以危险,不是因为它完全错误,而是因为它把一条本来分层推进、带着明确状态差异的链路,压扁成了一个看似瞬时完成的动作。对应用来说,write 像是一次写操作;对内核来说,它往往只是一次状态变更的起点。数据先进入 Page Cache,相关页被标记为脏,文件长度、时间戳、目录项等元数据也可能一并进入待更新状态,随后这些变化才会在未来某个时刻通过文件系统、块层和设备路径继续向下推进。因此,讨论 fsync、写回和刷盘语义,本质上不是在解释几个 API 的定义,而是在回答同一个问题:一份修改从“应用已经交给内核”演化到“系统承认它已经稳定提交”之间,到底经过了哪些状态边界,每个边界又意味着什么。

如果把一次普通文件写入按真实因果链展开,第一步几乎总是内存中的状态先变化,设备上的状态后变化。应用提交给 write 的字节进入内核后,最先得到保证的只是“这次修改已被当前内核实例接收并纳入管理”,而不是“这次修改已经抵达稳定介质”。这是 Page Cache 能提高吞吐的根本前提,因为系统由此获得了时间和空间上的重排能力。它不必把每一个小写入都立即翻译成一次同步设备操作,而可以先吸收、合并、排序、延迟,再挑选更合适的时机一批批推出去。也正因为这一步骤重排,应用感知到的延迟和设备承担的真实工作开始脱钩。许多写请求看上去返回得很快,不是因为设备响应极快,而是因为设备工作被推迟到了调用返回之后。这个看似朴素的事实,正是后面所有持久化语义、崩溃恢复语义和性能抖动语义的源头。

写回机制就是系统用来管理这段“先成功、后落盘”时间窗口的方式。所谓写回,不是第二次写同一份数据,而是把已经存在于页缓存中的脏页往下游推进,让它们逐步穿过文件系统的内部结构、块层的请求组织方式以及设备控制器的执行路径。写回什么时候发生,通常并不由单次 write 单独决定,而是由系统整体状态决定。脏页积累到一定比例,系统会担心风险窗口过大或后续成本过高;定时回写线程苏醒,系统会清理长期滞留的脏状态;内存压力增大,系统为了回收页而不得不先把脏页变成可回收的干净页;应用调用 fsync、fdatasync 或其他同步接口,则是主动要求某一部分修改尽快越过更强的提交边界。于是写回不是一个点事件,而是一种持续的后台推进过程。它的存在说明,内核管理的并不是“有没有写”,而是“一批已写但尚未真正稳定化的状态如何在时间上有序结算”。

这里最值得建立的模型,是把写入视为跨越多层状态边界的过程,而不是单个动作。第一层边界通常是 write 返回成功,它只代表修改已经进入内核控制范围。第二层边界是页已经成为脏页,系统明确知道这份修改尚欠一次向下游的推进。第三层边界是修改已经进入文件系统和块层的执行上下文,可能体现在日志事务、BIO、request 或设备队列元素中。第四层边界才接近人们直觉中的“落盘”,也就是相关数据和必要的元数据已经被推进到当前系统承认的稳定持久化位置。问题恰恰在于,不同层对“完成”的定义并不一样。应用只看到系统调用是否返回,文件系统关心自己的内部一致性是否被维护,块层关心请求是否被正确组织与下发,设备则还要面对控制器缓存、写缓存策略、flush 命令以及介质本身是否具备断电保护。所谓“刷盘语义”真正讨论的,从来都不是一个抽象的“写到磁盘了没有”,而是到底把哪一层的完成当作了提交线。

fsync 的价值,就在于它把这条默认异步推进的链路,临时改造成一条必须跨越特定边界才能返回的同步路径。它并不会改变数据内容本身,而是改变应用可以依赖的语义强度。没有 fsync 时,系统默认用更大的风险窗口换更高吞吐;调用 fsync 之后,应用是在要求系统尽快缩小这个窗口,把与该文件相关的数据以及必要的元数据推进到当前承认的持久化边界,并在边界被越过之前阻塞调用者。换一种建模方式,write 更像是提出“这里发生了一个新状态”,而 fsync 更像是要求系统回答“这个状态什么时候才能算真正完成”。这就是为什么 fsync 的成本常常并不体现在拷贝本身,而体现在等待之前被延迟的那一整串后台工作被提前结算。

但只要继续往下看,就会发现 fsync 也绝不是一个语义终结者。它能把状态推进到哪里,取决于文件系统如何实现同步语义,也取决于块层和设备是否真的配合完成了后续动作。文件数据可能已经从页缓存写出,但相关元数据也许还在另一条事务路径里;文件系统日志可能已经提交,但设备控制器里的易失写缓存还没有被 flush;设备可能已经回报完成,但真正的介质持久化能力又和断电保护、FUA、缓存策略等实现细节相关。也就是说,fsync 并不是一把自动斩断全部不确定性的刀,它只是把原本默认分散在后台的风险,尽可能向一条显式同步边界上收拢。工程上的真正风险,往往不是“忘了用 fsync”,而是“以为用了 fsync 就天然跨越了所有层次的隐藏前提”。如果没有把这些前提梳理清楚,系统表面上已经有了提交动作,实际故障时恢复出来的状态仍然可能与应用所理解的“已提交”不同。

异常路径最能暴露这套机制的真实面貌。应用进程退出时,数据可能还安静地留在 Page Cache 里;内核 panic 时,部分脏页可能已经进入块层而另一部分还未离开内存;文件系统日志可能只写出了一半事务,或者日志完整但检查点尚未推进;块层请求可能已经投递给设备,设备却还未来得及把缓存内容固化到真正稳定的介质上。系统恢复时真正要面对的,就是这些悬在半空中的中间状态。哪些修改应该保留,哪些必须回滚,哪些可以通过日志重放恢复,哪些会被视为从未提交,这些判断都依赖于系统对提交边界的定义。因此 fsync 与崩溃恢复其实是同一个问题的两面:前者回答“我要把状态推进到哪一条线”,后者回答“如果在线之前或在线之后的某处出事,系统还能认出哪一条线”。数据库 WAL、分布式存储确认协议以及本地文件系统日志机制之所以看起来分属不同层次,底层关心的却是同一件事,即如何在故障后重建一条不含糊的提交边界。

性能上的取舍也能用同一个模型讲清楚。异步写回让系统有机会利用时间窗口进行批处理,因此平均吞吐往往更好,前台调用也更少被设备延迟直接牵制;但同步点一旦增加,例如频繁 fsync、小事务提交或强制 flush,这个窗口就会被不断切碎,后台原本可以自由调度的工作会被拉回前台等待,于是延迟升高、抖动增大、尾延迟恶化。很多工程技术其实都在围绕这个窗口做文章。group commit 试图让多个逻辑提交共享一次更贵的同步边界,WAL 试图把“先保证日志可恢复”与“稍后回写数据页”拆开,批量刷盘试图把多个请求合并成更大的持久化动作,而前台确认与后台整理分离则是在控制用户可感知延迟与系统整体清算效率之间的平衡。只要把这看成“风险窗口大小与同步成本大小之间的交换”,很多看似杂乱的优化策略就会落回同一个因果框架。

如果要把这个主题压缩成一个足够稳固的知识骨架,我会抓住三个持续追问。第一,数据当前处在哪一层状态边界,是只进入了内核缓存,还是已经进入文件系统事务,抑或已经跨过设备承认的持久化门槛。第二,状态推进是由谁触发的,是后台写回策略、内存压力、脏页阈值,还是应用主动要求同步。第三,系统在什么时点向上层宣称“这次修改现在可以被视为成功”,而一旦发生掉电、panic 或设备异常,这个“成功”还能否在恢复后成立。只要这三个问题不丢,后面无论转向 fdatasync、flush 与 barrier、文件系统日志、数据库 WAL,还是 Ceph、BlueStore 这类更高层的存储系统,都只是把同一条状态链延长,而不是换了另一套完全无关的逻辑。

因此,这篇文章和前一篇 Page Cache总览 的关系,不是简单的前后章节关系,而是一种模型递进。前者解释的是数据进入内核之后,为什么会先沉淀为缓存状态;这一篇解释的是这些缓存状态如何被继续推进,以及系统为什么必须发明 fsync 这样的边界工具,来约束这条异步链路何时算真正完成。后面继续写到文件系统日志与崩溃恢复、块层调度、多队列和高速路径时,真正展开的也都会是这条链路在不同层的延续形式,而不是几个孤立术语的堆叠。

Edit (opens new window)
Last Updated: 2026/04/27, 14:16:46
Page Cache总览
文件系统日志与崩溃恢复

← Page Cache总览 文件系统日志与崩溃恢复→

最近更新
01
待完成专题池
04-28
02
待完成专题池
04-28
03
为什么 Kubernetes CSI 插件架构要拆成 Controller、Node 与 Sidecar
04-28
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式