From: Jan Kara <jack@suse.cz>
To: <linux-fsdevel@vger.kernel.org>
Cc: <linux-mm@kvack.org>, Christian Brauner <brauner@kernel.org>,
Christoph Hellwig <hch@infradead.org>,
Mikhail Rudenko <xyzzy@yandex-team.ru>, Jan Kara <jack@suse.cz>
Subject: [PATCH v2 4/5] fs: Add throttling to deferred inode reclaim
Date: Fri, 11 Sep 2026 10:51:40 +0200 [thread overview]
Message-ID: <20260911085142.1774803-9-jack@suse.cz> (raw)
In-Reply-To: <20260911081309.14137-1-jack@suse.cz>
Deferring difficult inode reclaim from prune_icache_sb() to a workqueue
removes the natural feedback loop of blocking tasks in direct reclaim
until they make space for new allocations. This can result in the list
of deferred inodes to grow beyond any bounds and possibly push the
machine to a reclaim storm or OOM.
Add a throttling mechanism slowing down tasks in
mark_inode_reclaim_deferred() if the number of deferred inodes to
reclaim for a superblock grows over limit. We measure average time it
takes to reclaim inode on deferred list for a superblock and block tasks
proportionally to that.
Signed-off-by: Jan Kara <jack@suse.cz>
---
fs/inode.c | 102 +++++++++++++++++++++++++++----
include/linux/fs/super_types.h | 2 +
include/trace/events/writeback.h | 51 ++++++++++++++++
3 files changed, 144 insertions(+), 11 deletions(-)
diff --git a/fs/inode.c b/fs/inode.c
index e902095cf39b..0491300c5ccb 100644
--- a/fs/inode.c
+++ b/fs/inode.c
@@ -1113,23 +1113,53 @@ static int get_inode_reclaim_batch(int *id, struct list_head *list)
return count;
}
+static void inode_reclaim_update_stat(struct super_block *sb, unsigned int n,
+ u64 start)
+{
+ u64 delay;
+
+ if (!sb)
+ return;
+
+ delay = div_u64(ktime_get_ns() - start, n);
+ /*
+ * Smooth delay updates with exponential moving average. Updates can
+ * get lost if workers race but we don't really care.
+ */
+ WRITE_ONCE(sb->s_deferred_reclaim_delay,
+ (63 * READ_ONCE(sb->s_deferred_reclaim_delay) + delay) / 64);
+
+ trace_inode_reclaim_update_stat(sb, n, delay,
+ READ_ONCE(sb->s_deferred_reclaim_delay));
+
+ /*
+ * The elevated s_deferred_reclaim_count keeps sb alive until we drop
+ * it
+ */
+ if (!atomic_sub_return(n, &sb->s_deferred_reclaim_count))
+ wake_up_var(&sb->s_deferred_reclaim_count);
+}
static void inode_reclaim_deferred(struct work_struct *work)
{
struct inode *inode;
LIST_HEAD(inode_batch);
+ u64 start;
+ int id, count;
+ struct super_block *sb;
+
/*
* We start with the list corresponding to the work but rolling a dice
* would work as well
*/
- int id = work - deferred_reclaim.work;
+ id = work - deferred_reclaim.work;
while (1) {
if (!get_inode_reclaim_batch(&id, &inode_batch))
break;
- while (!list_empty(&inode_batch)) {
- struct super_block *sb;
+ sb = NULL;
+ while (!list_empty(&inode_batch)) {
/*
* inode_batch list is private and I_FREEING flags
* protect us from anybody else trying to remove the
@@ -1137,17 +1167,20 @@ static void inode_reclaim_deferred(struct work_struct *work)
*/
inode = list_first_entry(&inode_batch, struct inode,
i_lru);
+ if (inode->i_sb != sb) {
+ inode_reclaim_update_stat(sb, count, start);
+
+ sb = inode->i_sb;
+ count = 0;
+ start = ktime_get_ns();
+ }
+ count++;
list_del_init(&inode->i_lru);
- sb = inode->i_sb;
evict(inode);
- /*
- * The elevated s_deferred_reclaim_count keeps sb alive
- * until we drop it
- */
- if (atomic_dec_and_test(&sb->s_deferred_reclaim_count))
- wake_up_var(&sb->s_deferred_reclaim_count);
cond_resched();
}
+
+ inode_reclaim_update_stat(sb, count, start);
}
}
@@ -1183,14 +1216,61 @@ static void __init deferred_reclaim_init(void)
}
}
+/*
+ * Size of deferred reclaim list from which we start throttling tasks creating
+ * inodes marked for deferred reclaim.
+ */
+#define INODE_DEFERRED_RECLAIM_LIMIT 8192
+
+static void throttle_inode_deferred_reclaim(struct inode *inode)
+{
+ struct super_block *sb = inode->i_sb;
+ unsigned int len;
+
+ /*
+ * If inodes with deferred reclaim are accumulating too much, slow down
+ * tasks creating them. This doesn't provide any kind of guarantee on
+ * the length of the deferred list since lots of inodes with
+ * I_DEFER_RECLAIM can be already present in the inode cache and we
+ * have no control when they reach the deferred list. But if the
+ * pressure on the deferred list is sustained, the balance should
+ * eventually be established.
+ */
+ len = atomic_read(&sb->s_deferred_reclaim_count);
+ if (len > INODE_DEFERRED_RECLAIM_LIMIT) {
+ u64 delay = READ_ONCE(sb->s_deferred_reclaim_delay);
+
+ if (!delay)
+ return;
+ /*
+ * Scale the delay based on how much we exceed the limit. Wait
+ * at most 4x as long as estimated time to reclaim the inode.
+ */
+ len = min(len, 5 * INODE_DEFERRED_RECLAIM_LIMIT);
+ delay = div_u64(delay * (len - INODE_DEFERRED_RECLAIM_LIMIT),
+ INODE_DEFERRED_RECLAIM_LIMIT);
+ trace_mark_inode_reclaim_deferred_throttle(inode, len, delay);
+
+ schedule_timeout_killable(nsecs_to_jiffies(delay));
+ }
+}
+
void mark_inode_reclaim_deferred(struct inode *inode)
{
+ bool throttle = false;
+
if (inode_state_read_once(inode) & I_DEFER_RECLAIM)
return;
spin_lock(&inode->i_lock);
- inode_state_set(inode, I_DEFER_RECLAIM);
+ if (!(inode_state_read(inode) & I_DEFER_RECLAIM)) {
+ inode_state_set(inode, I_DEFER_RECLAIM);
+ throttle = true;
+ }
spin_unlock(&inode->i_lock);
+
+ if (throttle)
+ throttle_inode_deferred_reclaim(inode);
}
EXPORT_SYMBOL_GPL(mark_inode_reclaim_deferred);
diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h
index a41bd57fc2e6..6db5df6fd35f 100644
--- a/include/linux/fs/super_types.h
+++ b/include/linux/fs/super_types.h
@@ -234,6 +234,8 @@ struct super_block {
/* Number of inodes queued for deferred reclaim */
atomic_t s_deferred_reclaim_count;
+ /* Average time to reclaim one deferred inode */
+ u64 s_deferred_reclaim_delay;
/* Number of inodes with nlink == 0 but still referenced */
atomic_long_t s_remove_count;
diff --git a/include/trace/events/writeback.h b/include/trace/events/writeback.h
index 4a9cfe458820..aec38eaeb10e 100644
--- a/include/trace/events/writeback.h
+++ b/include/trace/events/writeback.h
@@ -881,6 +881,57 @@ DEFINE_EVENT(writeback_inode_template, sb_clear_inode_writeback,
TP_ARGS(inode)
);
+TRACE_EVENT(inode_reclaim_update_stat,
+ TP_PROTO(
+ struct super_block *sb,
+ unsigned int n,
+ u64 batch_delay,
+ u64 avg_delay
+ ),
+ TP_ARGS(sb, n, batch_delay, avg_delay),
+
+ TP_STRUCT__entry(
+ __field(dev_t, dev)
+ __field(unsigned int, n)
+ __field(u64, batch_delay)
+ __field(u64, avg_delay)
+ ),
+
+ TP_fast_assign(
+ __entry->dev = sb->s_dev;
+ __entry->n = n;
+ __entry->batch_delay = batch_delay;
+ __entry->avg_delay = avg_delay;
+ ),
+
+ TP_printk("dev %d,%d batch size %u batch delay %llu ns avg delay %llu ns",
+ MAJOR(__entry->dev), MINOR(__entry->dev), __entry->n,
+ __entry->batch_delay, __entry->avg_delay)
+);
+
+TRACE_EVENT(mark_inode_reclaim_deferred_throttle,
+ TP_PROTO(struct inode *inode, unsigned int len, u64 delay),
+ TP_ARGS(inode, len, delay),
+
+ TP_STRUCT__entry(
+ __field(u64, ino)
+ __field(dev_t, dev)
+ __field(unsigned int, len)
+ __field(u64, delay)
+ ),
+
+ TP_fast_assign(
+ __entry->ino = inode->i_ino;
+ __entry->dev = inode->i_sb->s_dev;
+ __entry->len = len;
+ __entry->delay = delay;
+ ),
+
+ TP_printk("dev %d,%d ino %llu deferred list len %u delay %llu ns",
+ MAJOR(__entry->dev), MINOR(__entry->dev),
+ __entry->ino, __entry->len, __entry->delay)
+);
+
#endif /* _TRACE_WRITEBACK_H */
/* This part must be outside protection */
--
2.51.0
next prev parent reply other threads:[~2026-09-11 8:52 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-11 8:51 [PATCH 0/5 v2] fs: Deferred inode reclaim Jan Kara
2026-09-11 8:51 ` [PATCH v2 1/5] writeback: Fix missed lazytime flush Jan Kara
2026-09-11 8:51 ` [PATCH v2 2/5] fs: Avoid inode dirtying on last iput Jan Kara
2026-09-11 8:51 ` [PATCH v2 3/5] fs: Basic infrastructure for offloading inode reclaim Jan Kara
2026-09-11 8:51 ` Jan Kara [this message]
2026-09-11 8:51 ` [PATCH v2 5/5] ext4: Defer inode reclaim if it has preallocations Jan Kara
2026-09-11 14:55 ` [PATCH 0/5 v2] fs: Deferred inode reclaim Theodore Tso
2026-09-11 15:05 ` Darrick J. Wong
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260911085142.1774803-9-jack@suse.cz \
--to=jack@suse.cz \
--cc=brauner@kernel.org \
--cc=hch@infradead.org \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=xyzzy@yandex-team.ru \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox