Expand description
§Metadata Reservations in Fxfs
Fxfs maintains a metadata reservation (M_total) in the allocator so that space-freeing
operations and journal compactions can proceed even when the filesystem has zero free space:
- Funded transactions: Normal transactions reserve metadata space from the allocator up
front (
4xtheir journal byte size) and transfer that reservation intoM_totalon commit. - Borrowing transactions: Operations that free space (such as unlinking files, purging
graveyard tombstones, or trimming extents) must succeed even when the disk is full, so they
borrow from
M_total(Borrowed) instead of reserving from the allocator. - Minor vs. Major Compaction: Flushing an object store normally performs a minor
compaction, writing recent journal mutations (including deletion tombstones) into a new small
layer file stacked on top of existing immutable layers. Minor compactions consume blocks
from
M_totalto write the new layer file and do not free older layers. Borrowed metadata space is only reclaimed when a store performs a major compaction, which merges all layers into a single base layer, drops all tombstones, and deletes the old layer files. To prevent repeated minor compactions from drainingM_total, Fxfs forces a major compaction once borrowed space reaches50%of the available borrowing headroom.
§Why Per-Store Borrowing Tracking Is Required (and the Locked-Store Problem)
Encrypted volumes can be locked (ObjectStore::lock), dropping their decryption keys from
memory. If a locked store has unflushed journal mutations when the journal compacts,
ObjectStore::flush_locked appends them to a sidecar encrypted_mutations file in
root_store; critically, a locked store cannot decrypt and merge its layers to perform a major
compaction until it is unlocked again.
If borrowed space and major-compaction thresholds were tracked as a single global scalar across
the entire filesystem (borrowed = R_total - M_total >= R_total / 2, as Fxfs did prior to V61),
a locked store could tie up the filesystem’s metadata space in two ways:
- Consuming other stores’ headroom before locking: Because a global
R_total / 2threshold includesjournal::RESERVED_SPACEplus1.5 * Lforroot_store, theallocator, and all child volumes, a single volumeS_lockedcould perform many borrowing deletions and minor compactions while unlocked without ever crossing globalR_total / 2, borrowing far more than its own0.5 * Lheadroom and eating into the reservations of other volumes androot_store. IfS_lockedwas then locked, all of that borrowed space remained frozen in its unmerged layers while locked. - Starving other stores and forcing endless major compactions: With
S_lockedtying up most of globalR_total / 2, any small borrow by an active unlocked storeS_activeorroot_storepushed globalborrowedpastR_total / 2. Every subsequent journal flush then forced a major compaction on every unlocked store (S_activeandroot_store) even when they had no tombstones to reclaim, whileS_locked(the store actually holding the debt) was locked and skipped (or worse, exhaustedM_totalaltogether).
Assigning each child store S its own borrowing budget req_S and borrowed counter B_S
prevents both failure modes:
- While
S_lockedis unlocked, it is forced to major-compact as soon as its own borrowing reachesB_S >= req_S / 2(resettingB_S = 0and freeing its tombstone layers), regardless of how large other stores are. WhenS_lockedis locked, the uncompacted debt it can hold is strictly bounded by its ownreq_Sshare (whichS_lockeditself funded when its layers were created) and can never dip intoreq_otherorreq_sys. - While
S_lockedis locked, each unlocked storeS_activeis evaluated solely against its ownB_{S_active} >= req_{S_active} / 2,root_storesubtractssum(B_S)when computing system borrowed space, andS_lockedis ignored by compactions until unlocked.
§Reservation Model
Each LSM tree (the allocator, the root store, and each child volume) with total layer size L
has a target reservation budget of 1.5 * L, which decomposes into:
- Compaction scratch space (
1.0 * L): Worst-case temporary space needed to write a new compacted layer file before tombstoning the old layer files. Because only one tree is compacted at a time, this scratch space is shared across all trees (max(L)across all trees). - Borrowing and system headroom (
0.5 * L): Space that can be borrowed across compactions (e.g. unmerged minor layers of deletion tombstones, orencrypted_mutationsin a locked volume) before a major compaction is required to reclaim it.
To isolate borrowing between volumes while sharing compaction scratch space, ObjectManager
computes required reservations as:
- System Requirement (
req_sys): Covers shared compaction scratch space across all trees (max(L)), the allocator and root store headroom (0.5 * (L_alloc + L_root)), a scaling share of child stores (1/8 * sum(L_child)forroot_parent_store,root_store, andallocatorrecords of the child stores’ layer files themselves; if extreme layer file fragmentation ever exceeded this share,R_totalis unaffected androot_storewould simply major-compact slightly earlier), unflushed system journal usage (4 * J_sys), andjournal::RESERVED_SPACE. - Per-Child-Store Requirement (
req_S): For each child volumeS, covers that volume’s borrowing headroom (3/8 * L_S) and its unflushed journal mutations (4 * J_S). - Total Required Reservation (
R_total):req_sys + sum(req_S).
Rather than partitioning the physical reservation M_total (metadata_reservation.amount())
into per-store balances (which would require scaling per-store balances down whenever M_total
has a shortfall and could cause repeated major compactions on subsequent transactions),
ObjectManager tracks:
M_total: The total physical bytes currently held inmetadata_reservation.store_borrowed[S]: The metadata space borrowed by each child storeS’s flushed non-base layers (orencrypted_mutations), whilejournal_checkpoints[S]tracks unflushed journal borrowing before (Old) and after (Current)BeginFlush. StoreS’s total borrowed space isB_S = store_borrowed[S] + 4 * journal_checkpoints[S].borrowed_usage().
§Accounting Lifecycle
- Transaction Commit: Non-borrowing transactions (
New/Hold) transfer4 * txn_sizeintoM_totaland recordusageinjournal_checkpoints[S]. Borrowing transactions (Borrowed) transfer nothing intoM_totaland record bothusageandborrowed_usageinjournal_checkpoints[S]for each mutated child storeS. - Tree Compaction (
BeginFlush&EndFlush): WhenBeginFlushis applied to child storeS,journal_checkpoints[S]movesCurrentintoOldso any concurrent transactions betweenBeginFlushandEndFlushaccumulate into a freshCurrentspan. AtEndFlush(borrowed),Oldis removed (leaving only any post-BeginFlushborrowing inCurrent) andstore_borrowed[S]is set toborrowed(0for a full compaction, or the updated flushed layer borrowing computed bystore_borrowed_for_flushfor a minor or locked flush). - Major Compaction: Each child store
Sindependently forces a major compaction when its borrowed space exceeds half of its layer headroom (B_S > h_S / 2, whereh_S = 3/8 * L_S) and the store can be compacted (needs_flush(S)orlayers.len() > 1).root_storeforces a major compaction when system borrowed space ((R_total - M_total).saturating_sub(sum(B_S))) exceeds half of the system’s non-scratch layer and journal headroom (0.5 * (L_alloc + L_root) + 1/8 * sum(L_child) + RESERVED_SPACE). Because a full compaction ofSunconditionally resetsstore_borrowed[S] = 0and dropsCheckpoints::Oldeven whenM_total < R_total, a single subsequent transaction onSwill not trigger another major compaction.
§Journal Replay and Software Upgrades
The target reservation formulas (such as journal::RESERVED_SPACE or the layer/journal
multipliers) can change between software versions, and during journal replay the full set of
child volumes and their layer sizes are not yet known. If the super-block only recorded global
borrowed space B and computed the mount-time reservation as R_total - B, any software update
that increased R_total would try to reserve more space from the allocator at mount time than
was actually set aside before the unmount or crash, which could fail with NoSpace on a nearly
full filesystem.
Instead, SuperBlockHeader::reserved_metadata_space records M_total at key 0 and each child
store’s flushed layer borrowing store_borrowed[S] at key store_id as of
super_block_journal_file_offset, and the journal distinguishes non-borrowing (Commit) from
borrowing (CommitBorrowed(u64)) transactions:
- Seeding from the Super-Block:
init_replay_metadata_reservationseedsM_totalfrom key0andstore_borrowed[S]from eachstore_id != 0. - Replaying Journal Transactions (
replay_mutations):- All mutations in
transactions(whichJournal::read_transactionsalready filtered to those unflushed as ofsuper_block.journal_file_offsets) run throughInner::apply_mutationandrecord_journal_usage, takingjournal_checkpointsandstore_borrowedthrough the sameBeginFlush->Old->EndFlush(borrowed)state transitions as live mode. - For transactions after
super_block_journal_file_offset:Commit(non-borrowing): Adds4 * txn_sizetoM_total.CommitBorrowed(reserved)(borrowing): UpdatesM_totaltoreserved(the post-apply_transactionmetadata_reservation.amount()recorded in live mode).
- All mutations in
- Reconciling with Current Software Requirements (
init_metadata_reservation): Once replay completes and all child volumes are opened,min(M_total, R_total)is reserved from the allocator (whiletotal_store_borrowedclamps eachstore_borrowed[S]toh_Son read):- If required reservations increased (
M_total < R_total): OnlyM_totalis reserved from the allocator at mount time, never asking for extra space. - If required reservations decreased (
M_total > R_total): The reservation is capped atR_total, leaving the excess unreserved in the allocator. - Legacy (< V61) migration: Pre-V61 super-blocks only recorded global borrowed space
Bcomputed against the legacy required reservation (legacy_required = max(1.5 * L_i) + 4 * J_total + reserved_space), soM_total = min(legacy_required - B, R_total)is reserved and any non-journal deficit is distributed proportionally across child stores’ layer headroom to seedstore_borrowed[S].
- If required reservations increased (
Structs§
- Object
Manager - ObjectManager is a global loading cache for object stores and other special objects.
- Reservation
Update ReservationUpdateis an associated object attached toMutation::EndFlushthat updates the tree layer size (layer_size) for an object.
Enums§
- Replay
Metadata Reservation - Tracks the metadata reservation state during journal replay before
init_metadata_reservationinitializes the liveReservation.