]> git.ipfire.org Git - thirdparty/kernel/stable.git/commitdiff
jbd2: Avoid long replay times due to high number or revoke blocks
authorJan Kara <jack@suse.cz>
Tue, 21 Jan 2025 14:09:26 +0000 (15:09 +0100)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Thu, 29 May 2025 09:13:24 +0000 (11:13 +0200)
[ Upstream commit a399af4e3b1ab2c5d83292d4487c4d18de551659 ]

Some users are reporting journal replay takes a long time when there is
excessive number of revoke blocks in the journal. Reported times are
like:

1048576 records - 95 seconds
2097152 records - 580 seconds

The problem is that hash chains in the revoke table gets excessively
long in these cases. Fix the problem by sizing the revoke table
appropriately before the revoke pass.

Thanks to Alexey Zhuravlev <azhuravlev@ddn.com> for benchmarking the
patch with large numbers of revoke blocks [1].

[1] https://lore.kernel.org/all/20250113183107.7bfef7b6@x390.bzzz77.ru

Signed-off-by: Jan Kara <jack@suse.cz>
Reviewed-by: Andreas Dilger <adilger@dilger.ca>
Reviewed-by: Zhang Yi <yi.zhang@huawei.com>
Link: https://patch.msgid.link/20250121140925.17231-2-jack@suse.cz
Signed-off-by: Theodore Ts'o <tytso@mit.edu>
Signed-off-by: Sasha Levin <sashal@kernel.org>
fs/jbd2/recovery.c
fs/jbd2/revoke.c
include/linux/jbd2.h

index 23502f1a67c1ebe0cda5f9acb3dddfdc97965965..a3c39a71c4ad3026760a93c29e18ea48fd773f2e 100644 (file)
@@ -39,7 +39,7 @@ struct recovery_info
 
 static int do_one_pass(journal_t *journal,
                                struct recovery_info *info, enum passtype pass);
-static int scan_revoke_records(journal_t *, struct buffer_head *,
+static int scan_revoke_records(journal_t *, enum passtype, struct buffer_head *,
                                tid_t, struct recovery_info *);
 
 #ifdef __KERNEL__
@@ -328,6 +328,12 @@ int jbd2_journal_recover(journal_t *journal)
                  journal->j_transaction_sequence, journal->j_head);
 
        jbd2_journal_clear_revoke(journal);
+       /* Free revoke table allocated for replay */
+       if (journal->j_revoke != journal->j_revoke_table[0] &&
+           journal->j_revoke != journal->j_revoke_table[1]) {
+               jbd2_journal_destroy_revoke_table(journal->j_revoke);
+               journal->j_revoke = journal->j_revoke_table[1];
+       }
        err2 = sync_blockdev(journal->j_fs_dev);
        if (!err)
                err = err2;
