Computer SystemsWAL and fsync

WAL and fsync

Write-Ahead Log, fsync, LSN, pageLSN, checkpoint, and crash recovery mental models for durable data systems.

WAL và fsync: vì sao database ghi log trước khi ghi data thật

Vì sao note này nằm trong Computer Systems?

WAL và fsync() là phần nền của database/storage systems, nhưng tác động trực tiếp tới data platform:

  • metadata store của Airflow/Dagster/Hive Metastore có thể write chậm dù CPU thấp;
  • Spark Streaming checkpoint, Delta transaction log, audit log đều phụ thuộc vào durability path;
  • transaction log trên object storage/lakehouse vẫn phải giải quyết bài toán tương tự: commit đã bền vững chưa, recovery đọc từ đâu;
  • benchmark write throughput dễ sai nếu bỏ qua fsync, group commit, disk latency, hoặc cloud volume IOPS.

Mental model ngắn:

write()  = đưa data cho OS/kernel buffer, chưa chắc xuống storage thật
fsync()  = ép dirty data/metadata của file xuống storage bền vững
WAL      = nhật ký thay đổi append-only, được fsync trước khi báo commit

Vấn đề: ghi data page trực tiếp không atomic

Một transaction nhỏ thường không chỉ sửa một nơi. Ví dụ insert một order có thể đụng:

- table data page
- primary-key index page
- secondary index page
- MVCC/visibility metadata
- free-space metadata

Nếu database ghi thẳng các page này xuống disk rồi crash giữa chừng, disk có thể rơi vào trạng thái nửa cũ nửa mới:

ghi table page xong
ghi primary index page xong
crash trước khi ghi secondary index page

Không có log, database rất khó biết ý định ban đầu là gì: transaction đã commit chưa, page nào đã được apply, page nào còn thiếu, page nào cần rollback.

Write-Ahead Log là gì?

WAL, Write-Ahead Log, là nguyên tắc:

Trước khi data page chứa một thay đổi được flush xuống disk, log record mô tả thay đổi đó phải được ghi vào WAL trước.

Commit path thường giống:

1. Transaction sửa data trong buffer pool
2. Database append WAL records mô tả thay đổi
3. Append COMMIT record
4. fsync WAL
5. Báo COMMIT thành công cho client
6. Data pages thật được background writer/checkpoint flush sau

WAL trở thành source of truth để recovery:

COMMIT record durable     -> transaction phải được redo cho đủ
COMMIT record chưa durable -> transaction xem như chưa xảy ra / bị undo / không visible

Vì sao append WAL trước lại tốt hơn?

1. Sequential write thân thiện hơn random write

WAL là append-only:

WAL file:
[record][record][record][commit] ->

Trong khi data page thật thường là random writes rải rác:

table page 51
index page 9
index page 482
metadata page 3

Sequential write dễ batch, dễ group commit, và thường có latency ổn định hơn so với ép nhiều page rải rác durable trong cùng một transaction.

2. Commit atomic hơn

Atomicity không đến từ chuyện append “tự nhiên là không thể hỏng”. WAL record vẫn có thể bị crash giữa chừng. Điểm quan trọng là WAL được thiết kế có boundary rõ:

valid WAL prefix
partial/corrupt tail

WAL record thường có length/checksum. Khi crash giữa lúc append, recovery đọc tới đoạn tail hỏng thì cắt bỏ phần không hợp lệ. Nếu COMMIT không nằm trong valid durable prefix, transaction không được công nhận.

3. Data page flush được làm lười hơn

Sau khi WAL đã durable, data pages có thể còn nằm trong buffer pool:

COMMIT acknowledged
WAL durable
Data pages dirty in memory

Nếu crash lúc này, database replay WAL để tái tạo các committed changes còn thiếu. Nhờ vậy transaction latency không phải chờ toàn bộ table/index pages được flush ngay lập tức.

Trade-off là write amplification:

1 lần ghi WAL
+ 1 lần ghi data page thật sau đó

Database chấp nhận ghi thêm để đổi lấy durability, atomicity, recovery rõ ràng, và commit path dễ tối ưu hơn.

fsync nằm ở đâu?

write() thường chỉ đảm bảo data đã vào kernel buffer/page cache:

application -> write(fd) -> kernel/page cache

fsync(fd) yêu cầu OS đẩy dirty data/metadata liên quan tới file xuống storage bền vững:

kernel/page cache -> storage device confirms durability

Với WAL, điểm quyết định là:

append COMMIT record
fsync WAL
return COMMIT success

Nếu database báo commit trước khi WAL được fsync, crash có thể làm mất transaction mà client tưởng đã thành công.

LSN và pageLSN: database biết đã xử lý tới đâu bằng cách nào?

WAL có thứ tự logic gọi là LSN, Log Sequence Number:

LSN 100: T1 update page A
LSN 120: T1 update page B
LSN 150: T1 COMMIT

Mỗi data page cũng thường lưu pageLSN:

page.pageLSN = LSN mới nhất đã được apply vào page này

