Sala 12

Compresión, deduplicación, cifrado

Entre la aplicación y el disco, tres funciones pueden transformar un bloque. Cada una ahorra o protege algo, y cada una tiene un precio que el código paga en cada escritura.

La compresión ahorra espacio, pero comprimir cuesta CPU, y un bloque que encoge unos pocos bytes no ahorra nada una vez redondeado a sectores enteros. La deduplicación evita escribir un bloque que ya está en el disco, pero para saberlo tiene que buscar cada bloque escrito en una tabla, guardar esa tabla en el disco y mantenerla al día. El cifrado oculta los datos, pero el pool tiene que poder seguir verificando, reparando y enviando bloques de los que quizá no tenga la clave. El código responde a cada problema con una regla, y esta sala recorre las tres reglas una tras otra. En el pipeline de escritura se ejecutan en este orden: la compresión, después el cifrado, después la suma de comprobación de lo que se va a escribir, y después la búsqueda de deduplicación por esa suma de comprobación.

1 Compresión: ahorrar un octavo, o almacenar tal cual

El compresor recibe un límite: siete octavos del bloque, redondeados hacia abajo a un número entero de sectores. Si su salida no cabe, el bloque se almacena sin comprimir. Hay dos casos que nunca llegan al compresor o nunca llegan al disco: un bloque de ceros se convierte en un hueco, y una salida de 112 bytes o menos se pliega dentro del propio puntero de bloque. Elige los datos, el tamaño del bloque, el del sector y la compresión, y sigue la rama que decide.

  1. Compresión LZ4: el bloque pasa por las ramas siguientes. código
  2. El bloque no contiene solo ceros. código
  3. La salida (49 807 bytes) cabe por debajo del límite (112 KiB). código
  4. Más de 112 bytes: la salida no cabe en el puntero de bloque. código
  5. Redondeado al sector: 52 KiB, menos que 128 KiB. Se almacena comprimido, con el final a cero. código (esta rama decidió)

Almacenado comprimido. Salida de 49 807 bytes, comprimida con LZ4, redondeada al sector: PSIZE 52 KiB, 13 sectores con ashift 12.

Puntero de bloque: PSIZE 52 KiB, LSIZE 128 KiB, compresión LZ4; ASIZE 52 KiB.

Las 16 palabras de un puntero de bloque: no cifrado
PalabraContiene
0DVA 1: vdev, ASIZE
1DVA 1: G, desplazamiento
2DVA 2: vdev, ASIZE
3DVA 2: G, desplazamiento
4DVA 3: vdev, ASIZE
5DVA 3: G, desplazamiento
6B D X, nivel, tipo, suma de comprobación, E, compresión, PSIZE, LSIZE
7R, relleno
8relleno
9txg de nacimiento físico
atxg de nacimiento lógico
brecuento de bloques llenos
csuma de comprobación [0]
dsuma de comprobación [1]
esuma de comprobación [2]
fsuma de comprobación [3]

Los mismos datos, 128 KiB, LZ4, con cada tamaño de sector
ashiftLímiteResultadoPSIZEAsignado
9 (512 bytes)114 688Almacenado comprimido.50 17650 176
12 (4 KiB)114 688Almacenado comprimido.53 24853 248
13 (8 KiB)114 688Almacenado comprimido.57 34457 344
Versión en texto de esta figura

Salida de 49 807 bytes, comprimida con LZ4, redondeada al sector: PSIZE 52 KiB, 13 sectores con ashift 12.

2 Deduplicación: una tabla consultada en cada escritura

Con dedup=on, cada bloque escrito se busca por su suma de comprobación en la tabla de deduplicación (DDT). Si la tabla ya lo tiene, el nuevo puntero de bloque recibe la dirección de la copia existente y un contador de referencias aumenta: no se escribe nada. Si no, el bloque se escribe y una entrada nueva anota dónde. Escribe bloques, vuelve a escribir el mismo, y después lleva la tabla por encima de su cuota.

3 escrituras3 búsquedas en la DDT2 bloques de datos escritos2 entradas

La última escritura

  1. Bloque A, txg 102: suma de comprobación 5cf94b00…, buscada en la DDT.
  2. Está en la tabla: ddt_lookup devuelve su entrada, que contiene un DVA. código
  3. Acierto: el nuevo puntero de bloque recibe 0:400000 de la entrada, nacido en el txg 102 con nacimiento físico 100; contador de referencias 2. No se escribe nada. código

Tabla de deduplicación (DDT)

Una entrada por suma de comprobación
Clave (suma de comprobación)DatosRefs.DVANacimiento físico
5cf94b00…A20:400000100
fa9a4a00…B10:420000101

Punteros de bloque escritos

  1. bp0AD=10:400000nacido en el txg 100, nacimiento físico 100
  2. bp1BD=10:420000nacido en el txg 101, nacimiento físico 101
  3. bp2AD=10:400000nacido en el txg 102, nacimiento físico 100