@@ -613,6 +619,31 @@ static int do_one_pass(journal_t *journal,
        first_commit_ID = next_commit_ID;
        if (pass == PASS_SCAN)
                info->start_transaction = first_commit_ID;
+       else if (pass == PASS_REVOKE) {
+               /*
+                * Would the default revoke table have too long hash chains
+                * during replay?
+                */
+               if (info->nr_revokes > JOURNAL_REVOKE_DEFAULT_HASH * 16) {
+                       unsigned int hash_size;
+
+                       /*
+                        * Aim for average chain length of 8, limit at 1M
+                        * entries to avoid problems with malicious
+                        * filesystems.
+                        */
+                       hash_size = min(roundup_pow_of_two(info->nr_revokes / 8),
+                                       1U << 20);
+                       journal->j_revoke =
+                               jbd2_journal_init_revoke_table(hash_size);
+                       if (!journal->j_revoke) {
+                               printk(KERN_ERR
+                                      "JBD2: failed to allocate revoke table for replay with %u entries. "
+                                      "Journal replay may be slow.\n", hash_size);
+                               journal->j_revoke = journal->j_revoke_table[1];
+                       }
+               }
+       }
 
        jbd2_debug(1, "Starting recovery pass %d\n", pass);
 
@@ -852,6 +883,13 @@ chksum_ok:
                        continue;
 
                case JBD2_REVOKE_BLOCK:
+                       /*
+                        * If we aren't in the SCAN or REVOKE pass, then we can
+                        * just skip over this block.
+                        */
+                       if (pass != PASS_REVOKE && pass != PASS_SCAN)
+                               continue;
+
                        /*
                         * Check revoke block crc in pass_scan, if csum verify
                         * failed, check commit block time later.
@@ -864,12 +902,7 @@ chksum_ok:
                                need_check_commit_time = true;
                        }
 
-                       /* If we aren't in the REVOKE pass, then we can
-                        * just skip over this block. */
-                       if (pass != PASS_REVOKE)
-                               continue;
-
-                       err = scan_revoke_records(journal, bh,
+                       err = scan_revoke_records(journal, pass, bh,
                                                  next_commit_ID, info);
                        if (err)
                                goto failed;
@@ -923,8 +956,9 @@ chksum_ok:
 
 /* Scan a revoke record, marking all blocks mentioned as revoked. */
 
-static int scan_revoke_records(journal_t *journal, struct buffer_head *bh,
-                              tid_t sequence, struct recovery_info *info)
+static int scan_revoke_records(journal_t *journal, enum passtype pass,
+                              struct buffer_head *bh, tid_t sequence,
+                              struct recovery_info *info)
 {
        jbd2_journal_revoke_header_t *header;
        int offset, max;
@@ -945,6 +979,11 @@ static int scan_revoke_records(journal_t *journal, struct buffer_head *bh,
        if (jbd2_has_feature_64bit(journal))
                record_len = 8;
 
+       if (pass == PASS_SCAN) {
+               info->nr_revokes += (max - offset) / record_len;
+               return 0;
+       }
+
        while (offset + record_len <= max) {
                unsigned long long blocknr;
                int err;
@@ -957,7 +996,6 @@ static int scan_revoke_records(journal_t *journal, struct buffer_head *bh,
                err = jbd2_journal_set_revoke(journal, blocknr, sequence);
                if (err)
                        return err;
-               ++info->nr_revokes;
        }
        return 0;
 }
index f68fc8c255f0079a8705bae9a728d2ae35245a2a..bc328c39028a2136e61e7057d9023401d5b95c7b 100644 (file)
@@ -215,7 +215,7 @@ int __init jbd2_journal_init_revoke_table_cache(void)
        return 0;
 }
 
-static struct jbd2_revoke_table_s *jbd2_journal_init_revoke_table(int hash_size)
+struct jbd2_revoke_table_s *jbd2_journal_init_revoke_table(int hash_size)
 {
        int shift = 0;
        int tmp = hash_size;
@@ -231,7 +231,7 @@ static struct jbd2_revoke_table_s *jbd2_journal_init_revoke_table(int hash_size)
        table->hash_size = hash_size;
        table->hash_shift = shift;
        table->hash_table =
-               kmalloc_array(hash_size, sizeof(struct list_head), GFP_KERNEL);
+               kvmalloc_array(hash_size, sizeof(struct list_head), GFP_KERNEL);
        if (!table->hash_table) {
                kmem_cache_free(jbd2_revoke_table_cache, table);
                table = NULL;
@@ -245,7 +245,7 @@ out:
        return table;
 }
 
-static void jbd2_journal_destroy_revoke_table(struct jbd2_revoke_table_s *table)
+void jbd2_journal_destroy_revoke_table(struct jbd2_revoke_table_s *table)
 {
        int i;
        struct list_head *hash_list;
@@ -255,7 +255,7 @@ static void jbd2_journal_destroy_revoke_table(struct jbd2_revoke_table_s *table)
                J_ASSERT(list_empty(hash_list));
        }
 
-       kfree(table->hash_table);
+       kvfree(table->hash_table);
        kmem_cache_free(jbd2_revoke_table_cache, table);
 }
 
index 561025b4f3d91da4c07918d79e0ea1a8d1899f7a..469c4a191ced43b67bd3f7849691aa68a3d46974 100644 (file)
@@ -1627,6 +1627,8 @@ extern void          jbd2_journal_destroy_revoke_record_cache(void);
 extern void       jbd2_journal_destroy_revoke_table_cache(void);
 extern int __init jbd2_journal_init_revoke_record_cache(void);
 extern int __init jbd2_journal_init_revoke_table_cache(void);
+struct jbd2_revoke_table_s *jbd2_journal_init_revoke_table(int hash_size);
+void jbd2_journal_destroy_revoke_table(struct jbd2_revoke_table_s *table);
 
 extern void       jbd2_journal_destroy_revoke(journal_t *);
 extern int        jbd2_journal_revoke (handle_t *, unsigned long long, struct buffer_head *);