Khi recovery, database dùng pageLSN để quyết định có cần redo không:

if page.pageLSN < wal_record.LSN:
    apply wal_record
    page.pageLSN = wal_record.LSN
else:
    skip, page already has this change

Ví dụ:

WAL:
LSN 100: update page A
LSN 120: update page B
LSN 150: commit T1

Disk after crash:
page A pageLSN = 100
page B pageLSN = 0

Recovery:

LSN 100 -> page A đã có rồi, skip
LSN 120 -> page B thiếu, redo
LSN 150 -> T1 committed, transaction hợp lệ

Checkpoint: recovery không đọc WAL từ đầu lịch sử

Nếu crash mà phải scan WAL từ ngày database sinh ra thì quá chậm. Database định kỳ ghi checkpoint:

CHECKPOINT at LSN 10000
- transaction nào đang active
- dirty page nào có thể chưa flush
- WAL segment nào còn cần giữ

Sau crash, recovery bắt đầu từ checkpoint gần nhất:

last checkpoint
  -> scan WAL forward
  -> redo committed changes nếu pageLSN thiếu
  -> undo/ignore uncommitted transactions

Recovery flow kiểu ARIES

Nhiều database dùng biến thể của ARIES với 3 phase:

1. Analysis
   Đọc WAL từ checkpoint để biết:
   - transaction nào committed
   - transaction nào loser/uncommitted
   - dirty page nào có thể chưa flush

2. Redo
   "Repeat history": apply lại các WAL records cần thiết dựa trên pageLSN.
   Mục tiêu là đưa database về trạng thái ngay trước crash.

3. Undo
   Rollback các transaction chưa commit.
   Có thể ghi thêm compensation log records để nếu crash trong lúc undo thì vẫn tiếp tục recovery được.

Pseudo-flow:

for record in WAL_from_checkpoint:
    update transaction_table
    update dirty_page_table

for record in WAL_from_redo_start:
    if page.pageLSN < record.LSN:
        apply(record)
        page.pageLSN = record.LSN

for txn in loser_transactions:
    undo txn backward
    write compensation log record

Ví dụ crash recovery

WAL:

LSN 10: CHECKPOINT
LSN 20: T1 update page A
LSN 30: T2 update page B
LSN 40: T1 COMMIT
LSN 50: T3 update page C
CRASH

Disk lúc crash:

page A pageLSN = 0     # chưa flush
page B pageLSN = 30    # lỡ flush dù T2 chưa commit
page C pageLSN = 0

Recovery:

Analysis:
- T1 committed
- T2 uncommitted
- T3 uncommitted

Redo:
- LSN 20 page A: pageLSN 0 < 20 -> redo
- LSN 30 page B: pageLSN 30 >= 30 -> skip redo
- LSN 50 page C: pageLSN 0 < 50 -> redo

Undo:
- T2 chưa commit -> undo/mark aborted/không visible
- T3 chưa commit -> undo/mark aborted/không visible

Kết quả cuối:

T1 giữ lại
T2 rollback/không visible
T3 rollback/không visible

PostgreSQL nuance: không phải lúc nào cũng undo physical page ngay

PostgreSQL dùng MVCC, nên row version của transaction chưa commit có transaction id. Sau crash, transaction chưa commit bị xem là aborted; row version đó không visible với query bình thường và sẽ được vacuum dọn sau.

Vì vậy PostgreSQL vẫn dùng WAL, LSN, checkpoint, redo để đảm bảo committed changes được phục hồi, nhưng “undo” có thể là vấn đề visibility/MVCC thay vì đảo ngược physical page ngay lập tức như một số engine khác.

Liên hệ với data systems

Metadata store write chậm dù CPU thấp

Nếu nhiều pipeline cùng update metadata/checkpoint/audit log liên tục:

many small transactions
-> many WAL appends
-> many fsync/group commits
-> commit latency cao dù CPU rảnh

Dấu hiệu thường thấy:

  • DB CPU thấp nhưng write latency cao;
  • disk IOPS/latency cao;
  • nhiều transaction nhỏ;
  • p95/p99 commit latency spike khi storage hoặc cloud volume nghẽn.

Lakehouse transaction log cũng là cùng một họ vấn đề

Delta transaction log không giống WAL của database truyền thống 1-1, nhưng cùng giải bài toán:

commit intent/order
atomic visibility
recovery/read consistent snapshot

Thay vì sửa dữ liệu cũ tại chỗ, lakehouse thường append file mới và commit metadata/log để readers biết snapshot nào hợp lệ.

Chốt mental model

WAL = nhật ký pháp lý của thay đổi
LSN = số thứ tự trên timeline
pageLSN = data page đã apply tới mốc nào
checkpoint = recovery bắt đầu lại từ đâu
fsync = ép phần quan trọng của log xuống storage thật

Câu nhớ nhanh:

WAL không chỉ giúp ghi nhanh hơn. Nó cho database một timeline durable để biết sau crash cần redo phần nào, undo/ignore phần nào, và transaction nào thật sự đã tồn tại.

On this page