Bloques de datos en el disco

  1. 0:400000Areferenciado por bp0, bp2
  2. 0:420000Breferenciado por bp1

Versión en texto de esta figura
  1. Fallo: A se escribe en 0:400000; la entrada recibe el DVA, contador de referencias 1.
  2. Fallo: B se escribe en 0:420000; la entrada recibe el DVA, contador de referencias 1.
  3. Acierto: el nuevo puntero de bloque recibe 0:400000 de la entrada, nacido en el txg 102 con nacimiento físico 100; contador de referencias 2. No se escribe nada.

3 escrituras, 3 búsquedas en la DDT, 2 bloques de datos escritos, 2 entradas.

Lo que cuesta. La figura cuenta una búsqueda en la DDT por escritura, con acierto o con fallo: un dataset con dedup=on la paga por cada bloque, no solo por los duplicados. Cada entrada es una clave de 40 bytes (la suma de comprobación de 256 bits y una palabra de tamaños e indicadores) y un valor de 256 bytes en el formato tradicional, 72 en el formato plano («flat») más reciente. La tabla vive en el disco como objetos ZAP, y el código explica por qué actualizarla es caro: «each entry only takes up a tiny portion of a ZAP leaf node, and so required reading the whole node, updating the entry, and writing it back. On busy pools, this could add serious IO and memory overheads.» (cada entrada ocupa solo una pequeña parte de una hoja de ZAP, así que había que leer la hoja entera, actualizar la entrada y volver a escribirla. En un pool con mucha carga, esto podía suponer un coste serio de E/S y de memoria). La funcionalidad fast_dedup responde con un log, el log de la DDT: las entradas modificadas se añaden a un log en el disco y se mantienen en memoria, y se vuelcan al ZAP poco a poco en cada txg. Solo las tablas creadas con la funcionalidad reciben el log; una tabla creada antes conserva la reescritura de una hoja de ZAP en cada txg. Un dedup_table_quota puede limitar el tamaño de la tabla en el disco; por encima de él, los bloques nuevos simplemente no se deduplican. Su valor por defecto, auto, no fija ningún tamaño y solo limita la tabla en un pool con un vdev dedup o special: en un pool de discos simples, la tabla crece con los datos. El código no da ninguna cifra de memoria por terabyte ni de relación de deduplicación: dependen de los datos, y esta sala no las inventa.

3 Cifrado: los secretos en el puntero de bloque, las sumas de comprobación en claro

Un bloque cifrado necesita una sal, un IV y un MAC, y estos viajan allí donde se hace referencia al bloque: en su puntero de bloque, en el lugar de la dirección de la tercera copia y de la mitad de la suma de comprobación. La otra mitad de la suma de comprobación se calcula sobre el texto cifrado, de modo que el pool puede verificar cada bloque sin la clave. Compara las palabras de cuatro clases de puntero de bloque, y después descarga la clave de un archivo cifrado y haz un scrub.

Un bloque de nivel 0 de un tipo cifrado: el tercer DVA contiene la sal y el principio del IV, la parte alta del recuento de bloques llenos el resto del IV, la mitad de la suma de comprobación el MAC. Copias, como máximo: 2. código

Las 16 palabras de un puntero de bloque: cifrado
PalabraContiene
0DVA 1: vdev, ASIZE
1DVA 1: G, desplazamiento
2DVA 2: vdev, ASIZE
3DVA 2: G, desplazamiento
4sal (cambiado)
5IV1: primeros 64 bits del IV (cambiado)
6B D X, nivel, tipo, suma de comprobación, E, compresión, PSIZE, LSIZE
7R, relleno
8relleno
9txg de nacimiento físico
atxg de nacimiento lógico
bIV2: últimos 32 bits del IV | recuento de bloques llenos, 32 bits (cambiado)
csuma de comprobación [0]
dsuma de comprobación [1]
eMAC [0] (cambiado)
fMAC [1] (cambiado)

Un archivo cifrado

Clave cargada: los bloques cifrados se descifran al leerlos.

  • bloque de dnodestipo cifrado, núcleo en claro
    núcleo del dnode, en claro: tipo, niveles, puntero de bloquebúfer bonus: «atributos del archivo»
    • bloque indirecto (L1)indirecto, suma de comprobación de los MAC
      tres punteros de bloque, en claro: DVA, nacimientos, sumas de comprobación, sales, IV, MAC
      • bloque de datos 0cifrado
        «Este texto»
      • bloque de datos 1cifrado
        «sigue legible»
      • bloque de datos 2cifrado
        «con la clave.»

Versión en texto de esta figura

Un puntero de bloque corriente: tres DVA, una suma de comprobación de 256 bits. Copias, como máximo: 3.

Un bloque de nivel 0 de un tipo cifrado: el tercer DVA contiene la sal y el principio del IV, la parte alta del recuento de bloques llenos el resto del IV, la mitad de la suma de comprobación el MAC. Copias, como máximo: 2.

