Skip to content

bcachefs: implement online filesystem shrinking#1073

Draft
jullanggit wants to merge 75 commits into
koverstreet:masterfrom
jullanggit:shrink
Draft

bcachefs: implement online filesystem shrinking#1073
jullanggit wants to merge 75 commits into
koverstreet:masterfrom
jullanggit:shrink

Conversation

@jullanggit

@jullanggit jullanggit commented Mar 2, 2026

Copy link
Copy Markdown

Implement online filesystem shrinking through reconcile. Closes #781 once done.
This is hopefully complementary to #1070, which targets offline shrink.

Goal

A robust online shrinking implementation, that automatically resumes after restarts/crashes, as shrinking is a potentially long-running operation, and supports changing the target size mid-shrink.

Current state

  • Data movement, including stripes seems to work
  • Not all metadata (especially surrounding bucket gens) seems to be correctly handled yet
  • Journals are not explicitly handled, but may need to be
  • Resuming and changing target size is not yet implemented
  • Uses a whole-device reconcile scan, I plan to make it only scan the affected area

Implementation

Reuses large parts of the device remove/evacuate paths

Documentation

Not yet written

Testing

See https://github.com/jullanggit/ktest/tree/shrink for the tests used

@koverstreet

Copy link
Copy Markdown
Owner

Nice work — the reconcile-based approach for online shrinking is the right direction. Some feedback:

On-disk format:

Adding target_nbuckets to struct bch_member is a reasonable approach for tracking in-progress shrinks across restarts. But appending to bch_member needs care — make sure you're checking sizeof for the member struct version in the superblock validation path so older kernels reading a newer superblock don't read garbage. (This might already be handled by the existing versioned member struct machinery, but worth verifying.)

The evacuation loop:

The tail_is_empty() / sleep / retry loop in bch2_dev_shrink() is the right structure, but there are some issues:

  • The TODO about cached data is a real problem. After reconcile evacuates, the old extents get marked cached, so tail_is_empty() (which checks backpointers) might never see them clear. You'll need to handle this — either by making evacuate fully remove the old extent rather than caching it, or by adding a pass that invalidates cached buckets in the shrink region.

  • schedule_timeout_killable(HZ/2) is a reasonable poll interval but you should probably also check c->reconcile progress or wait on an event rather than blind polling.

  • The signal handling (signal_pending → -EINTR) is good — long-running operations should be interruptible.

Allocation cutoff:

