Checkpointing: sharp, fuzzy, incremental
What does each kind of checkpoint pay, and why do stalls turn into background rates with a cliff?
A checkpoint writes dirty pages to disk and records a position in the log, so that recovery can start from there instead of from the beginning of time.
Sharp (consistent) checkpoint. Stop the world: pause writers, flush every dirty page, write a checkpoint record. The image on disk is transaction-consistent, recovery is trivial, and the cost is a stall of dirty_pages × page_size ÷ write_bandwidth.
This design is not rejected because its total work is too high. It is rejected because of its shape. A recurring 20-second stall is worse than a permanent 5% throughput tax, even if the tax costs more work in total. Every queue upstream must be sized for the stall, and every timeout downstream must be longer than it. This is the rule that variance costs more than volume, and here it is the only reason the textbook design is used almost nowhere.
Fuzzy checkpoint. Do not stop. Write a begin-checkpoint record that captures the dirty page table and the active transaction table. Let the background flusher continue at its own pace. Then write an end-checkpoint record. The image on disk is consistent at no single instant: it is a smear across the checkpoint window. Recovery copes by starting replay at the smallest recoveryLSN (the LSN of the oldest change not yet on disk) in the dirty page table, and re-applying everything. Pages that were already flushed past that point silently refuse the old records, because pageLSN ≥ recordLSN.
Notice the dependency, because it is the best example of mechanisms building on each other: the LSN rule bought in Logs and undo to make redo safe to repeat is what makes fuzzy checkpoints possible. Without repeatable redo, you need a consistent image. With it, you can accept an inconsistent one and let recovery sort it out. One mechanism, bought for one reason, pays for a second, unrelated one. A design that many other things depend on in this way is a sign of a sound foundation.
Incremental checkpoint. Remove the event completely. Keep a flush list ordered by each page's oldest modification LSN, and flush continuously from the head of that list. The checkpoint position then moves forward smoothly instead of jumping. InnoDB's adaptive flushing is this: the flush rate is driven by checkpoint age (how far the log head has run ahead of the oldest unflushed change) as a fraction of the redo log capacity.
This is a general pattern: any periodic maintenance event whose cost is a visible stall eventually becomes a continuous background rate.
| Stall version | Continuous version |
|---|---|
| Stop-the-world garbage collection | Concurrent garbage collection |
| Major compaction | Levelled compaction |
| Sharp checkpoint | Adaptive flushing |
| Full backup | Continuous replication |
But be honest about what the conversion does not do. Costs are conserved. Smearing does not remove the stall. It turns a predictable periodic stall into a load-dependent threshold. When the write rate stays above the flush rate for long enough, checkpoint age approaches the redo capacity, and the engine has no choice but to slow down writes, first softly, then hard. You traded a stall you could schedule for a cliff you cannot, and its position depends on your workload. The system is smoother 99% of the time and less predictable in the 1% that matters.
The coupling nobody expects
In Postgres, the setting full_page_writes makes the first change to a page after each checkpoint write the whole page image into the WAL. This protects against torn pages (When the storage lies). Therefore:
- More frequent checkpoints → more "first touches" per unit of time → more full page images → more WAL volume.
For scattered single-row updates, an 8 KB full-page image replaces a record of about 100 bytes: 8,192 ÷ 100 ≈ 80× amplification on first touch. So the checkpoint interval, which you adjusted to control recovery time, is also the main control on write amplification, and it moves the two in opposite directions. The best value is somewhere in the middle and depends on the workload: it depends on how many distinct pages your writes touch per interval, which is a property of access locality, not of write rate. This is the cleanest demonstration that the knobs in this area are not independent.
The same mechanism in a distributed system
Flink (a stream-processing engine) checkpoints with asynchronous barrier snapshotting (the Chandy–Lamport idea): special barrier markers are injected at the sources and flow through the pipeline together with the records. An operator snapshots its state when the barriers have arrived on all its inputs.
- Aligned checkpointing waits for the slowest input. The image is consistent, but the wait pushes backpressure (a slow part forcing upstream parts to slow down) into the pipeline.
- Unaligned checkpointing (Flink 1.11 and later) saves the in-flight records instead of waiting. No stall, but the checkpoint contains the channel contents and is larger.
That is sharp versus fuzzy, rediscovered on a distributed system: pay a stall for a clean image, or pay storage and complexity for a smeared one. If you filed "database checkpointing" and "stream-processing checkpointing" as two unrelated topics, merge them. The only difference is that a distributed system cannot take a global instant, so it uses a barrier travelling through cause and effect as a substitute for one.