Un bloque de nivel 0 de un tipo que debe seguir siendo legible: sus datos están en claro, protegidos por un MAC en la mitad de la suma de comprobación. Copias, como máximo: 3.

Un bloque indirecto de un dataset cifrado: en claro, sus palabras de MAC contienen una suma de comprobación de los MAC del nivel de debajo. Copias, como máximo: 3.

Clave descargada: los bloques de datos y el búfer bonus se leen como texto cifrado; el núcleo del dnode y el bloque indirecto están en claro en el disco.

La idea, a tres profundidades

En una frase

La compresión solo mantiene un bloque comprimido si ahorra al menos un octavo, contado en sectores enteros; la deduplicación sustituye una escritura por una referencia cuando la suma de comprobación del bloque ya está en su tabla, al precio de una búsqueda en cada escritura; el cifrado pone su sal, su IV y su MAC en el puntero de bloque y deja una suma de comprobación truncada del texto cifrado, de modo que el scrub no necesita clave.

En un párrafo

En zio_write_compress, un bloque de ceros se convierte en un hueco; si no, el compresor recibe un límite de siete octavos del bloque, ajustado hacia abajo al sector, y una salida que no cabe deja el bloque sin comprimir; una salida de 112 bytes como máximo va al puntero de bloque, que entonces contiene datos en lugar de direcciones; todo lo demás se redondea a sectores enteros. Después, en un dataset cifrado, el cifrado usa el tercer DVA para la sal y el principio del IV, la parte alta del recuento de bloques llenos para el resto del IV, y la mitad de la suma de comprobación para el MAC; solo se cifran los bloques de nivel 0 de los tipos cifrados. La suma de comprobación viene a continuación, calculada sobre lo que se va a escribir, texto cifrado incluido, y el scrub, que lee los bloques en bruto, verifica sus 128 bits restantes. Por último, con la deduplicación, zio_ddt_write busca esa suma de comprobación en la DDT: un acierto copia el DVA de la entrada en el nuevo puntero de bloque y añade una referencia, sin escritura; un fallo escribe el bloque y anota su DVA en una entrada nueva; una tabla por encima de su cuota rechaza las entradas nuevas, y el bloque se escribe sin su bit D.

En detalle

El límite. Antes de comprimir, la ruta de escritura pregunta qué tamaño puede tener la salida.

zio_get_compression_max_size Extracto de module/zfs/zio.c, líneas 1791–1809, OpenZFS zfs-2.4.4. Ver en GitHub
size_t
zio_get_compression_max_size(enum zio_compress compress, uint64_t gcd_alloc,
    uint64_t min_alloc, size_t s_len)
{
	size_t d_len;

	/* minimum 12.5% must be saved (legacy value, may be changed later) */
	d_len = s_len - (s_len >> 3);

	/* ZLE can't use exactly d_len bytes, it needs more, so ignore it */
	if (compress == ZIO_COMPRESS_ZLE)
		return (d_len);

	d_len = d_len - d_len % gcd_alloc;

	if (d_len < min_alloc)
		return (BPE_PAYLOAD_SIZE);
	return (d_len);
}
  1. L1797–L1798 Siete octavos del origen: hay que ahorrar al menos un 12,5 %, un «legacy value» (valor heredado), dice el comentario.
  2. L1800–L1802 ZLE, que solo comprime series de ceros, queda exento de lo que sigue.
  3. L1804 Redondeado hacia abajo a un múltiplo de spa_gcd_alloc, que en un pool de discos simples de un solo ashift es el sector (el máximo común divisor de los tamaños de asignación de los vdevs): un ahorro menor que un sector no ahorra nada.
  4. L1806–L1807 Si eso deja menos de un sector, la única salida que vale la pena es la que cabe en un puntero de bloque: BPE_PAYLOAD_SIZE, 14 palabras de 8 bytes, 112 bytes.

Siete octavos de 128 KiB son 112 KiB, ya múltiplo de 512 bytes, de 4 KiB y de 8 KiB: con el recordsize por defecto, el ajuste no cambia nada. Importa en los bloques pequeños: con ashift 12, siete octavos de 8 KiB se ajustan a 4 KiB, y siete octavos de 4 KiB se ajustan a nada, de modo que un bloque de 4 KiB en sectores de 4 KiB solo se comprime si encoge hasta 112 bytes. La tabla de la figura muestra los mismos datos con los tres tamaños de sector.

Los cuatro resultados.