if (unlikely(ca->mi.target_nbuckets && bucket >= ca->mi.target_nbuckets)) {

This replaces bch2_bucket_nouse() — but buckets_nouse was also used for other things (marking individual buckets bad). You've removed ENOMEM_buckets_nouse and no_resize_with_buckets_nouse from the error codes. Make sure nothing else was relying on that bitmap.

bch2_ptr_bad_or_evacuating_rcu:

This inline function does a division (div_u64(ptr->offset, ca->mi.bucket_size)) in what can be a hot path. Consider whether you can precompute the cutoff sector instead.

Commented-out code:

The __bch2_dev_resize_alloc block is commented out with a TODO. Either figure out what's needed there or remove it — commented-out code shouldn't ship.

Style nits:

  • Opening braces on function definitions should be on their own line (kernel style)
  • // TODO comments are fine for WIP but should be resolved before merge
  • The _typos.toml change doesn't belong in this PR
  • Several unrelated typo fixes (sentinal→sentinel, dosen't→doesn't, minumum→minimum, elligible→eligible) — those are welcome but should be a separate commit

Testing:

Good that you have ktest tests for this: https://github.com/jullanggit/ktest/tree/shrink. Consider adding cases for:

  • Shrink interrupted by signal, then resumed
  • Shrink with concurrent heavy writes to the device being shrunk
  • Shrink that needs to move striped/EC data
  • Shrink below journal location

Overall this is solid WIP. The hard parts (cached data handling, journal, resume after crash) are acknowledged as TODOs, which is the right approach — get the happy path working first.

— ProofOfConcept

@koverstreet koverstreet force-pushed the master branch 2 times, most recently from 6303f5b to 990d039 Compare March 14, 2026 03:32
@jullanggit

Copy link
Copy Markdown
Author

Thank you for the review! I'll continue working on this, and will ping you once I feel like another review would help.

this addition is backwards compatible because new fields are initialized
to zero, which means no pending resize, and are not read by older
kernels
also comment in outline of shrink path
…elying on ca->mi.target_nbuckets

avoids possible edge cases if device is being removed mid-shrink etc.
This is done analogous to the remove alloc info path
Shrink still used a wall-clock no-progress deadline after moving to tail
head plus aggregate backpointer tracking. That fixed the minute-scale
outlier, but it kept the final ENOSPC heuristic tied to host speed and
load.

Keep the shrink-local tail snapshots, but replace the wall-clock deadline
with counted reconcile work. Record how much work a completed reconcile
kick actually scanned or processed, rescan the shrinking device whenever
a completed kick found nothing to do, and only count completed
no-progress kicks that did real reconcile work toward ENOSPC.

Shrink still wakes once per second so it can rescan the tail instead of
sitting behind one long reconcile kick, but the impossible-shrink
heuristic itself is now based on completed no-progress work, not time.
A shrink tail can stay flat for many reconcile kicks while foreground IO or
reconcile itself is still changing metadata. Counting those no-progress kicks
as ENOSPC evidence can fail a shrink that would have completed on the next
wave of writes.

Tighten the heuristic so it only counts no-progress passes after a full
device rescan and only when the journal stayed quiet across that pass. If the
journal moved, force another device rescan from the current cutoff instead of
claiming the tail is impossible to evacuate.

That keeps ENOSPC on the stall path, but only after the blocker set has stopped
moving for repeated full rescans.
@Komzpa

Komzpa commented Jun 28, 2026

Copy link
Copy Markdown

Small helper PR for this branch:

It is intentionally narrow: kernel brace style for the new pointer helper, avoids the hot-path divide in bch2_ptr_bad_or_evacuating_rcu(), replaces the stale tail-snapshot TODO with the current journal/superblock handling note, and removes the commented-out __bch2_dev_resize_alloc() block.

Related tools-side bridge:

That tools PR is clean/green at head 81ee809c6d8922a04c0e5d2cebbb19f7950ad47e and only lets mounted bcachefs device resize delegate smaller target sizes to kernels that implement shrink; it does not claim full shrink support by itself.

This does not try to solve the larger cached-data, progress/wakeup, journal/resume, or ktest follow-ups from Kent’s review.

@Komzpa

Komzpa commented Jun 29, 2026

Copy link
Copy Markdown

I took another pass on this because it is blocking the high-score shrink cluster (koverstreet/bcachefs#781 / koverstreet/bcachefs-tools#268).

Rebased helper branch:

What changed beyond the existing shrink stack:

  • rebased the branch over current master;
  • kept the existing author stack intact;
  • added one small compatibility commit for the rebase: the shrink discard guard now uses the current resize target state instead of the deleted buckets_nouse bitmap, so both the normal and fast discard paths still skip the shrinking tail.

Validation I ran:

  • git diff --check origin/master..HEAD
  • no conflict markers or stale bch2_bucket_nouse / dev_bucket_nouse references under fs/bcachefs
  • targeted build:
    make -j32 fs/bcachefs/alloc/buckets.o fs/bcachefs/alloc/discard.o fs/bcachefs/alloc/foreground.o fs/bcachefs/data/reconcile/work.o fs/bcachefs/data/ec/init.o fs/bcachefs/init/dev.o fs/bcachefs/init/fs.o fs/bcachefs/btree/interior.o fs/bcachefs/journal/write.o

I did not force-push jullanggit:shrink; this is just a review/rebase branch people can pull from.

This is already internally called by bch2_dev_remove_alloc
…nto caller

bch2_dev_remove_alloc() now does the same operations wheter it is passed
a cutoff or not. Removing usage is not directly handled by both the
shrink (as previously) and remove (new) caller.
use helper macros and add comments
…r_extent_reconcile_phys_update and update comments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for shrinking filesystem

3 participants