Sala 6

RAID-Z

El RAID-5 clásico mantiene una banda fija sobre sus discos: unos datos y la paridad de esos datos. Para cambiar un fragmento, debe escribir los datos nuevos y la paridad nueva, in situ, en dos escrituras separadas. Un corte de corriente entre ambas deja una banda cuya paridad ya no coincide con sus datos, y nada en el disco lo indica. Cuando más tarde muere un disco, su contenido se reconstruye a partir de esa paridad: erróneo, sin ningún aviso. Es el agujero de escritura («write hole»).

Una escritura pequeña también sale cara: para calcular la paridad nueva, el RAID-5 tiene que leer primero el resto de la banda, o los datos antiguos y la paridad antigua. RAID-Z no hace ni lo uno ni lo otro, porque nunca actualiza una banda: cada bloque recibe una banda propia, tan ancha como el bloque necesite.

1 Un bloque, una banda

Cuando ZFS escribe un bloque en un vdev raidz, vdev_raidz_map_alloc corta ese bloque, y solo ese bloque, en columnas, una por disco hijo. Las columnas de paridad van primero, P, y después Q y R para raidz2 y raidz3 (3 como máximo). Les siguen las columnas de datos, cada una un tramo contiguo del bloque: no un sector por disco por turnos, como suele dibujarse el RAID. Después vienen los sectores de relleno que redondean la asignación hacia arriba. Elige un tamaño, una paridad, un número de discos y un tamaño de sector: el mapa y cada número son lo que calcula el código.

Bloque de 24 KiB: 6 sectores de datos, 4 sectores de paridad, 2 sectores de relleno, en 6 de 6 columnas. asize = 48 KiB, 2,00 veces psize.

s
⌈psize / 2ashift⌉ = ⌈24576 / 4096⌉ = 6 código
f
b % dcols = 0 % 6 = 0 código
o
b / dcols = 0 / 6 = 0 código
q
s / (dcols − nparity) = 6 / 4 = 1 código
r
s − q × 4 = 6 − 4 = 2 código
bc
r + nparity = 2 + 2 = 4 código
tot
s + nparity × (q + 1) = 6 + 2 × 2 = 10 código
acols
dcols = 6 código
scols
dcols = 6 código
rm_nskip
roundup(tot, 3) − tot = 12 − 10 = 2 código
asize
roundup(6 + 2 × ⌈6 / 4⌉, 3) = 12 × 4096 = 49152 (48 KiB) código
Versión en texto de esta figura

Bloque de 24 KiB: 6 sectores de datos, 4 sectores de paridad, 2 sectores de relleno, en 6 de 6 columnas. asize = 48 KiB, 2,00 veces psize.

  1. Columna 0, P, en el hijo 0 desde el sector 0: 2 sectores de paridad.
  2. Columna 1, Q, en el hijo 1 desde el sector 0: 2 sectores de paridad.
  3. Columna 2, D0, en el hijo 2 desde el sector 0: sectores 0 y 1 del bloque.
  4. Columna 3, D1, en el hijo 3 desde el sector 0: sectores 2 y 3 del bloque.
  5. Columna 4, D2, en el hijo 4 en el sector 0: sector 4 del bloque. Después, un sector de relleno, el sector 1 de ese hijo.
  6. Columna 5, D3, en el hijo 5 en el sector 0: sector 5 del bloque. Después, un sector de relleno, el sector 1 de ese hijo.

2 Sin agujero de escritura

La paridad de un bloque RAID-Z se calcula a partir de sus propias columnas de datos, y todas las columnas se envían a la vez a sus discos: no se lee nada antiguo, no se reescribe nada antiguo. El bloque va al espacio libre, como todo bloque en ZFS (copia en escritura), y la versión antigua se queda donde estaba hasta que un grupo de transacciones que ya no la necesita se confirma con una sola escritura de uberblock (grupos de transacciones). Un corte de corriente a mitad deja una banda a medio escribir a la que nada apunta.

Esa mitad del argumento no está en vdev_raidz.c: RAID-Z escribe una banda entera, y la copia en escritura y la confirmación del txg garantizan que nadie la lea antes de que esté entera.

RAID-5: la banda se actualiza in situ

  1. Antes: D1 forma parte de una banda compartida con D0 y D2; P es su paridad.

  2. Leer D0, D2 (o el antiguo D1 y P), calcular P′, escribir D1′ sobre D1. La corriente se corta antes de que se escriba P′.

  3. Tras el reinicio, P ya no coincide con D0, D1′, D2, y nada lo indica. Cuando el disco 0 muere, D0 se reconstruye a partir de la paridad errónea: basura, en silencio.