zio_write_compress Extracto de module/zfs/zio.c, líneas 1982–2017, OpenZFS zfs-2.4.4. Ver en GitHub
	/* If it's a compressed write that is not raw, compress the buffer. */
	if (compress != ZIO_COMPRESS_OFF &&
	    !(zio->io_flags & ZIO_FLAG_RAW_COMPRESS)) {
		abd_t *cabd = NULL;
		if (abd_cmp_zero(zio->io_abd, lsize) == 0)
			psize = 0;
		else if (compress == ZIO_COMPRESS_EMPTY)
			psize = lsize;
		else
			psize = zio_compress_data(compress, zio->io_abd, &cabd,
			    lsize,
			    zio_get_compression_max_size(compress,
			    spa->spa_gcd_alloc, spa->spa_min_alloc, lsize),
			    zp->zp_complevel);
		if (psize == 0) {
			compress = ZIO_COMPRESS_OFF;
		} else if (psize >= lsize) {
			compress = ZIO_COMPRESS_OFF;
			if (cabd != NULL)
				abd_free(cabd);
		} else if (psize <= BPE_PAYLOAD_SIZE && !zp->zp_encrypt &&
		    zp->zp_level == 0 && !DMU_OT_HAS_FILL(zp->zp_type) &&
		    spa_feature_is_enabled(spa, SPA_FEATURE_EMBEDDED_DATA)) {
			void *cbuf = abd_borrow_buf_copy(cabd, lsize);
			encode_embedded_bp_compressed(bp,
			    cbuf, compress, lsize, psize);
			BPE_SET_ETYPE(bp, BP_EMBEDDED_TYPE_DATA);
			BP_SET_TYPE(bp, zio->io_prop.zp_type);
			BP_SET_LEVEL(bp, zio->io_prop.zp_level);
			abd_return_buf(cabd, cbuf, lsize);
			abd_free(cabd);
			BP_SET_LOGICAL_BIRTH(bp, zio->io_txg);
			zio->io_pipeline = ZIO_INTERLOCK_PIPELINE;
			ASSERT(spa_feature_is_active(spa,
			    SPA_FEATURE_EMBEDDED_DATA));
			return (zio);
  1. L1983–L1984 Nada de esto se ejecuta con la compresión desactivada: un bloque de ceros se escribe entonces como cualquier otro, salvo si la deduplicación está activa, que activa la detección de ceros porque «storing many references to an all zeros block in the dedup table would be expensive» (guardar muchas referencias a un bloque de ceros en la tabla de deduplicación saldría caro).
  2. L1986–L1987 Solo ceros: PSIZE 0, que más abajo se convierte en un hueco.
  3. L1991–L1995 El compresor se ejecuta con el límite como tamaño de salida; zio_compress_data devuelve el tamaño de origen cuando la salida no cabe.
  4. L1996–L1997 Un hueco: la compresión se marca como desactivada, y PSIZE sigue en 0.
  5. L1998–L2001 Una salida que no ha ahorrado nada: la compresión queda desactivada para este bloque, que se almacena con su tamaño lógico.
  6. L2002–L2017 112 bytes como máximo, sin cifrar, de nivel 0, de un tipo sin recuento de bloques llenos, con embedded_data activado: los datos se codifican dentro del puntero de bloque, cuyas palabras contienen entonces carga útil, y la escritura se detiene ahí, sin asignación.
zio_write_compress Extracto de module/zfs/zio.c, líneas 2018–2039, OpenZFS zfs-2.4.4. Ver en GitHub
		} else {
			/*
			 * Round compressed size up to the minimum allocation
			 * size of the smallest-ashift device, and zero the
			 * tail. This ensures that the compressed size of the
			 * BP (and thus compressratio property) are correct,
			 * in that we charge for the padding used to fill out
			 * the last sector.
			 */
			size_t rounded = (size_t)zio_roundup_alloc_size(spa,
			    psize);
			if (rounded >= lsize) {
				compress = ZIO_COMPRESS_OFF;
				abd_free(cabd);
				psize = lsize;
			} else {
				abd_zero_off(cabd, psize, rounded - psize);
				psize = rounded;
				zio_push_transform(zio, cabd,
				    psize, lsize, NULL);
			}
		}
  1. L2027–L2028 zio_roundup_alloc_size redondea hacia arriba a la asignación mínima, o a un múltiplo del sector por encima de ella.
  2. L2029–L2032 Si el resultado no es menor que el bloque, este se almacena sin comprimir. El límite ajustado hace que sea raro; ZLE, sin ajuste, llega a este caso, y también una salida de 112 bytes de un bloque cifrado, que no puede integrarse.
  3. L2034–L2035 El final del último sector se pone a cero y PSIZE es el tamaño redondeado, para que «the compressed size of the BP (and thus compressratio property) are correct» (el tamaño comprimido del puntero de bloque, y por tanto la propiedad compressratio, sean correctos). Un hueco no asigna nada; si el bloque ya existía, conserva su tamaño, su tipo y su nivel, y nace en este txg (hole_birth), de modo que un envío incremental lo ve; los demás bloques reciben PSIZE, compresión, suma de comprobación y el bit D.

En un solo disco, el espacio asignado es PSIZE redondeado hacia arriba al sector: a un bloque se le cobran sectores enteros, comprimido o no, y por eso el límite se cuenta en sectores.

Deduplicación: la búsqueda. Un bloque con el bit D pasa por zio_ddt_write.

zio_ddt_write Extracto de module/zfs/zio.c, líneas 3816–3832, OpenZFS zfs-2.4.4. Ver en GitHub
	ddt_enter(ddt);
	/*
	 * Search DDT for matching entry.  Skip DVAs verification here, since
	 * they can go only from override, and once we get here the override
	 * pointer can't have "D" flag to be confused with pruned DDT entries.
	 */
	IMPLY(zio->io_bp_override, !BP_GET_DEDUP(zio->io_bp_override));
	dde = ddt_lookup(ddt, bp, B_FALSE);
	if (dde == NULL) {
		/* DDT size is over its quota so no new entries */
		ddt_exit(ddt);
		zp->zp_dedup = B_FALSE;
		BP_SET_DEDUP(bp, B_FALSE);
		if (zio->io_bp_override == NULL)
			zio->io_pipeline = ZIO_WRITE_PIPELINE;
		return (zio);
	}
  1. L3823 Cada escritura con deduplicación busca su puntero de bloque, bajo el cerrojo de la DDT.
  2. L3824–L3832 NULL: la tabla supera su cuota. Se borra el bit D y el bloque pasa por el pipeline de escritura ordinario, sin deduplicar.
ddt_lookup Extracto de module/zfs/ddt.c, líneas 1374–1387, OpenZFS zfs-2.4.4. Ver en GitHub
	boolean_t valid = B_TRUE;

	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(ddt, dde);
			return (NULL);
		}

		/* Flag cleanup required */
		dde->dde_flags |= DDE_FLAG_OVERQUOTA;
  1. L1376–L1384 NULL viene de aquí: la clave no está en ninguna tabla (un bloque nunca visto) y ddt_over_quota dice que la tabla es demasiado grande. Una clave que se encuentra se sigue devolviendo: por encima de la cuota, un duplicado se sigue deduplicando; solo se rechazan las entradas nuevas.

