03.08 持久化机制:从历史文件替换到崩溃后恢复
返回第三卷 · 页缓存前置:03.06 · 文件身份前置:03.07 · 本地工具前置:01.12
03.08 持久化机制:从历史文件替换到崩溃后恢复
DeepTutor 初稿经技术与教学审阅后的静态课程。文件、崩溃点与恢复结果都是带假设的纸上模型;没有执行 Go、文件系统写入、断电或故障注入、IM 服务、基准或站点。以下同步流程以Linux、同一目录与文件系统、每步返回成功、旧版本原先已持久、存储栈遵守同步语义为讨论前提;跨平台、网络文件系统和故障硬件需另行验证。
一、“保存成功”至少要分成三个时刻
01.12 的本地历史工具可把虚构 c-a 的消息导出成 history.json。现在设旧文件是完整且已持久的版本 V1,内容只含序号 1;要生成新版本 V2,内容含序号 1、2。若程序说“导出成功”,它究竟指字节已经交给内核、此刻新打开文件可读到 V2,还是机器突然掉电后仍能找到 V2?这三个命题不同。
| 合同 | 要回答的具体问题 | 本章怎样给证据 |
|---|---|---|
| 调用结果 | 本次 write 实际接受多少字节、是否报错? | 检查 n 与 error,处理短写 |
| 运行时可见性 | 新打开 history.json 的读者看到哪一版? | 单次同文件系统 rename 的名称切换 |
| 崩溃后耐久 | 掉电恢复后,V2 内容与名字是否还在? | 文件数据/元数据同步与目录同步,且假设均成立 |
这是本地文件导出问题,不等于 S2 HTTP 的“6 UTF-8 字节正文、accepted_in_memory”合同,也不替 S3 提议的数据库提交确认背书。一个本地文件同步成功,不会自动证明 IM 消息已送达设备。先把每种结果命名清楚,才能设计练习和错误反馈。
| 第一遍 | 第二遍 | 交付物 |
|---|---|---|
文件字节、页缓存、inode/目录项、write | 文件同步、替换与目录同步、崩溃矩阵、业务确认 | 一张 V1/V2 时间线、一张 C0–C4 崩溃表、一份恢复检查清单 |
二、从 Go 字节到稳定介质:中间不是一个动作
03.06 已把进程缓冲与内核页缓存分开,03.07 又把文件内容、inode 元数据与目录名字分开。一次文件写入可粗略画成:
Go 的字节切片
→ write 的实际 n 字节
→ 内核管理的文件页/元数据
→ 文件系统安排的数据块与目录项更新
→ 设备缓存、稳定介质和恢复过程write 可能只接受一部分字节,也可能在后续操作才报告某些错误;成功返回不等于所有涉及的页已写到稳定介质。close 负责结束打开文件的生命周期,不能当作“文件内容与父目录都已持久”的通用替代。若导出 V2 长 100 字节、write 只返回 n=60,下一次应从第 61 个字节继续,或报告失败;不能把 V2 当完整新版本公开。
Linux fsync(fd) 用于同步该文件修改过的数据和相关元数据;fdatasync 可以省略与后续读取无关的某些元数据同步,但这里先用 fsync 建立清楚的合同。对文件调用 fsync 不一定使父目录中的“这个名字指向谁”同步完成。 新建、重命名、删除都改变目录项,因此要单独讨论父目录的同步。实际能否承诺耐久还取决于文件系统、设备缓存、错误处理和硬件是否遵守报告;本章的纸上成功条件明确限定这些前提。
三、直接覆盖旧文件:可见性与崩溃都暴露中间状态
一种看似简单的更新是按 O_TRUNC 打开现有 history.json,把 V1 截空,再逐步写 V2。这个过程从第一步就破坏了旧的完整版本:另一个读者可能在 V2 写完之前看到空文件或部分内容;若程序在中间失败或掉电,恢复时也不能凭“曾经有 V1”推断 V1 仍完整。即使最终 write 返回了目标长度,也还没建立掉电后的耐久边界。
原地覆盖的纸上时间线
t0 history.json = 完整 V1
t1 O_TRUNC 后长度 0
t2 只写入 V2 的一部分
t3 V2 字节写完,但未同步
t4 同步成功(仍须看目录和错误条件)这条反例解释为什么“直接覆盖文件”不适合需要始终保留一份完整快照的本地历史导出。它不是说任何文件写入都必须用临时文件:追加式日志、数据库 WAL、原地页更新有自己的格式和恢复协议。我们当前的需求是一个完整 V1 或完整 V2 的文件版本,因此下一节选择临时文件替换的教学方案。
四、同目录临时文件替换:每步给一个不同的保证
假定 /data/history.json 已有持久的完整 V1;在同一目录选唯一临时名 /data/history.json.tmp-<id>,避免与其他导出冲突。按如下顺序纸上执行,并且每一步都检查错误:
- 创建临时文件,检查路径、权限与返回的打开对象。
- 完整写入 V2;每次
write用实际 n 推进,直到全部字节写完,并检查写入错误。 - 对临时文件调用
fsync,等待其数据及必要元数据完成同步,检查返回结果。 - 关闭临时文件并检查错误;这一步位于文件同步之后、替换之前。
- 在同一文件系统调用
rename(temp, history.json),以新文件替换旧名字。运行中的新路径查询切向 V2;此前已打开旧目标的 FD 仍引用旧文件对象。 - 对
/data父目录调用同步,并检查结果,使目录项切换也进入本章假定的耐久边界。
V1 已持久
→ temp 完整写 V2
→ fsync(temp 文件)
→ close(temp,检查错误)
→ rename(temp, history.json)
→ fsync(/data 目录)
→ 在限定假设下报告“本地导出 V2 已耐久”rename 的关键是名称替换的原子可见性:假设目标原本存在且调用成功,同文件系统上的新路径查询不会在替换中间遇到“目标名字暂不存在”的半步;在我们不修改已公开文件内容的条件下,新打开的读者在切换前看 V1、切换后看 V2。它不让前面的写入自动落盘,也不替后面的目录同步;旧读者若握着旧 FD,也可能继续读 V1。03.07 的轮转先改旧名、再创建新原名是两步,会有空档;本章是临时文件直接覆盖已有目标的一次 rename,两种流程不能混为一谈。
五、C0–C4 崩溃点:只写证据能证明的恢复结论
固定成功步骤序列,设系统可能在任一时刻突然掉电。下表不是对所有文件系统和设备给绝对结果,而是指出哪项合同尚未建立:
| 崩溃点 | 掉电前已经完成 | 运行时目标名与可见内容 | 恢复时能稳妥声称什么 |
|---|---|---|---|
| C0 | 尚未创建/写 temp | history.json 是 V1 | V1 由前提保证已持久;本次 V2 未开始 |
| C1 | temp 只写一部分或写完但未同步 | 目标仍是 V1,temp 可能不完整 | 不能把 temp 当完整持久 V2;目标 V1 的先前持久前提仍在 |
| C2 | temp 完整写且文件同步成功,尚未替换 | 目标仍是 V1 | V2 文件内容已有同步证据;temp 名字是否在掉电后保留仍需目录证据 |
| C3 | rename 成功,父目录未同步 | 新打开目标可见 V2;旧 FD 仍可见 V1 | 新目标名字的崩溃后耐久未证明;不能承诺恢复后一定是 V2 |
| C4 | 父目录同步成功 | 新打开目标可见 V2 | 在本章全部限定条件下,V2 的内容及目标名字有耐久证据 |
尤其不要把 C3 简化成“掉电后一定恢复 V1”或“一定恢复 V2”;文件系统具体实现、先前目录状态和故障时刻影响结果,我们缺的正是新名称的同步证据。C2 也不能把文件同步误读为临时名字持久。若任何步骤返回错误,流程不进入下一行的成功状态;调用者要保留错误和可能已经发生的副作用,不能凭“报告失败”自动断定目录和文件没有改变。
六、原子、顺序、持久各自的边界
原子可见性在这里说的是同文件系统 rename 对一个目标路径的名称切换,不是“V2 内所有字节一次原子写入”。顺序说的是先写完整并同步 temp,再替换名字,然后同步目录;颠倒步骤会改变崩溃窗口。持久性要求在相应文件与目录对象上获得同步成功,且依赖存储栈兑现承诺。三者对应不同问题,因此一个词“原子保存”不足以描述完整协议。
文件系统日志或写时复制机制可以帮助文件系统维护自己的元数据一致性,但不会替应用验证 V2 是否包含正确消息、是否越权、是否重复,也不能把业务消息协议自动变成可靠投递。若 V2 在编码时漏掉序号 2,所有 fsync 都只会让一个错误版本更耐久。先用 10.01/10.02 的需求和测试方法检查文件内容,再讨论同步与恢复。
Linux rename 与 Go os.Rename 的跨平台行为不能无条件等同;跨文件系统通常无法用一次 rename 做原子替换。网络文件系统、存储设备虚报刷新、硬件故障、目录同步不受支持或返回错误,都超出本章成功假设。正式产品需要在其目标环境中做故障注入和恢复验证;本页没有声称已经得到那种证据。
七、Go 调用入口与 IM 业务确认点
Go 初学者可把上述步骤对应到 os.OpenFile、(*File).Write、(*File).Sync、(*File).Close 与 os.Rename;父目录也要作为可同步的目录对象打开并检查其同步结果。这里列的是操作顺序,不是一个已实现、可复制投入生产的 Go 函数。路径唯一性、权限、并发导出、清理旧 temp、跨平台支持和失败重试都需要完整实现与验证。
为本地 CLI 导出定义三个不同的反馈点:
| 可报告的状态 | 必须已经检查 | 仍不能声称 |
|---|---|---|
| “生成中” | 输入与旧版本基线 | V2 可见或耐久 |
| “V2 当前可见” | V2 完整、文件同步与 rename 成功 | 父目录未同步时的掉电耐久 |
| “V2 本地耐久” | 文件和父目录同步均成功,环境假设成立 | 设备已收到 IM 消息、数据库已提交 |
若响应/终端输出在最后一步附近丢失,调用者可能面对未知结果:导出可能已成功,但反馈没到。恢复时应读取目标文件版本标识与校验信息,核对是 V1 还是 V2,再决定重试;不能盲目把“没看到成功提示”当作“没有副作用”。这与 S2 accepted_in_memory、S3 提议的 stored_in_teaching_db 是不同教学合同;这里没有变更现有 6 UTF-8 字节 HTTP 正文上限。
八、交付崩溃评审与分层练习
学习者交付:画出 V1 直接覆盖的反例;写出 temp 完整写、文件同步、替换和目录同步的成功条件;标注 C0–C4 时目标名、已打开旧 FD 和恢复时可证明的状态;最后为“文件同步失败、rename 成功但目录同步失败、成功反馈丢失”各列一条回查规则。全部是静态纸上评审,不是已运行的掉电实验。
分层练习:先答,再展开反馈
1. `write` 返回成功等于掉电后可恢复吗?
不等于;写入的页和目录项可能尚未按耐久要求同步。
2. 一次 `write` 请求 100 字节却返回 60,已经完成多少?
本次只确认 60 字节;须继续处理剩余 40 字节或报告失败。
3. `close` 可以通用替代文件和父目录同步吗?
不能。关闭文件生命周期与两类持久化合同不同。
4. `fsync` 临时文件主要解决哪一层?
为临时文件内容及相关元数据提供同步证据,不自动同步其父目录名字。
5. 为什么直接 `O_TRUNC` 覆盖 V1 有风险?
旧完整内容会先被截空,读者和崩溃恢复都可能遇到空或部分新版本。
6. 本章 temp 与目标为何放在同一目录?
便于处在同一文件系统,使用单次名称替换并同步同一个父目录;仍要确认实际文件系统条件。
7. temp 写入一半能先 `rename` 为正式目标吗?
不应按本章合同这么做;新路径会公开不完整 V2。
8. `rename(temp,target)` 主要给什么可见性?
在本章同文件系统成功条件下,目标路径的名称替换是单步原子可见的。
9. `rename` 成功会让已打开 V1 的 FD 自动读 V2 吗?
不会;旧 FD 仍指向先前打开的旧对象。
10. 替换前 V1 为什么要先假定已持久?
否则早期崩溃点连旧版本的恢复基线都没有,不能说“至少有 V1”。
11. C1 崩溃时可把 temp 当完整持久 V2 吗?
不能。temp 可能只写了一部分或尚未同步。
12. C2 文件同步后、改名前,目标路径仍指向谁?
仍指向完整 V1;V2 临时文件内容有同步证据,但其名字未获父目录同步保证。
13. C3 改名成功、父目录未同步,运行时新打开目标见谁?
可见 V2;这仍不证明掉电后新名字耐久。
14. C3 掉电后一定恢复 V1 吗?
不能断定。新名称未获持久证据,具体恢复结果取决于文件系统与故障时刻。
15. C4 的耐久结论要带哪些前提?
所有步骤成功、Linux 同一文件系统、旧版原先持久,且文件系统与设备兑现同步语义。
16. 文件 `fsync` 已成功就能保证新文件名掉电后存在吗?
不能,父目录项还需单独同步。
17. 文件系统日志能检查 V2 的消息序号是否正确吗?
不能。业务内容正确性由应用规则和验证负责。
18. temp 写完但 `fsync` 返回错误,可继续按成功路径改名吗?
不能把它当作已获文件耐久保证;应报告并按失败状态处理。
19. `rename` 返回成功但目录同步失败,可报告“本地耐久”吗?
不能。运行时可能可见 V2,掉电后名称恢复合同尚未证明。
20. 成功提示丢失,就能断定导出没有副作用吗?
不能。应回查目标版本与校验信息,再决定是否重试。
21. 本地历史文件耐久能证明设备已收到 IM 消息吗?
不能。文件导出、内存受理、数据库提交和设备交付是不同确认点。
22. 把 temp 放在另一文件系统,还能假定一次 `rename` 原子替换吗?
不能。跨文件系统通常不满足本章单次同文件系统替换前提。
来源与下一步
- OSTEP:文件与目录及崩溃一致性:文件系统对象、缓存与多对象更新的崩溃问题。
- Linux
write(2)、fsync(2)与rename(2):短写、文件/目录同步与同文件系统名称替换。 - Go
os包:文件打开、写入、同步、关闭与重命名的 API 入口;Linux 纸上语义不可直接推广为所有平台保证。
按学习路线,下一章03.09 I/O 与事件通知进入 I/O 阻塞、就绪通知与短读短写的运行机制。离开本章前,应能说出V1/V2 各在什么时刻可见、哪一步使文件内容有同步证据、哪一步使名字有同步证据,以及反馈丢失时应怎样核对版本。