From: Denis V. Lunev <[email protected]>

The balloon holds file system space that never carries data, but its
blocks are allocated, so ext4_trim_all_free() never discards them, and
inflating the balloon is a plain fallocate() that does not discard
either. Once the balloon is inflated over blocks that held data, e.g.
on an online shrink after files were removed, the device below keeps
them allocated: a ploop or qcow2 image keeps host disk space the
container can never use, and compaction, which is FITRIM, does not give
it back.

Walk the balloon after the free space in ext4_trim_fs() and discard its
unwritten extents that fall into the requested range. The walk goes to
the end of the extent tree, as fallocate() with FALLOC_FL_KEEP_SIZE can
leave extents past i_size. Physically adjacent extents are merged into
one discard, otherwise every cut at the unwritten extent length limit
leaves a partly covered cluster of the image below. A merged run is
discarded in chunks, with a check for fatal signals and freezing
between them.

The walk holds i_rwsem and the invalidate lock exclusively, after
waiting for direct I/O and flushing the page cache of the balloon, as
punch hole does. No block of the balloon can be freed and reused under
the discard, and nothing is written to it meanwhile, so an extent that
is still unwritten reads as zeroes before and after the discard. The
balloon is hidden from lookup and opened only through
EXT4_IOC_OPEN_BALLOON, which takes CAP_SYS_ADMIN, so the lock only
waits for ploop inflating or shrinking it.

An empty balloon is skipped before taking the locks. Any other one is
walked whole on every FITRIM, whatever range it asks for, as its
extent tree is indexed by file offset; FITRIM is rare enough for that.
Discarding the balloon again each time is cheap: dm-ploop completes
the discard of an unmapped cluster at once.

The balloon is not counted in the trimmed length reported to
userspace, and a discard error fails FITRIM.

A balloon inflated before this change is released by the next FITRIM.

https://virtuozzo.atlassian.net/browse/VSTOR-153590

Feature: fs/ext4: fast online shrink support
Signed-off-by: Denis V. Lunev <[email protected]>
---

Notes:
    The balloon is locked, flushed and walked whole on every FITRIM,
    whatever the range. This is deliberate: ploop runs FITRIM on
    compaction, about once a day. Measured on a 4-CPU VM with a balloon of
    84-99G over all the free space of a 100G image: a FITRIM after the
    first one spends 0.3-0.7s in the balloon, on ploop and qcow2, ext4 and
    xfs, for a contiguous balloon and for one in up to about 10000 pieces.
    The time goes mostly into discarding clusters that are already unmapped
    and grows with the size of the balloon and the number of its pieces.
    The locks held meanwhile only keep ploop from resizing the balloon.
    Narrowing the walk to the range would need a reverse map from blocks to
    file offsets. An empty balloon, the common case, is skipped before the
    locks are taken.

 fs/ext4/mballoc.c | 126 ++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 126 insertions(+)

diff --git a/fs/ext4/mballoc.c b/fs/ext4/mballoc.c
index aaf2c24c514c..4906f49353bf 100644
--- a/fs/ext4/mballoc.c
+++ b/fs/ext4/mballoc.c
@@ -7009,6 +7009,126 @@ ext4_trim_all_free(struct super_block *sb, ext4_group_t 
group,
        return ret;
 }
 