Un acierto. Una entrada con suficientes DVA para las copies del bloque:

zio_ddt_write Extracto de module/zfs/zio.c, líneas 3958–3987, OpenZFS zfs-2.4.4. Ver en GitHub
		/*
		 * If there are enough DVAs in the entry to service our request,
		 * then we can just use them as-is.
		 */
		if (have_dvas >= need_dvas) {
			if (dde_io != NULL)
				mutex_exit(&dde_io->dde_io_lock);

			/*
			 * For rewrite operations, try preserving the original
			 * logical birth time.  If the result matches the
			 * original BP, this becomes a NOP.
			 */
			if (zp->zp_rewrite) {
				uint64_t orig_logical_birth =
				    BP_GET_LOGICAL_BIRTH(&zio->io_bp_orig);
				ddt_bp_fill(ddp, v, bp, orig_logical_birth);
				if (BP_EQUAL(bp, &zio->io_bp_orig)) {
					/* We can skip accounting. */
					ddt_exit(ddt);
					zio->io_flags |= ZIO_FLAG_NOPWRITE;
					return (zio);
				}
			}

			ddt_bp_fill(ddp, v, bp, txg);
			ddt_phys_addref(ddp, v);
			ddt_exit(ddt);
			return (zio);
		}
  1. L3962 La entrada contiene tantos DVA como necesita la escritura.
  2. L3971–L3981 Una reescritura conserva su nacimiento original si eso da el mismo puntero de bloque: entonces no cambia absolutamente nada.
  3. L3983–L3986 ddt_bp_fill copia los DVA de la entrada en el nuevo puntero de bloque, con el txg actual como nacimiento lógico y el de la entrada como nacimiento físico; ddt_phys_addref añade una referencia; la función vuelve sin ninguna E/S de escritura.

Los dos nacimientos explican por qué la figura muestra «nacido en el txg 102, nacimiento físico 100»: los datos se escribieron en el txg 100, y el puntero de bloque se creó en el txg 102. La sala send / receive envía según el nacimiento lógico, de modo que un bloque deduplicado después de una instantánea se envía igualmente.

Un fallo. Una entrada nueva no tiene DVA: la rama del acierto no se toma, y se lanza una escritura hija con la entrada adjunta.

zio_ddt_write Extracto de module/zfs/zio.c, líneas 4156–4167, OpenZFS zfs-2.4.4. Ver en GitHub
	/*
	 * Optimistically increment the refcount for this dedup write.
	 * If the write fails, zio_ddt_child_write_done() will decrement
	 * for all non-DDT-child parents.
	 */
	ddt_phys_addref(ddp, v);

	ddt_exit(ddt);

	zio_nowait(cio);

	return (zio);
  1. L4161 El contador de referencias de la entrada nueva pasa de 0 a 1 ahora, antes de que termine la escritura; se deshace si la escritura falla.
  2. L4165 La escritura hija se ejecuta; cuando tiene su DVA, zio_ddt_child_write_ready lo copia en la entrada, para el próximo acierto.

La tabla en el disco. Las entradas modificadas en un txg tienen que llegar a los objetos de almacenamiento de la DDT.

