diff --git a/cmd/ztest.c b/cmd/ztest.c
index f77a37c21545..3775e2ef2516 100644
--- a/cmd/ztest.c
+++ b/cmd/ztest.c
@@ -8495,17 +8495,24 @@ print_time(hrtime_t t, char *timebuf)
}
static nvlist_t *
-make_random_props(void)
+make_random_pool_props(void)
{
nvlist_t *props;
props = fnvlist_alloc();
- if (ztest_random(2) == 0)
- return (props);
+ /* Twenty percent of the time enable ZPOOL_PROP_DEDUP_TABLE_QUOTA */
+ if (ztest_random(5) == 0) {
+ fnvlist_add_uint64(props,
+ zpool_prop_to_name(ZPOOL_PROP_DEDUP_TABLE_QUOTA),
+ 2 * 1024 * 1024);
+ }
- fnvlist_add_uint64(props,
- zpool_prop_to_name(ZPOOL_PROP_AUTOREPLACE), 1);
+ /* Fifty percent of the time enable ZPOOL_PROP_AUTOREPLACE */
+ if (ztest_random(2) == 0) {
+ fnvlist_add_uint64(props,
+ zpool_prop_to_name(ZPOOL_PROP_AUTOREPLACE), 1);
+ }
return (props);
}
@@ -8537,7 +8544,7 @@ ztest_init(ztest_shared_t *zs)
zs->zs_mirrors = ztest_opts.zo_mirrors;
nvroot = make_vdev_root(NULL, NULL, NULL, ztest_opts.zo_vdev_size, 0,
NULL, ztest_opts.zo_raid_children, zs->zs_mirrors, 1);
- props = make_random_props();
+ props = make_random_pool_props();
/*
* We don't expect the pool to suspend unless maxfaults == 0,
diff --git a/include/sys/ddt.h b/include/sys/ddt.h
index 726f1a3902eb..e0129eda5cf5 100644
--- a/include/sys/ddt.h
+++ b/include/sys/ddt.h
@@ -151,7 +151,8 @@ enum ddt_phys_type {
*/
/* State flags for dde_flags */
-#define DDE_FLAG_LOADED (1 << 0) /* entry ready for use */
+#define DDE_FLAG_LOADED (1 << 0) /* entry ready for use */
+#define DDE_FLAG_OVERQUOTA (1 << 1) /* entry unusable, no space */
typedef struct {
/* key must be first for ddt_key_compare */
@@ -170,6 +171,7 @@ typedef struct {
uint8_t dde_flags; /* load state flags */
kcondvar_t dde_cv; /* signaled when load completes */
+ uint64_t dde_waiters; /* count of waiters on dde_cv */
avl_node_t dde_node; /* ddt_tree node */
} ddt_entry_t;
@@ -228,6 +230,7 @@ extern void ddt_histogram_add(ddt_histogram_t *dst, const ddt_histogram_t *src);
extern void ddt_histogram_stat(ddt_stat_t *dds, const ddt_histogram_t *ddh);
extern boolean_t ddt_histogram_empty(const ddt_histogram_t *ddh);
extern void ddt_get_dedup_object_stats(spa_t *spa, ddt_object_t *ddo);
+extern uint64_t ddt_get_ddt_dsize(spa_t *spa);
extern void ddt_get_dedup_histogram(spa_t *spa, ddt_histogram_t *ddh);
extern void ddt_get_dedup_stats(spa_t *spa, ddt_stat_t *dds_total);
diff --git a/include/sys/fs/zfs.h b/include/sys/fs/zfs.h
index b572c22a294e..fb461c2f7a78 100644
--- a/include/sys/fs/zfs.h
+++ b/include/sys/fs/zfs.h
@@ -258,6 +258,8 @@ typedef enum {
ZPOOL_PROP_BCLONEUSED,
ZPOOL_PROP_BCLONESAVED,
ZPOOL_PROP_BCLONERATIO,
+ ZPOOL_PROP_DEDUP_TABLE_SIZE,
+ ZPOOL_PROP_DEDUP_TABLE_QUOTA,
ZPOOL_NUM_PROPS
} zpool_prop_t;
diff --git a/include/sys/spa.h b/include/sys/spa.h
index f50cb5e04ee0..df41002ed09b 100644
--- a/include/sys/spa.h
+++ b/include/sys/spa.h
@@ -1051,6 +1051,7 @@ extern metaslab_class_t *spa_special_class(spa_t *spa);
extern metaslab_class_t *spa_dedup_class(spa_t *spa);
extern metaslab_class_t *spa_preferred_class(spa_t *spa, uint64_t size,
dmu_object_type_t objtype, uint_t level, uint_t special_smallblk);
+extern boolean_t spa_special_has_ddt(spa_t *spa);
extern void spa_evicting_os_register(spa_t *, objset_t *os);
extern void spa_evicting_os_deregister(spa_t *, objset_t *os);
diff --git a/include/sys/spa_impl.h b/include/sys/spa_impl.h
index 5605a35b8641..47f349327461 100644
--- a/include/sys/spa_impl.h
+++ b/include/sys/spa_impl.h
@@ -465,6 +465,9 @@ struct spa {
boolean_t spa_waiters_cancel; /* waiters should return */
char *spa_compatibility; /* compatibility file(s) */
+ uint64_t spa_dedup_table_quota; /* property DDT maximum size */
+ uint64_t spa_dedup_dsize; /* cached on-disk size of DDT */
+ uint64_t spa_dedup_class_full_txg; /* txg dedup class was full */
/*
* spa_refcount & spa_config_lock must be the last elements
diff --git a/lib/libzfs/libzfs.abi b/lib/libzfs/libzfs.abi
index a75f5bbb47b0..aee6e59c6bf8 100644
--- a/lib/libzfs/libzfs.abi
+++ b/lib/libzfs/libzfs.abi
@@ -2921,7 +2921,9 @@
-
+
+
+
diff --git a/lib/libzfs/libzfs_pool.c b/lib/libzfs/libzfs_pool.c
index d5e934045f40..9896948a2248 100644
--- a/lib/libzfs/libzfs_pool.c
+++ b/lib/libzfs/libzfs_pool.c
@@ -332,6 +332,24 @@ zpool_get_prop(zpool_handle_t *zhp, zpool_prop_t prop, char *buf,
intval = zpool_get_prop_int(zhp, prop, &src);
switch (prop) {
+ case ZPOOL_PROP_DEDUP_TABLE_QUOTA:
+ /*
+ * If dedup quota is 0, we translate this into 'none'
+ * (unless literal is set). And if it is UINT64_MAX
+ * we translate that as 'automatic' (limit to size of
+ * the dedicated dedup VDEV. Otherwise, fall throught
+ * into the regular number formating.
+ */
+ if (intval == 0) {
+ (void) strlcpy(buf, literal ? "0" : "none",
+ len);
+ break;
+ } else if (intval == UINT64_MAX) {
+ (void) strlcpy(buf, "auto", len);
+ break;
+ }
+ zfs_fallthrough;
+
case ZPOOL_PROP_SIZE:
case ZPOOL_PROP_ALLOCATED:
case ZPOOL_PROP_FREE:
@@ -342,6 +360,7 @@ zpool_get_prop(zpool_handle_t *zhp, zpool_prop_t prop, char *buf,
case ZPOOL_PROP_MAXDNODESIZE:
case ZPOOL_PROP_BCLONESAVED:
case ZPOOL_PROP_BCLONEUSED:
+ case ZPOOL_PROP_DEDUP_TABLE_SIZE:
if (literal)
(void) snprintf(buf, len, "%llu",
(u_longlong_t)intval);
diff --git a/lib/libzfs/libzfs_util.c b/lib/libzfs/libzfs_util.c
index 73ae0950ccb6..b865af71a1dc 100644
--- a/lib/libzfs/libzfs_util.c
+++ b/lib/libzfs/libzfs_util.c
@@ -1691,6 +1691,16 @@ zprop_parse_value(libzfs_handle_t *hdl, nvpair_t *elem, int prop,
"use 'none' to disable quota/refquota"));
goto error;
}
+ /*
+ * Pool dedup table quota; force use of 'none' instead of 0
+ */
+ if ((type & ZFS_TYPE_POOL) && *ivalp == 0 &&
+ (!isnone && !isauto) &&
+ prop == ZPOOL_PROP_DEDUP_TABLE_QUOTA) {
+ zfs_error_aux(hdl, dgettext(TEXT_DOMAIN,
+ "use 'none' to disable ddt table quota"));
+ goto error;
+ }
/*
* Special handling for "*_limit=none". In this case it's not
@@ -1732,6 +1742,10 @@ zprop_parse_value(libzfs_handle_t *hdl, nvpair_t *elem, int prop,
}
*ivalp = UINT64_MAX;
break;
+ case ZPOOL_PROP_DEDUP_TABLE_QUOTA:
+ ASSERT(type & ZFS_TYPE_POOL);
+ *ivalp = UINT64_MAX;
+ break;
default:
zfs_error_aux(hdl, dgettext(TEXT_DOMAIN,
"'auto' is invalid value for '%s'"),
diff --git a/man/man7/zpoolprops.7 b/man/man7/zpoolprops.7
index 5428ab8d3076..ff21e5300ce7 100644
--- a/man/man7/zpoolprops.7
+++ b/man/man7/zpoolprops.7
@@ -28,7 +28,7 @@
.\" Copyright (c) 2021, Colm Buckley
.\" Copyright (c) 2023, Klara Inc.
.\"
-.Dd January 2, 2024
+.Dd January 14, 2024
.Dt ZPOOLPROPS 7
.Os
.
@@ -73,6 +73,8 @@ The amount of storage used by cloned blocks.
Percentage of pool space used.
This property can also be referred to by its shortened column name,
.Sy cap .
+.It Sy dedup_table_size
+Total on-disk size of the deduplication table.
.It Sy expandsize
Amount of uninitialized space within the pool or device that can be used to
increase the total capacity of the pool.
@@ -348,6 +350,27 @@ See
and
.Xr zpool-upgrade 8
for more information on the operation of compatibility feature sets.
+.It Sy dedup_table_quota Ns = Ns Ar number Ns | Ns Sy none Ns | Ns Sy auto
+This property sets a limit on the on-disk size of the pool's dedup table.
+Entries will not be added to the dedup table once this size is reached;
+if a dedup table already exists, and is larger than this size, they
+will not be removed as part of setting this property.
+Existing entries will still have their reference counts updated.
+.Pp
+The actual size limit of the table may be above or below the quota,
+depending on the actual on-disk size of the entries (which may be
+approximated for purposes of calculating the quota).
+That is, setting a quota size of 1M may result in the maximum size being
+slightly below, or slightly above, that value.
+Set to
+.Sy 'none'
+to disable.
+In automatic mode, which is the default, the size of a dedicated dedup vdev
+is used as the quota limit.
+.Pp
+The
+.Sy dedup_table_quota
+property works for both legacy and fast dedup tables.
.It Sy dedupditto Ns = Ns Ar number
This property is deprecated and no longer has any effect.
.It Sy delegation Ns = Ns Sy on Ns | Ns Sy off
diff --git a/module/zcommon/zpool_prop.c b/module/zcommon/zpool_prop.c
index b367c95b836a..1838c937b7ae 100644
--- a/module/zcommon/zpool_prop.c
+++ b/module/zcommon/zpool_prop.c
@@ -23,7 +23,7 @@
* Copyright 2011 Nexenta Systems, Inc. All rights reserved.
* Copyright (c) 2012, 2018 by Delphix. All rights reserved.
* Copyright (c) 2021, Colm Buckley
- * Copyright (c) 2021, Klara Inc.
+ * Copyright (c) 2021, 2023, Klara Inc.
*/
#include
@@ -125,6 +125,9 @@ zpool_prop_init(void)
zprop_register_number(ZPOOL_PROP_BCLONERATIO, "bcloneratio", 0,
PROP_READONLY, ZFS_TYPE_POOL, "<1.00x or higher if cloned>",
"BCLONE_RATIO", B_FALSE, sfeatures);
+ zprop_register_number(ZPOOL_PROP_DEDUP_TABLE_SIZE, "dedup_table_size",
+ 0, PROP_READONLY, ZFS_TYPE_POOL, "", "DDTSIZE", B_FALSE,
+ sfeatures);
/* default number properties */
zprop_register_number(ZPOOL_PROP_VERSION, "version", SPA_VERSION,
@@ -133,6 +136,9 @@ zpool_prop_init(void)
zprop_register_number(ZPOOL_PROP_ASHIFT, "ashift", 0, PROP_DEFAULT,
ZFS_TYPE_POOL, "", "ASHIFT", B_FALSE,
sfeatures);
+ zprop_register_number(ZPOOL_PROP_DEDUP_TABLE_QUOTA, "dedup_table_quota",
+ UINT64_MAX, PROP_DEFAULT, ZFS_TYPE_POOL, "", "DDTQUOTA",
+ B_FALSE, sfeatures);
/* default index (boolean) properties */
zprop_register_index(ZPOOL_PROP_DELEGATION, "delegation", 1,
diff --git a/module/zfs/ddt.c b/module/zfs/ddt.c
index 4c53cb0a2f9b..ca73f1a31408 100644
--- a/module/zfs/ddt.c
+++ b/module/zfs/ddt.c
@@ -101,6 +101,22 @@
* object and (if necessary), removed from an old one. ddt_tree is cleared and
* the next txg can start.
*
+ * ## Dedup quota
+ *
+ * A maximum size for all DDTs on the pool can be set with the
+ * dedup_table_quota property. This is determined in ddt_over_quota() and
+ * enforced during ddt_lookup(). If the pool is at or over its quota limit,
+ * ddt_lookup() will only return entries for existing blocks, as updates are
+ * still possible. New entries will not be created; instead, ddt_lookup() will
+ * return NULL. In response, the DDT write stage (zio_ddt_write()) will remove
+ * the D bit on the block and reissue the IO as a regular write. The block will
+ * not be deduplicated.
+ *
+ * Note that this is based on the on-disk size of the dedup store. Reclaiming
+ * this space after deleting entries relies on the ZAP "shrinking" behaviour,
+ * without which, no space would be recovered and the DDT would continue to be
+ * considered "over quota". See zap_shrink_enabled.
+ *
* ## Repair IO
*
* If a read on a dedup block fails, but there are other copies of the block in
@@ -152,6 +168,13 @@ static kmem_cache_t *ddt_entry_cache;
*/
int zfs_dedup_prefetch = 0;
+/*
+ * If the dedup class cannot satisfy a DDT allocation, treat as over quota
+ * for this many TXGs.
+ */
+uint_t dedup_class_wait_txgs = 5;
+
+
static const ddt_ops_t *const ddt_ops[DDT_TYPES] = {
&ddt_zap_ops,
};
@@ -554,8 +577,6 @@ ddt_alloc(const ddt_key_t *ddk)
static void
ddt_free(ddt_entry_t *dde)
{
- ASSERT(dde->dde_flags & DDE_FLAG_LOADED);
-
for (int p = 0; p < DDT_PHYS_TYPES; p++)
ASSERT3P(dde->dde_lead_zio[p], ==, NULL);
@@ -575,9 +596,66 @@ ddt_remove(ddt_t *ddt, ddt_entry_t *dde)
ddt_free(dde);
}
+static boolean_t
+ddt_special_over_quota(spa_t *spa, metaslab_class_t *mc)
+{
+ if (mc != NULL && metaslab_class_get_space(mc) > 0) {
+ /* Over quota if allocating outside of this special class */
+ if (spa_syncing_txg(spa) <= spa->spa_dedup_class_full_txg +
+ dedup_class_wait_txgs) {
+ /* Waiting for some deferred frees to be processed */
+ return (B_TRUE);
+ }
+
+ /*
+ * We're considered over quota when we hit 85% full, or for
+ * larger drives, when there is less than 8GB free.
+ */
+ uint64_t allocated = metaslab_class_get_alloc(mc);
+ uint64_t capacity = metaslab_class_get_space(mc);
+ uint64_t limit = MAX(capacity * 85 / 100,
+ (capacity > (1LL<<33)) ? capacity - (1LL<<33) : 0);
+
+ return (allocated >= limit);
+ }
+ return (B_FALSE);
+}
+
+/*
+ * Check if the DDT is over its quota. This can be due to a few conditions:
+ * 1. 'dedup_table_quota' property is not 0 (none) and the dedup dsize
+ * exceeds this limit
+ *
+ * 2. 'dedup_table_quota' property is set to automatic and
+ * a. the dedup or special allocation class could not satisfy a DDT
+ * allocation in a recent transaction
+ * b. the dedup or special allocation class has exceeded its 85% limit
+ */
+static boolean_t
+ddt_over_quota(spa_t *spa)
+{
+ if (spa->spa_dedup_table_quota == 0)
+ return (B_FALSE);
+
+ if (spa->spa_dedup_table_quota != UINT64_MAX)
+ return (ddt_get_ddt_dsize(spa) > spa->spa_dedup_table_quota);
+
+ /*
+ * For automatic quota, table size is limited by dedup or special class
+ */
+ if (ddt_special_over_quota(spa, spa_dedup_class(spa)))
+ return (B_TRUE);
+ else if (spa_special_has_ddt(spa) &&
+ ddt_special_over_quota(spa, spa_special_class(spa)))
+ return (B_TRUE);
+
+ return (B_FALSE);
+}
+
ddt_entry_t *
ddt_lookup(ddt_t *ddt, const blkptr_t *bp, boolean_t add)
{
+ spa_t *spa = ddt->ddt_spa;
ddt_key_t search;
ddt_entry_t *dde;
ddt_type_t type;
@@ -592,13 +670,28 @@ ddt_lookup(ddt_t *ddt, const blkptr_t *bp, boolean_t add)
/* Find an existing live entry */
dde = avl_find(&ddt->ddt_tree, &search, &where);
if (dde != NULL) {
- /* Found it. If it's already loaded, we can just return it. */
+ /* If we went over quota, act like we didn't find it */
+ if (dde->dde_flags & DDE_FLAG_OVERQUOTA)
+ return (NULL);
+
+ /* If it's already loaded, we can just return it. */
if (dde->dde_flags & DDE_FLAG_LOADED)
return (dde);
/* Someone else is loading it, wait for it. */
+ dde->dde_waiters++;
while (!(dde->dde_flags & DDE_FLAG_LOADED))
cv_wait(&dde->dde_cv, &ddt->ddt_lock);
+ dde->dde_waiters--;
+
+ /* Loaded but over quota, forget we were ever here */
+ if (dde->dde_flags & DDE_FLAG_OVERQUOTA) {
+ if (dde->dde_waiters == 0) {
+ avl_remove(&ddt->ddt_tree, dde);
+ ddt_free(dde);
+ }
+ return (NULL);
+ }
return (dde);
}
@@ -639,14 +732,27 @@ ddt_lookup(ddt_t *ddt, const blkptr_t *bp, boolean_t add)
dde->dde_type = type; /* will be DDT_TYPES if no entry found */
dde->dde_class = class; /* will be DDT_CLASSES if no entry found */
- if (error == 0)
+ if (dde->dde_type == DDT_TYPES &&
+ dde->dde_class == DDT_CLASSES &&
+ ddt_over_quota(spa)) {
+ /* Over quota. If no one is waiting, clean up right now. */
+ if (dde->dde_waiters == 0) {
+ avl_remove(&ddt->ddt_tree, dde);
+ ddt_free(dde);
+ return (NULL);
+ }
+
+ /* Flag cleanup required */
+ dde->dde_flags |= DDE_FLAG_OVERQUOTA;
+ } else if (error == 0) {
ddt_stat_update(ddt, dde, -1ULL);
+ }
/* Entry loaded, everyone can proceed now */
dde->dde_flags |= DDE_FLAG_LOADED;
cv_broadcast(&dde->dde_cv);
- return (dde);
+ return (dde->dde_flags & DDE_FLAG_OVERQUOTA ? NULL : dde);
}
void
@@ -775,6 +881,7 @@ ddt_load(spa_t *spa)
memcpy(&ddt->ddt_histogram_cache, ddt->ddt_histogram,
sizeof (ddt->ddt_histogram));
spa->spa_dedup_dspace = ~0ULL;
+ spa->spa_dedup_dsize = ~0ULL;
}
return (0);
@@ -1032,6 +1139,7 @@ ddt_sync_table(ddt_t *ddt, dmu_tx_t *tx, uint64_t txg)
memcpy(&ddt->ddt_histogram_cache, ddt->ddt_histogram,
sizeof (ddt->ddt_histogram));
spa->spa_dedup_dspace = ~0ULL;
+ spa->spa_dedup_dsize = ~0ULL;
}
void
@@ -1123,7 +1231,13 @@ ddt_addref(spa_t *spa, const blkptr_t *bp)
ddt_enter(ddt);
dde = ddt_lookup(ddt, bp, B_TRUE);
- ASSERT3P(dde, !=, NULL);
+
+ /* Can be NULL if the entry for this block was pruned. */
+ if (dde == NULL) {
+ ddt_exit(ddt);
+ spa_config_exit(spa, SCL_ZIO, FTAG);
+ return (B_FALSE);
+ }
if (dde->dde_type < DDT_TYPES) {
ddt_phys_t *ddp;
diff --git a/module/zfs/ddt_stats.c b/module/zfs/ddt_stats.c
index af5365a1d114..39b4edfc0f6a 100644
--- a/module/zfs/ddt_stats.c
+++ b/module/zfs/ddt_stats.c
@@ -129,7 +129,8 @@ ddt_histogram_empty(const ddt_histogram_t *ddh)
void
ddt_get_dedup_object_stats(spa_t *spa, ddt_object_t *ddo_total)
{
- /* Sum the statistics we cached in ddt_object_sync(). */
+ memset(ddo_total, 0, sizeof (*ddo_total));
+
for (enum zio_checksum c = 0; c < ZIO_CHECKSUM_FUNCTIONS; c++) {
ddt_t *ddt = spa->spa_ddt[c];
if (!ddt)
@@ -138,8 +139,32 @@ ddt_get_dedup_object_stats(spa_t *spa, ddt_object_t *ddo_total)
for (ddt_type_t type = 0; type < DDT_TYPES; type++) {
for (ddt_class_t class = 0; class < DDT_CLASSES;
class++) {
+ dmu_object_info_t doi;
+ uint64_t cnt;
+ int err;
+
+ /*
+ * These stats were originally calculated
+ * during ddt_object_load().
+ */
+
+ err = ddt_object_info(ddt, type, class, &doi);
+ if (err != 0)
+ continue;
+
+ err = ddt_object_count(ddt, type, class, &cnt);
+ if (err != 0)
+ continue;
+
ddt_object_t *ddo =
&ddt->ddt_object_stats[type][class];
+
+ ddo->ddo_count = cnt;
+ ddo->ddo_dspace =
+ doi.doi_physical_blocks_512 << 9;
+ ddo->ddo_mspace = doi.doi_fill_count *
+ doi.doi_data_block_size;
+
ddo_total->ddo_count += ddo->ddo_count;
ddo_total->ddo_dspace += ddo->ddo_dspace;
ddo_total->ddo_mspace += ddo->ddo_mspace;
@@ -147,11 +172,24 @@ ddt_get_dedup_object_stats(spa_t *spa, ddt_object_t *ddo_total)
}
}
- /* ... and compute the averages. */
- if (ddo_total->ddo_count != 0) {
- ddo_total->ddo_dspace /= ddo_total->ddo_count;
- ddo_total->ddo_mspace /= ddo_total->ddo_count;
- }
+ /*
+ * This returns raw counts (not averages). One of the consumers,
+ * print_dedup_stats(), historically has expected raw counts.
+ */
+
+ spa->spa_dedup_dsize = ddo_total->ddo_dspace;
+}
+
+uint64_t
+ddt_get_ddt_dsize(spa_t *spa)
+{
+ ddt_object_t ddo_total;
+
+ /* recalculate after each txg sync */
+ if (spa->spa_dedup_dsize == ~0ULL)
+ ddt_get_dedup_object_stats(spa, &ddo_total);
+
+ return (spa->spa_dedup_dsize);
}
void
diff --git a/module/zfs/spa.c b/module/zfs/spa.c
index 638572996c3a..1095c0af37f0 100644
--- a/module/zfs/spa.c
+++ b/module/zfs/spa.c
@@ -406,6 +406,9 @@ spa_prop_get_config(spa_t *spa, nvlist_t **nvp)
spa_prop_add_list(*nvp, ZPOOL_PROP_BCLONERATIO, NULL,
brt_get_ratio(spa), src);
+ spa_prop_add_list(*nvp, ZPOOL_PROP_DEDUP_TABLE_SIZE, NULL,
+ ddt_get_ddt_dsize(spa), src);
+
spa_prop_add_list(*nvp, ZPOOL_PROP_HEALTH, NULL,
rvd->vdev_state, src);
@@ -672,6 +675,10 @@ spa_prop_validate(spa_t *spa, nvlist_t *props)
error = SET_ERROR(EINVAL);
break;
+ case ZPOOL_PROP_DEDUP_TABLE_QUOTA:
+ error = nvpair_value_uint64(elem, &intval);
+ break;
+
case ZPOOL_PROP_DELEGATION:
case ZPOOL_PROP_AUTOREPLACE:
case ZPOOL_PROP_LISTSNAPS:
@@ -4732,6 +4739,8 @@ spa_ld_get_props(spa_t *spa)
spa_prop_find(spa, ZPOOL_PROP_DELEGATION, &spa->spa_delegation);
spa_prop_find(spa, ZPOOL_PROP_FAILUREMODE, &spa->spa_failmode);
spa_prop_find(spa, ZPOOL_PROP_AUTOEXPAND, &spa->spa_autoexpand);
+ spa_prop_find(spa, ZPOOL_PROP_DEDUP_TABLE_QUOTA,
+ &spa->spa_dedup_table_quota);
spa_prop_find(spa, ZPOOL_PROP_MULTIHOST, &spa->spa_multihost);
spa_prop_find(spa, ZPOOL_PROP_AUTOTRIM, &spa->spa_autotrim);
spa->spa_autoreplace = (autoreplace != 0);
@@ -6588,6 +6597,8 @@ spa_create(const char *pool, nvlist_t *nvroot, nvlist_t *props,
spa->spa_autoexpand = zpool_prop_default_numeric(ZPOOL_PROP_AUTOEXPAND);
spa->spa_multihost = zpool_prop_default_numeric(ZPOOL_PROP_MULTIHOST);
spa->spa_autotrim = zpool_prop_default_numeric(ZPOOL_PROP_AUTOTRIM);
+ spa->spa_dedup_table_quota =
+ zpool_prop_default_numeric(ZPOOL_PROP_DEDUP_TABLE_QUOTA);
if (props != NULL) {
spa_configfile_set(spa, props, B_FALSE);
@@ -9631,6 +9642,9 @@ spa_sync_props(void *arg, dmu_tx_t *tx)
case ZPOOL_PROP_MULTIHOST:
spa->spa_multihost = intval;
break;
+ case ZPOOL_PROP_DEDUP_TABLE_QUOTA:
+ spa->spa_dedup_table_quota = intval;
+ break;
default:
break;
}
diff --git a/module/zfs/spa_misc.c b/module/zfs/spa_misc.c
index d1d41bbe7214..439e56f0d0df 100644
--- a/module/zfs/spa_misc.c
+++ b/module/zfs/spa_misc.c
@@ -1996,6 +1996,13 @@ spa_dedup_class(spa_t *spa)
return (spa->spa_dedup_class);
}
+boolean_t
+spa_special_has_ddt(spa_t *spa)
+{
+ return (zfs_ddt_data_is_special &&
+ spa->spa_special_class->mc_groups != 0);
+}
+
/*
* Locate an appropriate allocation class
*/
diff --git a/module/zfs/zio.c b/module/zfs/zio.c
index d68d5ababe79..bc5a3c9b709b 100644
--- a/module/zfs/zio.c
+++ b/module/zfs/zio.c
@@ -3503,6 +3503,15 @@ zio_ddt_write(zio_t *zio)
ddt_enter(ddt);
dde = ddt_lookup(ddt, bp, B_TRUE);
+ if (dde == NULL) {
+ /* DDT size is over its quota so no new entries */
+ zp->zp_dedup = B_FALSE;
+ BP_SET_DEDUP(bp, B_FALSE);
+ if (zio->io_bp_override == NULL)
+ zio->io_pipeline = ZIO_WRITE_PIPELINE;
+ ddt_exit(ddt);
+ return (zio);
+ }
ddp = &dde->dde_phys[p];
if (zp->zp_dedup_verify && zio_ddt_collision(zio, ddt, dde)) {
@@ -3727,6 +3736,26 @@ zio_dva_allocate(zio_t *zio)
* Fallback to normal class when an alloc class is full
*/
if (error == ENOSPC && mc != spa_normal_class(spa)) {
+ /*
+ * When the dedup or special class is spilling into the normal
+ * class, there can still be significant space available due
+ * to deferred frees that are in-flight. We track the txg when
+ * this occurred and back off adding new DDT entries for a few
+ * txgs to allow the free blocks to be processed.
+ */
+ if ((mc == spa_dedup_class(spa) || (spa_special_has_ddt(spa) &&
+ mc == spa_special_class(spa))) &&
+ spa->spa_dedup_class_full_txg != zio->io_txg) {
+ spa->spa_dedup_class_full_txg = zio->io_txg;
+ zfs_dbgmsg("%s[%d]: %s class spilling, req size %d, "
+ "%llu allocated of %llu",
+ spa_name(spa), (int)zio->io_txg,
+ mc == spa_dedup_class(spa) ? "dedup" : "special",
+ (int)zio->io_size,
+ (u_longlong_t)metaslab_class_get_alloc(mc),
+ (u_longlong_t)metaslab_class_get_space(mc));
+ }
+
/*
* If throttling, transfer reservation over to normal class.
* The io_allocator slot can remain the same even though we
diff --git a/tests/runfiles/common.run b/tests/runfiles/common.run
index ac2c541a9188..d48b243eefed 100644
--- a/tests/runfiles/common.run
+++ b/tests/runfiles/common.run
@@ -662,6 +662,12 @@ pre =
post =
tags = ['functional', 'deadman']
+[tests/functional/dedup]
+tests = ['dedup_quota']
+pre =
+post =
+tags = ['functional', 'dedup']
+
[tests/functional/delegate]
tests = ['zfs_allow_001_pos', 'zfs_allow_002_pos', 'zfs_allow_003_pos',
'zfs_allow_004_pos', 'zfs_allow_005_pos', 'zfs_allow_006_pos',
diff --git a/tests/zfs-tests/include/tunables.cfg b/tests/zfs-tests/include/tunables.cfg
index 721cf27f48ca..b4d7c4f72bab 100644
--- a/tests/zfs-tests/include/tunables.cfg
+++ b/tests/zfs-tests/include/tunables.cfg
@@ -28,6 +28,7 @@ CONDENSE_INDIRECT_COMMIT_ENTRY_DELAY_MS condense.indirect_commit_entry_delay_ms
CONDENSE_INDIRECT_OBSOLETE_PCT condense.indirect_obsolete_pct zfs_condense_indirect_obsolete_pct
CONDENSE_MIN_MAPPING_BYTES condense.min_mapping_bytes zfs_condense_min_mapping_bytes
DBUF_CACHE_SHIFT dbuf.cache_shift dbuf_cache_shift
+DDT_ZAP_DEFAULT_BS ddt_zap_default_bs ddt_zap_default_bs
DEADMAN_CHECKTIME_MS deadman.checktime_ms zfs_deadman_checktime_ms
DEADMAN_EVENTS_PER_SECOND deadman_events_per_second zfs_deadman_events_per_second
DEADMAN_FAILMODE deadman.failmode zfs_deadman_failmode
diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am
index 00f306122daa..a55c86bd4d4f 100644
--- a/tests/zfs-tests/tests/Makefile.am
+++ b/tests/zfs-tests/tests/Makefile.am
@@ -1415,6 +1415,9 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
functional/deadman/deadman_ratelimit.ksh \
functional/deadman/deadman_sync.ksh \
functional/deadman/deadman_zio.ksh \
+ functional/dedup/cleanup.ksh \
+ functional/dedup/setup.ksh \
+ functional/dedup/dedup_quota.ksh \
functional/delegate/cleanup.ksh \
functional/delegate/setup.ksh \
functional/delegate/zfs_allow_001_pos.ksh \
diff --git a/tests/zfs-tests/tests/functional/cli_root/zpool_get/zpool_get.cfg b/tests/zfs-tests/tests/functional/cli_root/zpool_get/zpool_get.cfg
index 6ebce9459190..e8a94ce209bc 100644
--- a/tests/zfs-tests/tests/functional/cli_root/zpool_get/zpool_get.cfg
+++ b/tests/zfs-tests/tests/functional/cli_root/zpool_get/zpool_get.cfg
@@ -47,6 +47,8 @@ typeset -a properties=(
"listsnapshots"
"autoexpand"
"dedupratio"
+ "dedup_table_quota"
+ "dedup_table_size"
"free"
"allocated"
"readonly"
diff --git a/tests/zfs-tests/tests/functional/dedup/cleanup.ksh b/tests/zfs-tests/tests/functional/dedup/cleanup.ksh
new file mode 100755
index 000000000000..b3c4c04d7761
--- /dev/null
+++ b/tests/zfs-tests/tests/functional/dedup/cleanup.ksh
@@ -0,0 +1,29 @@
+#!/bin/ksh -p
+#
+# CDDL HEADER START
+#
+# The contents of this file are subject to the terms of the
+# Common Development and Distribution License (the "License").
+# You may not use this file except in compliance with the License.
+#
+# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+# or https://opensource.org/licenses/CDDL-1.0.
+# See the License for the specific language governing permissions
+# and limitations under the License.
+#
+# When distributing Covered Code, include this CDDL HEADER in each
+# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+# If applicable, add the following below this CDDL HEADER, with the
+# fields enclosed by brackets "[]" replaced with your own identifying
+# information: Portions Copyright [yyyy] [name of copyright owner]
+#
+# CDDL HEADER END
+#
+
+#
+# Copyright (c) 2017 by Lawrence Livermore National Security, LLC.
+#
+
+. $STF_SUITE/include/libtest.shlib
+
+default_cleanup
diff --git a/tests/zfs-tests/tests/functional/dedup/dedup_quota.ksh b/tests/zfs-tests/tests/functional/dedup/dedup_quota.ksh
new file mode 100755
index 000000000000..5b83a1ca396f
--- /dev/null
+++ b/tests/zfs-tests/tests/functional/dedup/dedup_quota.ksh
@@ -0,0 +1,223 @@
+#!/bin/ksh -p
+# CDDL HEADER START
+#
+# The contents of this file are subject to the terms of the
+# Common Development and Distribution License (the "License").
+# You may not use this file except in compliance with the License.
+#
+# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+# or https://opensource.org/licenses/CDDL-1.0.
+# See the License for the specific language governing permissions
+# and limitations under the License.
+#
+# When distributing Covered Code, include this CDDL HEADER in each
+# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+# If applicable, add the following below this CDDL HEADER, with the
+# fields enclosed by brackets "[]" replaced with your own identifying
+# information: Portions Copyright [yyyy] [name of copyright owner]
+#
+# CDDL HEADER END
+#
+
+#
+# Copyright (c) 2023, Klara Inc.
+#
+
+# DESCRIPTION:
+# Verify that new entries are not added to the DDT when dedup_table_quota has
+# been exceeded.
+#
+# STRATEGY:
+# 1. Create a pool with dedup=on
+# 2. Set threshold for on-disk DDT via dedup_table_quota
+# 3. Verify the threshold is exceeded after zpool sync
+# 4. Verify no new entries are added after subsequent sync's
+# 5. Remove all but one entry from DDT
+# 6. Verify new entries are added to DDT
+#
+
+. $STF_SUITE/include/libtest.shlib
+. $STF_SUITE/tests/functional/events/events_common.kshlib
+
+verify_runnable "both"
+
+log_assert "DDT quota is enforced"
+
+MOUNTDIR="$TEST_BASE_DIR/dedup_mount"
+FILEPATH="$MOUNTDIR/dedup_file"
+VDEV_GENERAL="$TEST_BASE_DIR/vdevfile.general.$$"
+VDEV_DEDUP="$TEST_BASE_DIR/vdevfile.dedup.$$"
+POOL="dedup_pool"
+
+save_tunable TXG_TIMEOUT
+
+function cleanup
+{
+ if poolexists $POOL ; then
+ destroy_pool $POOL
+ fi
+ log_must rm -fd $VDEV_GENERAL $VDEV_DEDUP $MOUNTDIR
+ log_must restore_tunable TXG_TIMEOUT
+}
+
+
+function do_clean
+{
+ log_must destroy_pool $POOL
+ log_must rm -fd $VDEV_GENERAL $VDEV_DEDUP $MOUNTDIR
+}
+
+function do_setup
+{
+ log_must truncate -s 5G $VDEV_GENERAL
+ # Use 'xattr=sa' to prevent selinux xattrs influencing our accounting
+ log_must zpool create -o ashift=12 -f -O xattr=sa -m $MOUNTDIR $POOL $VDEV_GENERAL
+ log_must zfs set dedup=on $POOL
+ log_must set_tunable32 TXG_TIMEOUT 600
+}
+
+function dedup_table_size
+{
+ get_pool_prop dedup_table_size $POOL
+}
+
+function dedup_table_quota
+{
+ get_pool_prop dedup_table_quota $POOL
+}
+
+function ddt_entries
+{
+ typeset -i entries=$(zpool status -D $POOL | \
+ grep "dedup: DDT entries" | awk '{print $4}')
+
+ echo ${entries}
+}
+
+function ddt_add_entry
+{
+ count=$1
+ offset=$2
+ expand=$3
+
+ if [ -z "$offset" ]; then
+ offset=1
+ fi
+
+ for i in {$offset..$count}; do
+ echo "$i" > $MOUNTDIR/dedup-$i.txt
+ done
+ log_must sync_pool $POOL
+
+ log_note range $offset - $(( count + offset - 1 ))
+ log_note ddt_add_entry got $(ddt_entries)
+}
+
+# Create 6000 entries over three syncs
+function ddt_nolimit
+{
+ do_setup
+
+ log_note base ddt entries is $(ddt_entries)
+
+ ddt_add_entry 1
+ ddt_add_entry 100
+ ddt_add_entry 101 5000
+ ddt_add_entry 5001 6000
+
+ log_must test $(ddt_entries) -eq 6000
+
+ do_clean
+}
+
+function ddt_limit
+{
+ do_setup
+
+ log_note base ddt entries is $(ddt_entries)
+
+ log_must zpool set dedup_table_quota=32768 $POOL
+ ddt_add_entry 100
+
+ # it's possible to exceed dedup_table_quota over a single transaction,
+ # ensure that the threshold has been exceeded
+ cursize=$(dedup_table_size)
+ log_must test $cursize -gt $(dedup_table_quota)
+
+ # count the entries we have
+ log_must test $(ddt_entries) -ge 100
+
+ # attempt to add new entries
+ ddt_add_entry 101 200
+ log_must test $(ddt_entries) -eq 100
+ log_must test $cursize -eq $(dedup_table_size)
+
+ # remove all but one entry
+ for i in {2..100}; do
+ rm $MOUNTDIR/dedup-$i.txt
+ done
+ log_must sync_pool $POOL
+
+ log_must test $(ddt_entries) -eq 1
+ log_must test $cursize -gt $(dedup_table_size)
+ cursize=$(dedup_table_size)
+
+ log_must zpool set dedup_table_quota=none $POOL
+
+ # create more entries
+ zpool status -D $POOL
+ ddt_add_entry 101 200
+ log_must sync_pool $POOL
+
+ log_must test $(ddt_entries) -eq 101
+ log_must test $cursize -lt $(dedup_table_size)
+
+ do_clean
+}
+
+function ddt_dedup_vdev_limit
+{
+ do_setup
+
+ # add a dedicated dedup/special VDEV and enable an automatic quota
+ if (( RANDOM % 2 == 0 )) ; then
+ class="special"
+ else
+ class="dedup"
+ fi
+ log_must truncate -s 200M $VDEV_DEDUP
+ log_must zpool add $POOL $class $VDEV_DEDUP
+ log_must zpool set dedup_table_quota=auto $POOL
+
+ log_must zfs set recordsize=1K $POOL
+ log_must zfs set compression=zstd $POOL
+
+ # Generate a working set to fill up the dedup/special allocation class
+ log_must fio --directory=$MOUNTDIR --name=dedup-filler-1 \
+ --rw=read --bs=1m --numjobs=2 --iodepth=8 \
+ --size=512M --end_fsync=1 --ioengine=posixaio --runtime=1 \
+ --group_reporting --fallocate=none --output-format=terse \
+ --dedupe_percentage=0
+ log_must sync_pool $POOL
+
+ zpool status -D $POOL
+ zpool list -v $POOL
+ echo DDT size $(dedup_table_size), with $(ddt_entries) entries
+
+ #
+ # With no DDT quota in place, the above workload will produce over
+ # 800,000 entries by using space in the normal class. With a quota,
+ # it will be well below 500,000 entries.
+ #
+ log_must test $(ddt_entries) -le 500000
+
+ do_clean
+}
+
+log_onexit cleanup
+
+ddt_limit
+ddt_nolimit
+ddt_dedup_vdev_limit
+
+log_pass "DDT quota is enforced"
diff --git a/tests/zfs-tests/tests/functional/dedup/setup.ksh b/tests/zfs-tests/tests/functional/dedup/setup.ksh
new file mode 100755
index 000000000000..3c0830401f81
--- /dev/null
+++ b/tests/zfs-tests/tests/functional/dedup/setup.ksh
@@ -0,0 +1,31 @@
+#!/bin/ksh -p
+#
+# CDDL HEADER START
+#
+# The contents of this file are subject to the terms of the
+# Common Development and Distribution License (the "License").
+# You may not use this file except in compliance with the License.
+#
+# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
+# or https://opensource.org/licenses/CDDL-1.0.
+# See the License for the specific language governing permissions
+# and limitations under the License.
+#
+# When distributing Covered Code, include this CDDL HEADER in each
+# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
+# If applicable, add the following below this CDDL HEADER, with the
+# fields enclosed by brackets "[]" replaced with your own identifying
+# information: Portions Copyright [yyyy] [name of copyright owner]
+#
+# CDDL HEADER END
+#
+
+#
+# Copyright (c) 2017 by Lawrence Livermore National Security, LLC.
+#
+
+. $STF_SUITE/include/libtest.shlib
+
+DISK=${DISKS%% *}
+
+default_setup $DISK