RAID-Z: el bloque nuevo recibe una banda nueva

  1. Antes: el bloque (A0, A1 y su paridad PA) forma una banda propia.

  2. La versión nueva (B0, B1, PB) va al espacio libre como una banda propia, con la paridad calculada solo a partir de ella. La corriente se corta en mitad de la escritura: B1 nunca llega.

  3. Tras el reinicio, el último uberblock confirmado sigue apuntando al bloque antiguo, intacto. Nada hace referencia a la banda a medio escribir: es espacio libre.

La idea, a tres profundidades

En una frase

RAID-Z nunca actualiza una banda: cada bloque se escribe en espacio libre como una banda propia, datos y paridad juntos, y la suma de comprobación de su padre dice qué disco miente cuando se vuelve a leer el bloque.

En un párrafo

Un vdev raidz de n hijos con p de paridad es un único espacio de direcciones, intercalado sector a sector entre los hijos. Un bloque de s sectores empieza en algún punto de él y ocupa ⌈s / (n − p)⌉ filas: sus datos en columnas que contienen cada una un tramo contiguo del bloque, y p columnas de paridad tan largas como la columna de datos más larga. Un bloque pequeño solo usa las columnas que necesita: un sector de datos cuesta siempre p + 1 sectores, sea cual sea el ancho. Cada asignación se redondea hacia arriba a un múltiplo de p + 1 sectores, el tamaño del bloque más pequeño. En una lectura, ZFS lee solo las columnas de datos y las comprueba con la suma de comprobación del puntero de bloque padre. Si falla y ningún disco ha señalado un error, ZFS lo lee todo y prueba, una combinación tras otra, de qué discos desconfiar, hasta que la suma de comprobación se verifica; después reescribe la columna errónea.

En detalle

Cortar el bloque. Cuando la E/S llega al vdev raidz, su tamaño ya se ha rellenado hasta un sector entero.

vdev_raidz_map_alloc Extracto de module/zfs/vdev_raidz.c, líneas 615–649, OpenZFS zfs-2.4.4. Ver en GitHub
	raidz_row_t *rr;
	/* The starting RAIDZ (parent) vdev sector of the block. */
	uint64_t b = zio->io_offset >> ashift;
	/* The zio's size in units of the vdev's minimum sector size. */
	uint64_t s = zio->io_size >> ashift;
	/* The first column for this stripe. */
	uint64_t f = b % dcols;
	/* The starting byte offset on each child vdev. */
	uint64_t o = (b / dcols) << ashift;
	uint64_t acols, scols;

	raidz_map_t *rm =
	    kmem_zalloc(offsetof(raidz_map_t, rm_row[1]), KM_SLEEP);
	rm->rm_nrows = 1;

	/*
	 * "Quotient": The number of data sectors for this stripe on all but
	 * the "big column" child vdevs that also contain "remainder" data.
	 */
	uint64_t q = s / (dcols - nparity);

	/*
	 * "Remainder": The number of partial stripe data sectors in this I/O.
	 * This will add a sector to some, but not all, child vdevs.
	 */
	uint64_t r = s - q * (dcols - nparity);

	/* The number of "big columns" - those which contain remainder data. */
	uint64_t bc = (r == 0 ? 0 : r + nparity);

	/*
	 * The total number of data and parity sectors associated with
	 * this I/O.
	 */
	uint64_t tot = s + nparity * (q + (r == 0 ? 0 : 1));
  1. L617–L623 Todo va en sectores de 1 << ashift. b es donde empieza el bloque en el vdev, s su tamaño. El sector b del vdev está en el hijo b % dcols, en el sector b / dcols de ese hijo.
  2. L628 Una fila: en un raidz que no se está ampliando, el bloque entero es una sola banda.
  3. L634–L643 q filas completas de datos, r sectores sobrantes. Las bc «columnas grandes» reciben un sector más: las r columnas de datos que contienen el resto, y las columnas de paridad, que deben ser tan largas como la columna de datos más larga.
  4. L649 tot: datos más paridad, los sectores que realmente se escriben.

Colocar las columnas. La misma función da después a cada columna su hijo, su desplazamiento y su tamaño.

