Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

WAL、fsync、崩溃一致性

一句话

工程上 90% 的"数据丢失 bug" 都因为误以为 write 成功就持久了, 误以为 fsync 一定成功就持久了. 这一章把 Linux + ext4 + NVMe 下的崩溃一致性模型拆开, 让你看到write / fsync / fdatasync / 终端 cache / 设备电池 这五层覆盖在 CRIT between user app 与 NVM 实际写完之间.

1. 五层缓存

让我们从用户 write(fd, buf, n) 把字节真正落到 NVME cell 之间拉一次完整路径:

1. user 写 page cache: write 调用 → mark dirty
2. fsync(fd) 等 page cache 实际 writeback
3. writeback → block layer → 组 bio → submit
4. NVMe driver → submission queue → 设备
5. NVMe controller 实际 NAND write + flush + checkpoint
6. NAND cell 物理 charge 写入完成 ack 

每层都是一个 cache, 每层都可丢失. 这就是为什么 fsync 也不 100% 安全:

ext4 fsync → journal commit → 块层 write → NVMe cache → NAND.

如果 NVMe 设备有 cache 而 fsync 提交需求没 pass through NVMe cache flush, fsync ack 但 cache 没到 NAND, 断电仍丢. 这是真实硬件隐患.

2. fsink 提交语义

ext4 / XFS / Btrfs 三种 fs 主流方式:

ext4 default data=ordered:
  - dirty pages 先 flushed to data area,
  - journal commit 中写 metadata commit block,
  - fsync 等 journal commit 完成才 return.

ext4 data=writeback:
  - 仅 metadata journal;
  - data 可能 metadata 之后写 → 看到 stale block.

ext4 data=journal:
  - 全部 data journal;
  - 安全但写放太严重 2× overhead.

XFS 默认 delay-write:
  - 类 ordered, 但数据 first delayed write then block flush.

Btrfs:
  - 每 fsync 触发 transid++ commit CoW;
  - 同 XFS 类性能.

主要不同: 默认 ext4 data=ordered 保证了 data 不会比 metadata 先丢——避免 commit metadata 后看到 stale block. 这就是早期 Linux 2.4 NFS 比 ext3 data=journal 用 5 年才改的原因: 写 + crash 后 fs state 一致.

3. fdatasync 卷语义

fdatasync 不强制 metadata 中 atime/mtime 写盘, 但强制 data 写盘. 对 append-only log 是 fdatasync 性能更好.

原因:

fsync:
  ## 必等 metadata (size, mtime 等) 写盘
  ## 强制 journal commit

fdatasync:
  ## data 写盘
  ## 仅在 size 改变时 fs 元数据也 stdsync flush

append-only log 上: 强制 size metadata 也 flush 当且仅当最后大小必须被下次 fs 状态读取. 否则 fdatasync 安全.

4. NVMe Force Unit Access (FUA) 与 Volatile Write Cache (VWC)

NVMe 命令支持 bit:

  • FUA = Force Unit Access. 命令绕 device cache 直接到 NAND;
  • 只 NVM 终端 fdatasync 是不够的 —— 内核驱动通过 IO command Flush 给设备写盘.

SCSI 类硬盘 / 部分 NVMe 有 volatile write cache. 设备自我 ack fast 但吸收在 device buf. 断电时 buf dump. Linux write cache = write throughwrite back 区分:

hdparm -W0 /dev/sda   # 关闭 write cache

关掉 write cache = performance drop 30-50% 但 fsync ack 即感真持久化. HRT / database 推荐.

5. 崩溃一致性概念

durability: 一次 fsync 后数据一定持久化 ⇒ ack 后断电也能 find.

atomicity: 写 multi-block 全完成或全未.

isolation: 在崩溃之后看到的 fs state, 等同 某个 in-order 之前完成 fsync 的快照.

ext4 ordered 默认满足: durability (fsync 等到 commit) + atomicity (journal) + isolation (transid).

