ddt: add support for prefetching tables into the ARC

This change adds a new `zpool prefetch -t ddt $pool` command which
causes a pool's DDT to be loaded into the ARC. The primary goal is to
remove the need to "warm" a pool's cache before deduplication stops
slowing write performance. It may also provide a way to reload portions
of a DDT if they have been flushed due to inactivity.

Sponsored-by: iXsystems, Inc.
Sponsored-by: Catalogics, Inc.
Sponsored-by: Klara, Inc.
Reviewed-by: Alexander Motin <mav@FreeBSD.org>
Reviewed-by: Tony Hutter <hutter2@llnl.gov>
Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Signed-off-by: Allan Jude <allan@klarasystems.com>
Signed-off-by: Will Andrews <will.andrews@klarasystems.com>
Signed-off-by: Fred Weigel <fred.weigel@klarasystems.com>
Signed-off-by: Rob Norris <rob.norris@klarasystems.com>
Signed-off-by: Don Brady <don.brady@klarasystems.com>
Co-authored-by: Will Andrews <will.andrews@klarasystems.com>
Co-authored-by: Don Brady <don.brady@klarasystems.com>
Closes #15890
This commit is contained in:
Allan Jude
2024-07-26 12:16:18 -04:00
committed by GitHub
parent 2ed1aebaf6
commit 62e7d3c89e
37 changed files with 1067 additions and 52 deletions
+210 -2
View File
@@ -26,7 +26,7 @@
* Copyright (c) 2016, Nexenta Systems, Inc. All rights reserved.
* Copyright (c) 2015 by Chunwei Chen. All rights reserved.
* Copyright (c) 2019 Datto Inc.
* Copyright (c) 2019, Klara Inc.
* Copyright (c) 2019, 2023, Klara Inc.
* Copyright (c) 2019, Allan Jude
* Copyright (c) 2022 Hewlett Packard Enterprise Development LP.
* Copyright (c) 2021, 2022 by Pawel Jakub Dawidek
@@ -701,7 +701,7 @@ dmu_buf_rele_array(dmu_buf_t **dbp_fake, int numbufs, const void *tag)
* Issue prefetch I/Os for the given blocks. If level is greater than 0, the
* indirect blocks prefetched will be those that point to the blocks containing
* the data starting at offset, and continuing to offset + len. If the range
* it too long, prefetch the first dmu_prefetch_max bytes as requested, while
* is too long, prefetch the first dmu_prefetch_max bytes as requested, while
* for the rest only a higher level, also fitting within dmu_prefetch_max. It
* should primarily help random reads, since for long sequential reads there is
* a speculative prefetcher.
@@ -777,6 +777,106 @@ dmu_prefetch_by_dnode(dnode_t *dn, int64_t level, uint64_t offset,
rw_exit(&dn->dn_struct_rwlock);
}
typedef struct {
kmutex_t dpa_lock;
kcondvar_t dpa_cv;
uint64_t dpa_pending_io;
} dmu_prefetch_arg_t;
static void
dmu_prefetch_done(void *arg, uint64_t level, uint64_t blkid, boolean_t issued)
{
(void) level; (void) blkid; (void)issued;
dmu_prefetch_arg_t *dpa = arg;
ASSERT0(level);
mutex_enter(&dpa->dpa_lock);
ASSERT3U(dpa->dpa_pending_io, >, 0);
if (--dpa->dpa_pending_io == 0)
cv_broadcast(&dpa->dpa_cv);
mutex_exit(&dpa->dpa_lock);
}
static void
dmu_prefetch_wait_by_dnode(dnode_t *dn, uint64_t offset, uint64_t len)
{
dmu_prefetch_arg_t dpa;
mutex_init(&dpa.dpa_lock, NULL, MUTEX_DEFAULT, NULL);
cv_init(&dpa.dpa_cv, NULL, CV_DEFAULT, NULL);
rw_enter(&dn->dn_struct_rwlock, RW_READER);
uint64_t start = dbuf_whichblock(dn, 0, offset);
uint64_t end = dbuf_whichblock(dn, 0, offset + len - 1) + 1;
dpa.dpa_pending_io = end - start;
for (uint64_t blk = start; blk < end; blk++) {
(void) dbuf_prefetch_impl(dn, 0, blk, ZIO_PRIORITY_ASYNC_READ,
0, dmu_prefetch_done, &dpa);
}
rw_exit(&dn->dn_struct_rwlock);
/* wait for prefetch L0 reads to finish */
mutex_enter(&dpa.dpa_lock);
while (dpa.dpa_pending_io > 0) {
cv_wait(&dpa.dpa_cv, &dpa.dpa_lock);
}
mutex_exit(&dpa.dpa_lock);
mutex_destroy(&dpa.dpa_lock);
cv_destroy(&dpa.dpa_cv);
}
/*
* Issue prefetch I/Os for the given L0 block range and wait for the I/O
* to complete. This does not enforce dmu_prefetch_max and will prefetch
* the entire range. The blocks are read from disk into the ARC but no
* decompression occurs (i.e., the dbuf cache is not required).
*/
int
dmu_prefetch_wait(objset_t *os, uint64_t object, uint64_t offset, uint64_t size)
{
dnode_t *dn;
int err = 0;
err = dnode_hold(os, object, FTAG, &dn);
if (err != 0)
return (err);
/*
* Chunk the requests (16 indirects worth) so that we can be interrupted
*/
uint64_t chunksize;
if (dn->dn_indblkshift) {
uint64_t nbps = bp_span_in_blocks(dn->dn_indblkshift, 1);
chunksize = (nbps * 16) << dn->dn_datablkshift;
} else {
chunksize = dn->dn_datablksz;
}
while (size > 0) {
uint64_t mylen = MIN(size, chunksize);
dmu_prefetch_wait_by_dnode(dn, offset, mylen);
offset += mylen;
size -= mylen;
if (issig()) {
err = SET_ERROR(EINTR);
break;
}
}
dnode_rele(dn, FTAG);
return (err);
}
/*
* Issue prefetch I/Os for the given object's dnode.
*/
@@ -1451,6 +1551,114 @@ dmu_write_uio(objset_t *os, uint64_t object, zfs_uio_t *uio, uint64_t size,
}
#endif /* _KERNEL */
static void
dmu_cached_bps(spa_t *spa, blkptr_t *bps, uint_t nbps,
uint64_t *l1sz, uint64_t *l2sz)
{
int cached_flags;
if (bps == NULL)
return;
for (size_t blk_off = 0; blk_off < nbps; blk_off++) {
blkptr_t *bp = &bps[blk_off];
if (BP_IS_HOLE(bp))
continue;
cached_flags = arc_cached(spa, bp);
if (cached_flags == 0)
continue;
if ((cached_flags & (ARC_CACHED_IN_L1 | ARC_CACHED_IN_L2)) ==
ARC_CACHED_IN_L2)
*l2sz += BP_GET_LSIZE(bp);
else
*l1sz += BP_GET_LSIZE(bp);
}
}
/*
* Estimate DMU object cached size.
*/
int
dmu_object_cached_size(objset_t *os, uint64_t object,
uint64_t *l1sz, uint64_t *l2sz)
{
dnode_t *dn;
dmu_object_info_t doi;
int err = 0;
*l1sz = *l2sz = 0;
if (dnode_hold(os, object, FTAG, &dn) != 0)
return (0);
if (dn->dn_nlevels < 2) {
dnode_rele(dn, FTAG);
return (0);
}
dmu_object_info_from_dnode(dn, &doi);
for (uint64_t off = 0; off < doi.doi_max_offset;
off += dmu_prefetch_max) {
/* dbuf_read doesn't prefetch L1 blocks. */
dmu_prefetch_by_dnode(dn, 1, off,
dmu_prefetch_max, ZIO_PRIORITY_SYNC_READ);
}
/*
* Hold all valid L1 blocks, asking ARC the status of each BP
* contained in each such L1 block.
*/
uint_t nbps = bp_span_in_blocks(dn->dn_indblkshift, 1);
uint64_t l1blks = 1 + (dn->dn_maxblkid / nbps);
rw_enter(&dn->dn_struct_rwlock, RW_READER);
for (uint64_t blk = 0; blk < l1blks; blk++) {
dmu_buf_impl_t *db = NULL;
if (issig()) {
/*
* On interrupt, get out, and bubble up EINTR
*/
err = EINTR;
break;
}
/*
* If we get an i/o error here, the L1 can't be read,
* and nothing under it could be cached, so we just
* continue. Ignoring the error from dbuf_hold_impl
* or from dbuf_read is then a reasonable choice.
*/
err = dbuf_hold_impl(dn, 1, blk, B_TRUE, B_FALSE, FTAG, &db);
if (err != 0) {
/*
* ignore error and continue
*/
err = 0;
continue;
}
err = dbuf_read(db, NULL, DB_RF_CANFAIL);
if (err == 0) {
dmu_cached_bps(dmu_objset_spa(os), db->db.db_data,
nbps, l1sz, l2sz);
}
/*
* error may be ignored, and we continue
*/
err = 0;
dbuf_rele(db, FTAG);
}
rw_exit(&dn->dn_struct_rwlock);
dnode_rele(dn, FTAG);
return (err);
}
/*
* Allocate a loaned anonymous arc buffer.
*/