zpool: accept more redundant special and dedup vdevs

The replication check required a special or dedup vdev to tolerate
exactly as many device failures as the normal vdevs in the pool.  A
3-way mirror special vdev on a raidz1 pool, or on a 2-way mirror pool,
was rejected as a mismatch, and the only way past it was -f, which
also overrides unrelated checks.

Accept a special or dedup vdev which tolerates at least as many
failures as the normal vdevs of a redundant pool.  Special and dedup
vdevs are now compared with the normal vdevs rather than with whichever
vdev happens to precede them, so normal vdevs are still required to
match each other.  Less redundant special or dedup vdevs, and redundant
ones added to a non-redundant pool, are still rejected.

As a side effect, a pool that already has a more redundant special
vdev is now considered consistent, so later additions to it are
checked instead of being skipped.

Reviewed-by: Brian Behlendorf <[email protected]>
Signed-off-by: Andrey Mnatsakanov <[email protected]>
Assisted-by: Claude Code
Closes #12596
Closes #19165
This commit is contained in:
Andrey Mnatsakanov authored and GitHub committed 2026-09-24 09:33:40 -07:00
1 parent a58b0d52a4
commit 1d9c32c160
6 files changed
+227 -115

No files matched your search

+144 -107
View File
@@ -407,6 +407,7 @@ typedef struct replication_level {
const char *zprl_type;
uint64_t zprl_children;
uint64_t zprl_parity;
boolean_t zprl_bias;
} replication_level_t;
#define ZPOOL_FUZZ (16 * 1024 * 1024)
@@ -445,6 +446,123 @@ is_raidz_draid(replication_level_t *a, replication_level_t *b)
return (B_FALSE);
}
/*
* Number of device failures a toplevel vdev can tolerate.
*/
static uint64_t
rep_failures(const replication_level_t *rep)
{
if (strcmp(rep->zprl_type, VDEV_TYPE_MIRROR) == 0)
return (rep->zprl_children - 1);
return (rep->zprl_parity);
}
/*
* A special or dedup vdev need not match the layout of the normal vdevs in
* a redundant pool, provided it can tolerate at least as many device
* failures. Returns B_TRUE if one of 'a' and 'b' is such a vdev and the
* other is a normal vdev which it satisfies.
*/
static boolean_t
is_bias_more_redundant(const replication_level_t *a,
const replication_level_t *b)
{
const replication_level_t *bias, *normal;
if (a->zprl_bias == b->zprl_bias)
return (B_FALSE);
bias = a->zprl_bias ? a : b;
normal = a->zprl_bias ? b : a;
return (rep_failures(normal) > 0 &&
rep_failures(bias) >= rep_failures(normal));
}
/*
* Compare the replication levels of two toplevel vdevs. Returns B_TRUE if
* they are consistent. Otherwise, if 'fatal' is set, an error message is
* displayed describing the mismatch.
*/
static boolean_t
rep_consistent(replication_level_t *lastrep, replication_level_t *rep,
boolean_t fatal)
{
replication_level_t *raidz, *mirror;
if (is_bias_more_redundant(lastrep, rep))
return (B_TRUE);
if (is_raidz_mirror(lastrep, rep, &raidz, &mirror) ||
is_raidz_mirror(rep, lastrep, &raidz, &mirror)) {
/*
* Accepted raidz and mirror when they can
* handle the same number of disk failures.
*/
if (raidz->zprl_parity != mirror->zprl_children - 1) {
if (fatal)
vdev_error(gettext(
"mismatched replication level: "
"%s and %s vdevs with different "
"redundancy, %llu vs. %llu (%llu-way) "
"are present\n"),
raidz->zprl_type,
mirror->zprl_type,
(u_longlong_t)raidz->zprl_parity,
(u_longlong_t)mirror->zprl_children - 1,
(u_longlong_t)mirror->zprl_children);
return (B_FALSE);
}
} else if (is_raidz_draid(lastrep, rep)) {
/*
* Accepted raidz and draid when they can
* handle the same number of disk failures.
*/
if (lastrep->zprl_parity != rep->zprl_parity) {
if (fatal)
vdev_error(gettext(
"mismatched replication level: "
"%s and %s vdevs with different "
"redundancy, %llu vs. %llu are present\n"),
lastrep->zprl_type,
rep->zprl_type,
(u_longlong_t)lastrep->zprl_parity,
(u_longlong_t)rep->zprl_parity);
return (B_FALSE);
}
} else if (strcmp(lastrep->zprl_type, rep->zprl_type) != 0) {
if (fatal)
vdev_error(gettext(
"mismatched replication level: "
"both %s and %s vdevs are present\n"),
lastrep->zprl_type, rep->zprl_type);
return (B_FALSE);
} else if (lastrep->zprl_parity != rep->zprl_parity) {
if (fatal)
vdev_error(gettext(
"mismatched replication level: "
"both %llu and %llu device parity "
"%s vdevs are present\n"),
(u_longlong_t)lastrep->zprl_parity,
(u_longlong_t)rep->zprl_parity,
rep->zprl_type);
return (B_FALSE);
} else if (lastrep->zprl_children != rep->zprl_children) {
if (fatal)
vdev_error(gettext(
"mismatched replication level: "
"both %llu-way and %llu-way %s "
"vdevs are present\n"),
(u_longlong_t)lastrep->zprl_children,
(u_longlong_t)rep->zprl_children,
rep->zprl_type);
return (B_FALSE);
}
return (B_TRUE);
}
/*
* Given a list of toplevel vdevs, return the current replication level. If
* the config is inconsistent, then NULL is returned. If 'fatal' is set, then
@@ -460,9 +578,10 @@ get_replication(nvlist_t *nvroot, boolean_t fatal)
nvlist_t *nv;
const char *type;
replication_level_t lastrep = {0};
replication_level_t lastbias = {0};
replication_level_t rep;
replication_level_t *ret;
replication_level_t *raidz, *mirror;
replication_level_t *same, *other;
boolean_t dontreport;
ret = safe_malloc(sizeof (replication_level_t));
@@ -657,116 +776,34 @@ get_replication(nvlist_t *nvroot, boolean_t fatal)
/*
* At this point, we have the replication of the last toplevel
* vdev in 'rep'. Compare it to 'lastrep' to see if it is
* different.
* vdev in 'rep'. Compare it to the last vdev of the same class
* (normal, or special and dedup), and to the last vdev of the
* other class.
*/
if (lastrep.zprl_type != NULL) {
if (is_raidz_mirror(&lastrep, &rep, &raidz, &mirror) ||
is_raidz_mirror(&rep, &lastrep, &raidz, &mirror)) {
/*
* Accepted raidz and mirror when they can
* handle the same number of disk failures.
*/
if (raidz->zprl_parity !=
mirror->zprl_children - 1) {
if (ret != NULL)
free(ret);
ret = NULL;
if (fatal)
vdev_error(gettext(
"mismatched replication "
"level: "
"%s and %s vdevs with "
"different redundancy, "
"%llu vs. %llu (%llu-way) "
"are present\n"),
raidz->zprl_type,
mirror->zprl_type,
(u_longlong_t)
raidz->zprl_parity,
(u_longlong_t)
mirror->zprl_children - 1,
(u_longlong_t)
mirror->zprl_children);
else
return (NULL);
}
} else if (is_raidz_draid(&lastrep, &rep)) {
/*
* Accepted raidz and draid when they can
* handle the same number of disk failures.
*/
if (lastrep.zprl_parity != rep.zprl_parity) {
if (ret != NULL)
free(ret);
ret = NULL;
if (fatal)
vdev_error(gettext(
"mismatched replication "
"level: %s and %s vdevs "
"with different "
"redundancy, %llu vs. "
"%llu are present\n"),
lastrep.zprl_type,
rep.zprl_type,
(u_longlong_t)
lastrep.zprl_parity,
(u_longlong_t)
rep.zprl_parity);
else
return (NULL);
}
} else if (strcmp(lastrep.zprl_type, rep.zprl_type) !=
0) {
if (ret != NULL)
free(ret);
ret = NULL;
if (fatal)
vdev_error(gettext(
"mismatched replication level: "
"both %s and %s vdevs are "
"present\n"),
lastrep.zprl_type, rep.zprl_type);
else
return (NULL);
} else if (lastrep.zprl_parity != rep.zprl_parity) {
if (ret)
free(ret);
ret = NULL;
if (fatal)
vdev_error(gettext(
"mismatched replication level: "
"both %llu and %llu device parity "
"%s vdevs are present\n"),
(u_longlong_t)
lastrep.zprl_parity,
(u_longlong_t)rep.zprl_parity,
rep.zprl_type);
else
return (NULL);
} else if (lastrep.zprl_children != rep.zprl_children) {
if (ret)
free(ret);
ret = NULL;
if (fatal)
vdev_error(gettext(
"mismatched replication level: "
"both %llu-way and %llu-way %s "
"vdevs are present\n"),
(u_longlong_t)
lastrep.zprl_children,
(u_longlong_t)
rep.zprl_children,
rep.zprl_type);
else
return (NULL);
}
rep.zprl_bias = nvlist_exists(nv,
ZPOOL_CONFIG_ALLOCATION_BIAS);
same = rep.zprl_bias ? &lastbias : &lastrep;
other = rep.zprl_bias ? &lastrep : &lastbias;
if ((same->zprl_type != NULL &&
!rep_consistent(same, &rep, fatal)) ||
(other->zprl_type != NULL &&
!rep_consistent(other, &rep, fatal))) {
free(ret);
ret = NULL;
if (!fatal)
return (NULL);
}
lastrep = rep;
*same = rep;
}
/*
* Report the replication level of the normal vdevs, since that is
* what any special or dedup vdevs were required to satisfy.
*/
if (ret != NULL)
*ret = rep;
*ret = (lastrep.zprl_type != NULL) ? lastrep :
(lastbias.zprl_type != NULL) ? lastbias : rep;
return (ret);
}
@@ -830,7 +867,7 @@ check_replication(nvlist_t *config, nvlist_t *newroot)
* the current pool.
*/
ret = 0;
if (current != NULL) {
if (current != NULL && !is_bias_more_redundant(current, new)) {
if (is_raidz_mirror(current, new, &raidz, &mirror) ||
is_raidz_mirror(new, current, &raidz, &mirror)) {
if (raidz->zprl_parity != mirror->zprl_children - 1) {
+5 -5
View File
@@ -18,7 +18,7 @@
.\" Copyright (c) 2017 Open-E, Inc. All Rights Reserved.
.\" Copyright (c) 2026 Seagate Technology, LLC.
.\"
.Dd August 6, 2025
.Dd September 21, 2026
.Dt ZPOOLCONCEPTS 7
.Os
.
@@ -193,15 +193,15 @@ For more information, see the
section.
.It Sy dedup
A device solely dedicated for deduplication tables.
The redundancy of this device should match the redundancy of the other normal
devices in the pool.
The redundancy of this device should match or exceed the redundancy of the
other normal devices in the pool.
If more than one dedup device is specified, then
allocations are load-balanced between those devices.
.It Sy special
A device dedicated solely for allocating various kinds of internal metadata,
and optionally small file blocks.
The redundancy of this device should match the redundancy of the other normal
devices in the pool.
The redundancy of this device should match or exceed the redundancy of the
other normal devices in the pool.
If more than one special device is specified, then
allocations are load-balanced between those devices.
.Pp
+1 -1
View File
@@ -38,7 +38,7 @@ tests = ['alloc_class_001_pos', 'alloc_class_002_neg', 'alloc_class_003_pos',
'alloc_class_007_pos', 'alloc_class_008_pos', 'alloc_class_009_pos',
'alloc_class_010_pos', 'alloc_class_011_neg', 'alloc_class_012_pos',
'alloc_class_013_pos', 'alloc_class_014_pos', 'alloc_class_015_neg',
'alloc_class_016_pos']
'alloc_class_016_pos', 'alloc_class_017_pos']
tags = ['functional', 'alloc_class']
[tests/functional/append]
+1
View File
@@ -483,6 +483,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
functional/alloc_class/alloc_class_014_pos.ksh \
functional/alloc_class/alloc_class_015_neg.ksh \
functional/alloc_class/alloc_class_016_pos.ksh \
functional/alloc_class/alloc_class_017_pos.ksh \
functional/alloc_class/cleanup.ksh \
functional/alloc_class/setup.ksh \
functional/append/file_append.ksh \
@@ -0,0 +1,74 @@
#!/bin/ksh -p
# SPDX-License-Identifier: CDDL-1.0
#
# This file and its contents are supplied under the terms of the
# Common Development and Distribution License ("CDDL"), version 1.0.
# You may only use this file in accordance with the terms of version
# 1.0 of the CDDL.
#
# A full copy of the text of the CDDL should have accompanied this
# source. A copy of the CDDL is also available via the Internet at
# https://opensource.org/license/CDDL-1.0.
#
. $STF_SUITE/tests/functional/alloc_class/alloc_class.kshlib
#
# DESCRIPTION:
# A special or dedup device which can tolerate more device failures
# than the normal vdevs of a redundant pool is accepted without -f.
# One which tolerates fewer failures, or which is added to a
# non-redundant pool, is still rejected.
#
# STRATEGY:
# 1. Create redundant pools with more redundant special/dedup mirrors.
# 2. Add a more redundant special mirror to an existing raidz pool.
# 3. Verify less redundant special mirrors are still rejected.
#
verify_runnable "global"
claim="A more redundant special or dedup device is accepted."
log_assert $claim
log_onexit cleanup
log_must disk_setup
# raidz1 tolerates one failure, a 3-way mirror tolerates two.
for class in special dedup; do
log_must zpool create $TESTPOOL raidz $ZPOOL_DISKS \
$class mirror $CLASS_DISK0 $CLASS_DISK1 $CLASS_DISK2
log_must zpool destroy -f $TESTPOOL
done
log_must zpool create $TESTPOOL mirror $ZPOOL_DISK0 $ZPOOL_DISK1 \
special mirror $CLASS_DISK0 $CLASS_DISK1 $CLASS_DISK2
log_must zpool destroy -f $TESTPOOL
log_must zpool create $TESTPOOL draid $ZPOOL_DISKS \
special mirror $CLASS_DISK0 $CLASS_DISK1 $CLASS_DISK2
log_must zpool destroy -f $TESTPOOL
log_must zpool create $TESTPOOL raidz $ZPOOL_DISKS
log_must zpool add $TESTPOOL special mirror \
$CLASS_DISK0 $CLASS_DISK1 $CLASS_DISK2
log_must zpool iostat -H $TESTPOOL $CLASS_DISK2
log_must zpool destroy -f $TESTPOOL
# raidz2 tolerates two failures, a 2-way mirror only one.
log_mustnot zpool create $TESTPOOL raidz2 $ZPOOL_DISKS \
special mirror $CLASS_DISK0 $CLASS_DISK1
log_mustnot poolexists $TESTPOOL
log_must zpool create $TESTPOOL raidz2 $ZPOOL_DISKS
log_mustnot zpool add $TESTPOOL special mirror $CLASS_DISK0 $CLASS_DISK1
log_must zpool destroy -f $TESTPOOL
# A non-redundant pool with a redundant special device is still rejected.
log_mustnot zpool create $TESTPOOL $ZPOOL_DISKS \
special mirror $CLASS_DISK0 $CLASS_DISK1 $CLASS_DISK2
log_mustnot poolexists $TESTPOOL
log_pass $claim
@@ -102,6 +102,8 @@ set -A valid_args \
cache $vdev7 log mirror $vdev8 $vdev9" \
"draid $vdev0 $vdev1 $vdev2 draid $vdev4 $vdev5 $vdev6 $vdev7 \
special mirror $vdev8 $vdev9" \
"draid $vdev0 $vdev1 $vdev2 draid $vdev4 $vdev5 $vdev6 \
special mirror $vdev7 $vdev8 $vdev9" \
"spare $vdev0 $vdev1 $vdev2 mirror $vdev3 $vdev4 raidz $vdev5 $vdev6"
set -A forced_args \
@@ -125,8 +127,6 @@ set -A forced_args \
draid2 $vdev5 $vdev6 $vdev7 $vdev8 spare $vdev9" \
"draid $vdev0 $vdev1 $vdev2 $vdev3 \
draid2 $vdev4 $vdev5 $vdev6 $vdev7 $vdev8" \
"draid $vdev0 $vdev1 $vdev2 draid $vdev4 $vdev5 $vdev6 \
special mirror $vdev7 $vdev8 $vdev9" \
"spare $vdev0 $vdev1 $vdev2 mirror $vdev3 $vdev4 \
raidz2 $vdev5 $vdev6 $vdev7"