]> git.ipfire.org Git - thirdparty/kernel/stable.git/commitdiff
writeback: Avoid excessively long inode switching times
authorJan Kara <jack@suse.cz>
Fri, 12 Sep 2025 10:38:37 +0000 (12:38 +0200)
committerChristian Brauner <brauner@kernel.org>
Fri, 19 Sep 2025 11:11:06 +0000 (13:11 +0200)
With lazytime mount option enabled we can be switching many dirty inodes
on cgroup exit to the parent cgroup. The numbers observed in practice
when systemd slice of a large cron job exits can easily reach hundreds
of thousands or millions. The logic in inode_do_switch_wbs() which sorts
the inode into appropriate place in b_dirty list of the target wb
however has linear complexity in the number of dirty inodes thus overall
time complexity of switching all the inodes is quadratic leading to
workers being pegged for hours consuming 100% of the CPU and switching
inodes to the parent wb.

Simple reproducer of the issue:
  FILES=10000
  # Filesystem mounted with lazytime mount option
  MNT=/mnt/
  echo "Creating files and switching timestamps"
  for (( j = 0; j < 50; j ++ )); do
      mkdir $MNT/dir$j
      for (( i = 0; i < $FILES; i++ )); do
          echo "foo" >$MNT/dir$j/file$i
      done
      touch -a -t 202501010000 $MNT/dir$j/file*
  done
  wait
  echo "Syncing and flushing"
  sync
  echo 3 >/proc/sys/vm/drop_caches

  echo "Reading all files from a cgroup"
  mkdir /sys/fs/cgroup/unified/mycg1 || exit
  echo $$ >/sys/fs/cgroup/unified/mycg1/cgroup.procs || exit
  for (( j = 0; j < 50; j ++ )); do
      cat /mnt/dir$j/file* >/dev/null &
  done
  wait
  echo "Switching wbs"
  # Now rmdir the cgroup after the script exits

We need to maintain b_dirty list ordering to keep writeback happy so
instead of sorting inode into appropriate place just append it at the
end of the list and clobber dirtied_time_when. This may result in inode
writeback starting later after cgroup switch however cgroup switches are
rare so it shouldn't matter much. Since the cgroup had write access to
the inode, there are no practical concerns of the possible DoS issues.

Acked-by: Tejun Heo <tj@kernel.org>
Signed-off-by: Jan Kara <jack@suse.cz>
Signed-off-by: Christian Brauner <brauner@kernel.org>
fs/fs-writeback.c

index 36ef1a796d4b987b2d88eced6315ffc9013d27f0..af5f396449f16e3c72b4a15b15e8431357f51785 100644 (file)
@@ -445,22 +445,23 @@ static bool inode_do_switch_wbs(struct inode *inode,
         * Transfer to @new_wb's IO list if necessary.  If the @inode is dirty,
         * the specific list @inode was on is ignored and the @inode is put on
         * ->b_dirty which is always correct including from ->b_dirty_time.
-        * The transfer preserves @inode->dirtied_when ordering.  If the @inode
-        * was clean, it means it was on the b_attached list, so move it onto
-        * the b_attached list of @new_wb.
+        * If the @inode was clean, it means it was on the b_attached list, so
+        * move it onto the b_attached list of @new_wb.
         */
        if (!list_empty(&inode->i_io_list)) {
                inode->i_wb = new_wb;
 
                if (inode->i_state & I_DIRTY_ALL) {
-                       struct inode *pos;
-
-                       list_for_each_entry(pos, &new_wb->b_dirty, i_io_list)
-                               if (time_after_eq(inode->dirtied_when,
-                                                 pos->dirtied_when))
-                                       break;
+                       /*
+                        * We need to keep b_dirty list sorted by
+                        * dirtied_time_when. However properly sorting the
+                        * inode in the list gets too expensive when switching
+                        * many inodes. So just attach inode at the end of the
+                        * dirty list and clobber the dirtied_time_when.
+                        */
+                       inode->dirtied_time_when = jiffies;
                        inode_io_list_move_locked(inode, new_wb,
-                                                 pos->i_io_list.prev);
+                                                 &new_wb->b_dirty);
                } else {
                        inode_cgwb_move_to_attached(inode, new_wb);
                }