vdev_raidz_map_alloc Extracto de module/zfs/vdev_raidz.c, líneas 677–700, OpenZFS zfs-2.4.4. Ver en GitHub
	for (uint64_t c = 0; c < scols; c++) {
		raidz_col_t *rc = &rr->rr_col[c];
		uint64_t col = f + c;
		uint64_t coff = o;
		if (col >= dcols) {
			col -= dcols;
			coff += 1ULL << ashift;
		}
		rc->rc_devidx = col;
		rc->rc_offset = coff;

		if (c >= acols)
			rc->rc_size = 0;
		else if (c < bc)
			rc->rc_size = (q + 1) << ashift;
		else
			rc->rc_size = q << ashift;

		asize += rc->rc_size;
	}

	ASSERT3U(asize, ==, tot << ashift);
	rm->rm_nskip = roundup(tot, nparity + 1) - tot;
	rm->rm_skipstart = bc;
  1. L679–L686 La columna c está en el hijo f + c. Pasado el último hijo, vuelve al hijo 0, un sector más abajo: el bloque es un tramo de sectores consecutivos del espacio de direcciones del vdev.
  2. L688–L693 Una columna más allá de acols no contiene datos (un bloque más estrecho que el vdev); las bc primeras columnas tienen q + 1 sectores, las demás q.
  3. L699–L700 El redondeo: roundup(tot, nparity + 1) - tot sectores de relleno, a partir de la columna bc. Dónde caen: uno detrás de cada columna corta, volviendo a las columnas de paridad si la banda se queda sin columnas. Se escriben como ceros, en la misma E/S que su columna, o como una E/S opcional propia para una columna sin datos, de modo que la cola del disco pueda fusionar escrituras contiguas.

Lo que reserva el asignador. El mapa escribe tot sectores; el asignador ya ha reservado el tamaño redondeado, calculado solo a partir del tamaño.