DDT: el log de la DDT Extracto de module/zfs/ddt.c, líneas 136–156, OpenZFS zfs-2.4.4. Ver en GitHub
 * ## Dedup log
 *
 * Historically, all entries modified on a txg were written back to dedup
 * storage objects at the end of every txg. This could cause significant
 * overheads, as each entry only takes up a tiny portion of a ZAP leaf node,
 * and so required reading the whole node, updating the entry, and writing it
 * back. On busy pools, this could add serious IO and memory overheads.
 *
 * To address this, the dedup log was added. If the "fast_dedup" feature is
 * enabled, at the end of each txg, modified entries will be copied to an
 * in-memory "log" object (ddt_log_t), and appended to an on-disk log. If the
 * same block is requested again, the in-memory object will be checked first,
 * and if its there, the entry inflated back onto the live tree without going
 * to storage. The on-disk log is only read at pool import time, to reload the
 * in-memory log.
 *
 * Each txg, some amount of the in-memory log will be flushed out to a DDT
 * storage object (ie ZAP) as normal. OpenZFS will try hard to flush enough to
 * keep up with the rate of change on dedup entries, but not so much that it
 * would impact overall throughput, and not using too much memory. See the
 * zfs_dedup_log_* tunables in zfs(4) for more details.
  1. L138–L142 Sin el log, cada entrada modificada se reescribía al final de su txg, y una entrada es una parte diminuta de una hoja de ZAP: leer la hoja entera, actualizar una entrada, volver a escribir la hoja.
  2. L144–L150 Con fast_dedup, en una tabla creada con esa funcionalidad (solo esas reciben DDT_FLAG_LOG), las entradas modificadas van a un log en memoria y se añaden a un log en el disco, que solo se relee en la importación. Por defecto, los logs se intercambian como máximo cada 8 txg.
  3. L152–L156 Una parte del log se vuelca al ZAP en cada txg, tanta como permite seguir el ritmo sin perjudicar el rendimiento, según deciden los parámetros zfs_dedup_log_*, «and not using too much memory» (y sin usar demasiada memoria). El comentario no da ninguna cifra aquí.

Dos mecanismos más completan el cuadro, en el mismo comentario: la poda elimina entradas antiguas que nunca se duplicaron, para hacer sitio por debajo de la cuota, y el scrub recorre primero la DDT, de modo que un bloque referenciado muchas veces se verifica una sola vez.

El puntero de bloque cifrado.

blkptr_t, cifrado Extracto de include/sys/spa.h, líneas 188–225, OpenZFS zfs-2.4.4. Ver en GitHub
/*
 * The blkptr_t's of encrypted blocks also need to store the encryption
 * parameters so that the block can be decrypted. This layout is as follows:
 *
 *	64	56	48	40	32	24	16	8	0
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 0	|  pad  |	  vdev1         | pad   |	  ASIZE		|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 1	|G|			 offset1				|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 2	|  pad  |	  vdev2         | pad   |	  ASIZE		|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 3	|G|			 offset2				|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 4	|			salt					|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 5	|			IV1					|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 6	|BDX|lvl| type	| cksum |E| comp|    PSIZE	|     LSIZE	|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 7	|R|			padding					|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 8	|			padding					|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 9	|			physical birth txg			|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * a	|			logical birth txg			|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * b	|		IV2		|	    fill count		|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * c	|			checksum[0]				|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * d	|			checksum[1]				|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * e	|			MAC[0]					|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * f	|			MAC[1]					|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
  1. L194–L201 Los dos primeros DVA, como en cualquier puntero de bloque: un bloque cifrado conserva como máximo dos copias.
  2. L202–L205 Las dos palabras del tercer DVA contienen la sal e IV1, los primeros 64 bits del IV de 96 bits (zio_crypt_encode_params_bp).
  3. L216 IV2, los últimos 32 bits del IV, en la mitad alta del recuento de bloques llenos, que un bloque cifrado de nivel 0 no necesita.
  4. L218–L225 128 bits de suma de comprobación, y después el MAC de 128 bits (zio_crypt_encode_mac_bp).

El bit X, el bit 61 de la palabra 6, tiene tres significados: en un bloque de nivel 0 de un tipo cifrado significa cifrado; en un bloque de nivel 0 de un tipo no cifrado, autenticado; en un bloque indirecto, que las palabras de MAC contienen una suma de comprobación de los MAC de debajo. Un puntero de bloque integrado no puede cifrarse: no le quedan palabras para la sal, el IV y el MAC. El cifrado es AES-GCM o AES-CCM, con claves de 128, 192 o 256 bits, la autenticación SHA512-HMAC, con una sal de 8 bytes, un IV de 12 bytes y un MAC de 16 bytes.

Lo que recibe cada clase de bloque.

zio_encrypt Extracto de module/zfs/zio.c, líneas 5123–5153, OpenZFS zfs-2.4.4. Ver en GitHub
	/* indirect blocks only maintain a cksum of the lower level MACs */
	if (BP_GET_LEVEL(bp) > 0) {
		BP_SET_CRYPT(bp, B_TRUE);
		VERIFY0(zio_crypt_do_indirect_mac_checksum_abd(B_TRUE,
		    zio->io_orig_abd, BP_GET_LSIZE(bp), BP_SHOULD_BYTESWAP(bp),
		    mac));
		zio_crypt_encode_mac_bp(bp, mac);
		return (zio);
	}

	/*
	 * Objset blocks are a special case since they have 2 256-bit MACs
	 * embedded within them.
	 */
	if (ot == DMU_OT_OBJSET) {
		ASSERT0(DMU_OT_IS_ENCRYPTED(ot));
		ASSERT3U(BP_GET_COMPRESS(bp), ==, ZIO_COMPRESS_OFF);
		BP_SET_CRYPT(bp, B_TRUE);
		VERIFY0(spa_do_crypt_objset_mac_abd(B_TRUE, spa, dsobj,
		    zio->io_abd, psize, BP_SHOULD_BYTESWAP(bp)));
		return (zio);
	}

	/* unencrypted object types are only authenticated with a MAC */
	if (!DMU_OT_IS_ENCRYPTED(ot)) {
		BP_SET_CRYPT(bp, B_TRUE);
		VERIFY0(spa_do_crypt_mac_abd(B_TRUE, spa, dsobj,
		    zio->io_abd, psize, mac));
		zio_crypt_encode_mac_bp(bp, mac);
		return (zio);
	}
  1. L5123–L5131 Un bloque indirecto no se cifra: su puntero de bloque recibe, en las palabras de MAC, una suma de comprobación de los MAC de los bloques a los que apunta.
  2. L5137–L5144 El bloque del objset lleva sus dos MAC dentro de sí mismo.
  3. L5146–L5153 Un bloque de nivel 0 de un tipo no cifrado solo se autentica: un MAC, los datos en claro. Lo que queda se cifra, y la sal, el IV y el MAC se codifican en su puntero de bloque.