但 ext4 writeback 不满足 atomicity (data 不 journal) ⇒ fsck 后你也许看到 partial data write + commit metadata.

6. WAL (Write-Ahead Log) 数据库的核心设计

数据库为了保证 ACID, 什么 dirty pages 都先写 WAL redo log + fsync.

事务 commit 步骤:
1. 修改 buffer pool pool page (in-mem);
2. 写 WAL redo log append (in-mem);
3. fsync(WAL fd);      ← 等待 truly persistent;
4. ack 客户端 "commit ok";
5. 后台异步 flush dirty page buffer pool → disk (写 data file).

崩溃 ~ 准 back eventually: WAL fsync 后 ack 即持久, data file 后续 lazy flush 是 ok 的. crash 后, 启动读 WAL, redo 重做 final commit.

这就是 InnoDB / PostgreSQL / RocksDB / CockroachDB 共同的 WAL 模型. 核心思想: 持久化只需要 redo fsync 一次, data 落盘可以 amortized.

7. 数据库的 group commit 优化

每 fsync ~ 100 μs - 5 ms, 不能每事务都做. → 组 commit: 多个事务的 WAL 一起进程, 一次 fsync 同时段 完成所有事务.

所有 commit_req 入队列,
线程 1 持有队列, 等待时间窗口 (e.g. 100 μs) 让 batch grow,
后 fsync 1 次, ack 全队员.

MySQL 5.7 提 binlog_group_commit, MySQL 8 后 Innodb redo log 也用 group commit, 单次 fsync 容纳 ~ 100+ 事务.

P99 写延迟: 1 个 commit ~ 5 ms (fsync overhead)→ group 100 个事务: ~ 5 ms / 100 = 50 μs / commit.

8. 数据库 + ext4 / XFS / Btrfs 调优建议

# ext4 database mount options
mount -o noatime,nodiratime,data=writeback,barrier=1 /dev/sda1 /data
# noatime: 不更新 atime (减少 metadata IO)
# data=writeback: 不 journal data (WAL 已经保证 atomicity)
# barrier=1: 关键 — 所有 metadata journal commit 必 flush device cache

barrier=1 是关键: 关闭 barrier 意味着 journalist commit 在 ext4 层完成 (in mem journal) 而设备 cache 没写. 断电破坏 fs ordering.

9. 多语言 / 多 runtime 同一抽象

语言 / runtimefsync / commit 模型
C / C++ stdexplicit fsync + 一次 syscall
Go os.File同上
Rust std::fsFile::sync_all → fsync
Java NIOFileChannel.force(true)
Pythonfile.flush() + os.fsync(fd)
SQLitedefault 通过 WAL + fsync
PostgreSQLfsync + group commit + WAL
RocksDB别提了 + 每 wrapper fsync
Redis每 BGSAVE + RDB + AOF fsync (slow mode)

所有持久化数据库都需要 fsync at least once per logical update. fsync 是 OS / 设备间义务.

10. 旋涡调试

# 强制清 cache
echo 3 > /proc/sys/vm/drop_caches

# 看 ext4 commit interval
cat /proc/mounts | grep data

# 看 NVMe cache 状态
nvme id-ctrl /dev/nvme0 | grep -i cache

# 关 write cache (warn: 性能降)
hdparm -W0 /dev/sda1

# 在 PG 里面 pgbench with synchronous_commit
pgbench -c 16 -j 4 -T 60 -M prepared --protocol=prepared mydb

11. 这一章带走的东西

  • write 不保证持久化, fsync 才真持久化, 但仍依赖 device cache;
  • ext4 data=ordered default 是历史正确选择, database 推荐 data=writeback + barrier=1;
  • WAL 模型让 fsync 摊还到 commit batched (group commit);
  • fsync P99 是数据库 commit P99 上界, group commit 拿到 100× throughput;
  • NVMe cache / FUA / barrier=1 是 fsync 与 device 层的合作 contact.

下一节 → 进程与线程调度