vdev_raidz_psize_to_asize Extracto de module/zfs/vdev_raidz.c, líneas 2305–2317, OpenZFS zfs-2.4.4. Ver en GitHub
static uint64_t
vdev_raidz_psize_to_asize(vdev_t *vd, uint64_t psize, uint64_t txg)
{
	vdev_raidz_t *vdrz = vd->vdev_tsd;
	uint64_t asize;
	uint64_t ashift = vd->vdev_top->vdev_ashift;
	uint64_t nparity = vdrz->vd_nparity;

	uint64_t cols = vdev_raidz_get_logical_width(vdrz, txg);

	asize = ((psize - 1) >> ashift) + 1;
	asize += nparity * ((asize + cols - nparity - 1) / (cols - nparity));
	asize = roundup(asize, nparity + 1) << ashift;
  1. L2315 Sectores de datos: psize redondeado hacia arriba a un sector.
  2. L2316 Más nparity sectores de paridad por cada fila empezada de cols - nparity sectores de datos.
  3. L2317 Redondeado hacia arriba a un múltiplo de nparity + 1. En un raidz2 de 12 discos con sectores de 4 KiB, un bloque de 4 KiB ocupa 12 KiB, uno de 8 KiB 24 KiB (dos sectores de relleno), uno de 40 KiB 48 KiB: la figura calcula cualquier otro.

En raidz1, la columna de paridad y la primera columna de datos intercambian sus hijos en uno de cada dos megabytes del vdev, para que la paridad no esté siempre en el mismo disco; el comentario dice que el beneficio es dudoso, pero que el formato en disco debe conservarlo para siempre. Pon el primer sector b en 256 con sectores de 4 KiB para verlo.

Volver a leerlo. Una lectura normal solo pide las columnas de datos; la paridad se lee cuando falta un disco, o durante un scrub o un resilver (la resincronización de un disco). El bloque se reensambla y se comprueba con la suma de comprobación de su puntero de bloque padre. Un disco ausente es un error que ZFS conoce: su columna de datos se reconstruye a partir de la primera columna de paridad aún válida, P antes que Q antes que R (la elección). Pero un disco también puede devolver bytes erróneos e informar de que todo ha ido bien. Entonces la suma de comprobación falla y ningún disco tiene la culpa. ZFS lee todas las columnas y llama a vdev_raidz_combrec.

vdev_raidz_combrec Extracto de module/zfs/vdev_raidz.c, líneas 3523–3558, OpenZFS zfs-2.4.4. Ver en GitHub
	for (int num_failures = 1; num_failures <= nparity; num_failures++) {
		int tstore[VDEV_RAIDZ_MAXPARITY + 2];
		int *ltgts = &tstore[1]; /* value is logical child ID */


		/*
		 * Determine number of logical children, n.  See comment
		 * above raidz_simulate_failure().
		 */
		int n = 0;
		for (int w = physical_width;
		    w >= original_width; w--) {
			n += w;
		}

		ASSERT3U(num_failures, <=, nparity);
		ASSERT3U(num_failures, <=, VDEV_RAIDZ_MAXPARITY);

		/* Handle corner cases in combrec logic */
		ltgts[-1] = -1;
		for (int i = 0; i < num_failures; i++) {
			ltgts[i] = i;
		}
		ltgts[num_failures] = n;

		for (;;) {
			int err = raidz_reconstruct(zio, ltgts, num_failures,
			    nparity);
			if (err == EINVAL) {
				/*
				 * Reconstruction not possible with this #
				 * failures; try more failures.
				 */
				break;
			} else if (err == 0)
				return (0);
  1. L3523 Suponer que un disco miente, después dos, hasta nparity.
  2. L3532–L3536 n hijos lógicos. Sin ampliación es el número de discos, y el hijo lógico i es el disco i.
  3. L3542–L3546 La primera combinación, los discos 0 a num_failures - 1; el bucle que sigue a este extracto recorre las demás en el orden que enumera el comentario de encima de la función.
  4. L3549–L3558 raidz_reconstruct da esos discos por muertos, reconstruye sus columnas de datos a partir de la paridad y vuelve a consultar la suma de comprobación. EINVAL: con los errores ya conocidos, más columnas muertas que paridad; probar con más fallos. 0: la suma de comprobación se verifica.

La suma de comprobación es el oráculo. La paridad sola puede reconstruir una columna que ZFS sabe que falta, pero no puede decir cuál de las columnas que tiene miente; la suma de comprobación, guardada un nivel más arriba en el árbol, sí puede. Cuando una combinación se verifica, sus columnas de datos se marcan como errores de suma de comprobación, la paridad leída pero no usada se regenera y se compara, y los datos buenos se vuelven a escribir en cada columna que resultó errónea. Un disco que devuelve paridad errónea no queda al descubierto en una lectura normal, que no lee la paridad; un scrub lee todas las columnas. Con más discos ausentes que paridad, el vdev no se puede abrir; con entre uno y nparity, está degradado.

Resilver. Cuando vuelve un disco al que le faltan algunas escrituras, ZFS no copia el disco entero. Un resilver recorre el árbol de punteros de bloque, bloque indirecto a bloque indirecto, y visita cada bloque. Se salta los bloques nacidos fuera del intervalo de txg que a algún disco le faltan, y después pregunta al vdev si ese bloque toca uno de esos discos.

vdev_raidz_need_resilver Extracto de module/zfs/vdev_raidz.c, líneas 4011–4037, OpenZFS zfs-2.4.4. Ver en GitHub
	uint64_t s = ((psize - 1) >> ashift) + 1;
	/* The first column for this stripe. */
	uint64_t f = b % dcols;

	/* Unreachable by sequential resilver. */
	ASSERT3U(phys_birth, !=, TXG_UNKNOWN);

	if (!vdev_dtl_contains(vd, DTL_PARTIAL, phys_birth, 1))
		return (B_FALSE);

	if (s + nparity >= dcols)
		return (B_TRUE);

	for (uint64_t c = 0; c < s + nparity; c++) {
		uint64_t devidx = (f + c) % dcols;
		vdev_t *cvd = vd->vdev_child[devidx];

		/*
		 * dsl_scan_need_resilver() already checked vd with
		 * vdev_dtl_contains(). So here just check cvd with
		 * vdev_dtl_empty(), cheaper and a good approximation.
		 */
		if (!vdev_dtl_empty(cvd, DTL_PARTIAL))
			return (B_TRUE);
	}

	return (B_FALSE);
  1. L4018–L4019 El bloque no nació mientras a un disco le faltaban escrituras: no hay nada que hacer.
  2. L4021–L4022 Un bloque con al menos tantas columnas como discos los toca todos.
  3. L4024–L4035 Si no, solo si a alguno de los s + nparity discos a partir de la primera columna le faltan escrituras. En la figura, marca un disco como ausente y lee: el resultado dice si un resilver reescribiría este bloque.

Lo que la figura deja fuera

La figura ejecuta la aritmética de vdev_raidz_map_alloc y de vdev_raidz_psize_to_asize tal como la escribe el código, así que cada disposición y cada número son los del código. La lectura sigue las decisiones del código (qué columnas se leen, qué paridad reconstruye qué, el orden de las combinaciones, qué se vuelve a escribir), pero solo registra si cada columna es correcta o errónea, no sus bytes: una reconstrucción es correcta exactamente cuando todas las columnas que lee son correctas. Deja fuera los discos a los que les faltan escrituras (leídos como obsoletos, no como ausentes), los discos lentos que se omiten a propósito, los bloques gang, el camino del scrub y dRAID. Los bloques se limitan a 128 KiB y 16 discos para que el mapa siga siendo legible. La ampliación de RAID-Z, que añade un disco a un raidz existente y da a los bloques más antiguos mapas de varias filas, queda para otra ocasión.