dmu_write_policy Extracto de module/zfs/dmu.c, líneas 2511–2535, OpenZFS zfs-2.4.4. Ver en GitHub
	/*
	 * All objects in an encrypted objset are protected from modification
	 * via a MAC. Encrypted objects store their IV and salt in the last DVA
	 * in the bp, so we cannot use all copies. Encrypted objects are also
	 * not subject to nopwrite since writing the same data will still
	 * result in a new ciphertext. Only encrypted blocks can be dedup'd
	 * to avoid ambiguity in the dedup code since the DDT does not store
	 * object types.
	 */
	if (os->os_encrypted && (wp & WP_NOFILL) == 0) {
		encrypt = B_TRUE;

		if (DMU_OT_IS_ENCRYPTED(type)) {
			copies = MIN(copies, SPA_DVAS_PER_BP - 1);
			gang_copies = MIN(gang_copies, SPA_DVAS_PER_BP - 1);
			nopwrite = B_FALSE;
		} else {
			dedup = B_FALSE;
		}

		if (level <= 0 &&
		    (type == DMU_OT_DNODE || type == DMU_OT_OBJSET)) {
			compress = ZIO_COMPRESS_EMPTY;
		}
	}
  1. L2523–L2526 En un dataset cifrado, un tipo cifrado conserva como máximo dos copias, ya que el tercer DVA contiene la sal y el IV.
  2. L2527–L2529 Los bloques de tipos no cifrados no se deduplican: la DDT no registra los tipos de objeto.

Lo que queda en claro, y por qué el scrub no necesita clave.