+#define EXT4_BALLOON_DISCARD_CHUNK     (128U << 20)
+
+static int ext4_trim_balloon_run(struct super_block *sb, ext4_fsblk_t pblk,
+                                ext4_fsblk_t len, ext4_fsblk_t start,
+                                ext4_fsblk_t end, ext4_fsblk_t minblocks)
+{
+       unsigned int gran = max_t(unsigned int, 1,
+                       bdev_discard_granularity(sb->s_bdev) >> 
sb->s_blocksize_bits);
+       unsigned int chunk = roundup(EXT4_BALLOON_DISCARD_CHUNK >>
+                                    sb->s_blocksize_bits, gran);
+       ext4_fsblk_t last = pblk + len - 1, next;
+       int ret;
+
+       if (!len)
+               return 0;
+       if (pblk < start)
+               pblk = start;
+       if (last > end)
+               last = end;
+       if (last < pblk || last - pblk + 1 < minblocks)
+               return 0;
+
+       /*
+        * Chunks keep a long run interruptible. They end on multiples of the
+        * discard granularity, so no cluster below is split between two.
+        */
+       while (pblk <= last) {
+               u64 rem = pblk;
+
+               next = pblk + chunk - do_div(rem, chunk);
+               if (next > last + 1)
+                       next = last + 1;
+               trace_ext4_discard_blocks(sb, pblk, next - pblk);
+               ret = sb_issue_discard(sb, pblk, next - pblk, GFP_NOFS, 0);
+               if (ret)
+                       return ret;
+               pblk = next;
+               if (ext4_trim_interrupted())
+                       break;
+               cond_resched();
+       }
+
+       return 0;
+}
+
+/*
+ * Blocks of the balloon never hold data, but they are allocated, so
+ * ext4_trim_all_free() never discards them. Discard its unwritten
+ * extents, merging physically adjacent ones, so that the device below
+ * can release the space as it does for free blocks.
+ */
+static int ext4_trim_balloon(struct super_block *sb, ext4_fsblk_t start,
+                            ext4_fsblk_t end, ext4_fsblk_t minblocks)
+{
+       struct ext4_sb_info *sbi = EXT4_SB(sb);
+       ext4_fsblk_t run_pblk = 0, run_len = 0;
+       struct ext4_map_blocks map;
+       struct inode *inode = NULL;
+       ext4_lblk_t lblk;
+       bool unwritten;
+       int ret, err;
+
+       /* ext4_load_balloon() replaces s_balloon_ino under s_umount */
+       down_read(&sb->s_umount);
+       if (sbi->s_balloon_ino && sbi->s_balloon_ino->i_blocks)
+               inode = igrab(sbi->s_balloon_ino);
+       up_read(&sb->s_umount);
+       if (!inode)
+               return 0;
+
+       /*
+        * Nobody may write the balloon meanwhile: data written into an
+        * unwritten extent would be discarded with it.
+        */
+       inode_lock(inode);
+       inode_dio_wait(inode);
+       filemap_invalidate_lock(inode->i_mapping);
+       ret = filemap_write_and_wait(inode->i_mapping);
+       if (ret)
+               goto out;
+
+       for (lblk = 0; lblk < EXT_MAX_BLOCKS; lblk += map.m_len) {
+               if (ext4_trim_interrupted())
+                       break;
+               cond_resched();
+
+               map.m_lblk = lblk;
+               map.m_len = EXT_MAX_BLOCKS - lblk;
+               err = ext4_map_blocks(NULL, inode, &map, 0);
+               if (err < 0) {
+                       ret = err;
+                       break;
+               }
+               if (!map.m_len)
+                       break;
+
+               unwritten = err > 0 && (map.m_flags & EXT4_MAP_UNWRITTEN);
+               if (unwritten && run_len && run_pblk + run_len == map.m_pblk) {
+                       run_len += map.m_len;
+                       continue;
+               }
+
+               ret = ext4_trim_balloon_run(sb, run_pblk, run_len, start, end,
+                                           minblocks);
+               if (ret)
+                       break;
+               run_pblk = map.m_pblk;
+               run_len = unwritten ? map.m_len : 0;
+       }
+       if (!ret && !ext4_trim_interrupted())
+               ret = ext4_trim_balloon_run(sb, run_pblk, run_len, start, end,
+                                           minblocks);
+out:
+       filemap_invalidate_unlock(inode->i_mapping);
+       inode_unlock(inode);
+       iput(inode);
+
+       return ret;
+}
+
 /**
  * ext4_trim_fs() -- trim ioctl handle function
  * @sb:                        superblock for filesystem
@@ -7028,6 +7148,7 @@ int ext4_trim_fs(struct super_block *sb, struct 
fstrim_range *range)
        ext4_group_t group, first_group, last_group;
        ext4_grpblk_t cnt = 0, first_cluster, last_cluster;
        uint64_t start, end, minlen, trimmed = 0;
+       ext4_fsblk_t last_blk;
        ext4_fsblk_t first_data_blk =
                        le32_to_cpu(EXT4_SB(sb)->s_es->s_first_data_block);
        ext4_fsblk_t max_blks = ext4_blocks_count(EXT4_SB(sb)->s_es);
@@ -7061,6 +7182,7 @@ int ext4_trim_fs(struct super_block *sb, struct 
fstrim_range *range)
                                     &first_group, &first_cluster);
        ext4_get_group_no_and_offset(sb, (ext4_fsblk_t) end,
                                     &last_group, &last_cluster);
+       last_blk = end;
 
        /* end now represents the last cluster to discard in this group */
        end = EXT4_CLUSTERS_PER_GROUP(sb) - 1;
@@ -7103,6 +7225,10 @@ int ext4_trim_fs(struct super_block *sb, struct 
fstrim_range *range)
                first_cluster = 0;
        }
 
+       if (!ret && !ext4_trim_interrupted())
+               ret = ext4_trim_balloon(sb, start, last_blk,
+                                       EXT4_C2B(EXT4_SB(sb), minlen));
+
        if (!ret)
                EXT4_SB(sb)->s_last_trim_minblks = minlen;
 
-- 
2.53.0

_______________________________________________
Devel mailing list
[email protected]
https://lists.openvz.org/mailman/listinfo/devel

Reply via email to