zio_crypt.c, comentario de cabecera Extracto de module/os/linux/zfs/zio_crypt.c, líneas 85–120, OpenZFS zfs-2.4.4. Ver en GitHub
 * Message Authentication Code (MAC)
 * The MAC is an output of authenticated encryption modes such as AES-GCM and
 * AES-CCM. Its purpose is to ensure that an attacker cannot modify encrypted
 * data on disk and return garbage to the application. Effectively, it is a
 * checksum that can not be reproduced by an attacker. We store the MAC in the
 * second 128 bits of blk_cksum, leaving the first 128 bits for a truncated
 * regular checksum of the ciphertext which can be used for scrubbing.
 *
 * OBJECT AUTHENTICATION:
 * Some object types, such as DMU_OT_MASTER_NODE cannot be encrypted because
 * they contain some info that always needs to be readable. To prevent this
 * data from being altered, we authenticate this data using SHA512-HMAC. This
 * will produce a MAC (similar to the one produced via encryption) which can
 * be used to verify the object was not modified. HMACs do not require key
 * rotation or IVs, so we can keep up to the full 3 copies of authenticated
 * data.
 *
 * ZIL ENCRYPTION:
 * ZIL blocks have their bp written to disk ahead of the associated data, so we
 * cannot store the MAC there as we normally do. For these blocks the MAC is
 * stored in the embedded checksum within the zil_chain_t header. The salt and
 * IV are generated for the block on bp allocation instead of at encryption
 * time. In addition, ZIL blocks have some pieces that must be left in plaintext
 * for claiming even though all of the sensitive user data still needs to be
 * encrypted. The function zio_crypt_init_uios_zil() handles parsing which
 * pieces of the block need to be encrypted. All data that is not encrypted is
 * authenticated using the AAD mechanisms that the supported encryption modes
 * provide for. In order to preserve the semantics of the ZIL for encrypted
 * datasets, the ZIL is not protected at the objset level as described below.
 *
 * DNODE ENCRYPTION:
 * Similarly to ZIL blocks, the core part of each dnode_phys_t needs to be left
 * in plaintext for scrubbing and claiming, but the bonus buffers might contain
 * sensitive user data. The function zio_crypt_init_uios_dnode() handles parsing
 * which pieces of the block need to be encrypted. For more details about
 * dnode authentication and encryption, see zio_crypt_init_uios_dnode().
  1. L89–L91 El MAC ocupa la segunda mitad de la suma de comprobación, «leaving the first 128 bits for a truncated regular checksum of the ciphertext which can be used for scrubbing» (lo que deja los primeros 128 bits para una suma de comprobación ordinaria truncada del texto cifrado, que puede usarse para el scrub).
  2. L94–L100 Los tipos de objeto que deben seguir siendo legibles, entre ellos el master node, solo se autentican, y pueden conservar hasta tres copias.
  3. L115–L120 El núcleo de cada dnode queda en texto en claro «for scrubbing and claiming» (para el scrub y la reclamación); solo se cifran los búferes bonus.
zio_checksum_error_impl Extracto de module/zfs/zio_checksum.c, líneas 508–526, OpenZFS zfs-2.4.4. Ver en GitHub
	/*
	 * MAC checksums are a special case since half of this checksum will
	 * actually be the encryption MAC. This will be verified by the
	 * decryption process, so we just check the truncated checksum now.
	 * Objset blocks use embedded MACs so we don't truncate the checksum
	 * for them.
	 */
	if (bp != NULL && BP_USES_CRYPT(bp) &&
	    BP_GET_TYPE(bp) != DMU_OT_OBJSET) {
		if (!(ci->ci_flags & ZCHECKSUM_FLAG_DEDUP)) {
			actual_cksum.zc_word[0] ^= actual_cksum.zc_word[2];
			actual_cksum.zc_word[1] ^= actual_cksum.zc_word[3];
		}

		actual_cksum.zc_word[2] = 0;
		actual_cksum.zc_word[3] = 0;
		expected_cksum.zc_word[2] = 0;
		expected_cksum.zc_word[3] = 0;
	}
  1. L515–L516 Un puntero de bloque con el bit X, salvo un bloque de objset.
  2. L517–L520 Para una suma de comprobación que no está hecha para la deduplicación (fletcher-4, por ejemplo), las palabras 2 y 3 se pliegan sobre la 0 y la 1, como hizo el escritor.
  3. L522–L525 Las palabras 2 y 3 se ignoran en ambos lados: el MAC queda a cargo del descifrado.

El scrub lee con ZIO_FLAG_RAW, que se salta el descifrado, así que esta comprobación es todo lo que hace: el scrub de la figura verifica cada bloque con la clave descargada, y detecta un bit invertido. Una lectura que tiene que descifrar busca la clave y falla con EACCES cuando no está cargada. La clave en sí es una clave maestra generada al azar, almacenada en el disco cifrada con la clave de envoltura del usuario, de modo que cambiar la frase de contraseña no vuelve a cifrar ningún dato; las instantáneas y los clones la comparten. Los bloques cifrados solo pueden deduplicarse si tienen el mismo texto cifrado: con la deduplicación, su sal y su IV proceden de un HMAC del texto en claro en lugar de ser aleatorios, y la deduplicación solo funciona dentro de una familia de clones.

Lo que las figuras dejan fuera

La salida del compresor es una entrada de la figura, no el cálculo de un compresor real: el código no da ninguna relación de compresión, y los ajustes predefinidos son ejemplos. LZ4 representa a todos los compresores salvo ZLE; todos siguen el mismo límite. No se muestran los niveles de compresión, ni la regla que deja de comprimir las reescrituras a partir de la pasada de sincronización 8. El pool es de discos simples de un solo ashift, donde la asignación mínima y el sector coinciden; no se tratan raidz ni los pools mixtos. La deduplicación escribe un bloque por txg con copies=1; la verificación de la deduplicación, las colisiones, las copias adicionales, la extensión de DVA parciales de fast dedup y el log de la DDT se cuentan, no se animan; los DVA son inventados. El texto cifrado, la sal, el IV y el MAC de la figura son sustitutos, no AES; la suma de comprobación es el fletcher-4 de los bytes de la figura. No se muestran la rotación de claves, que toma una sal nueva tras 400 000 000 bloques, el cifrado del ZIL ni los MAC del objset, como tampoco ninguna cifra de rendimiento, de memoria o de relación de compresión: el código no da ninguna.