Sala 3

Copia en escritura, sumas de comprobación y autorreparación

Los discos devuelven datos erróneos sin señalar ningún error. Un bit se degrada; una escritura cae en el lugar equivocado, o no llega a caer; un fallo del firmware responde con sectores obsoletos. Una suma de comprobación guardada junto a los datos no puede detectar una escritura perdida o desviada, porque el bloque obsoleto lleva una suma que coincide con él. Y un sistema de archivos que sobrescribe in situ los bloques vivos puede quedar, tras una caída, mitad antiguo y mitad nuevo.

ZFS responde a ambos problemas con una regla y una ubicación.

1 No sobrescribir nunca un bloque vivo

Cuando un bloque cambia, ZFS escribe la nueva versión en otro lugar y deja la antigua donde está. El puntero al bloque cambia, así que su padre cambia y también se escribe en otro lugar, y así sucesivamente hasta la raíz. El árbol antiguo sigue entero hasta que una sola escritura, el nuevo uberblock, hace pasar el pool de un árbol al otro. Reescribe un bloque paso a paso, y corta la corriente antes de la confirmación.

Versión en texto de esta figura
  1. El árbol tiene un uberblock en la cima, un bloque indirecto raíz R, dos bloques indirectos I0 e I1, y cuatro bloques de datos D0 a D3. Cada puntero de bloque contiene la dirección del hijo, su txg de nacimiento y su suma de comprobación.
  2. Reescribir un bloque de datos escribe una copia nueva en una dirección nueva; la copia antigua se queda donde está.
  3. Su padre necesita entonces un puntero nuevo (dirección nueva, suma nueva), así que el padre también se copia, y así sucesivamente hasta R.
  4. Escribir el nuevo uberblock en la ranura siguiente del anillo es la confirmación. Antes de ella, un corte de corriente deja el árbol antiguo intacto y vigente; después de ella, el árbol nuevo es el pool.
  5. Los bloques sustituidos se liberan, salvo que una instantánea siga haciendo referencia a ellos: entonces se quedan en la deadlist.

2 La suma de comprobación vive en el padre

Cada puntero de bloque, el blkptr_t de 128 bytes, lleva la suma de comprobación de 256 bits del bloque al que apunta. Un bloque nunca responde de sí mismo: responde su padre, y el padre del padre responde del padre, hasta el uberblock. Leer un bloque significa recalcular su suma y compararla con la del puntero que llevó hasta él. En un espejo, una discrepancia envía la lectura al otro disco, y la copia buena se escribe sobre la mala.

Punteros en el bloque padre: las sumas de comprobación esperadas

      Versión en texto de esta figura
      1. Un archivo de cuatro bloques, D0 a D3, está en un espejo de dos vías: el disco A y el disco B guardan cada uno una copia completa. El bloque padre contiene, en cada puntero de bloque, la suma fletcher4 del bloque al que apunta.
      2. Degradación silenciosa (bit rot): un bit de D1 se invierte en el disco A. El disco devuelve los bytes sin error. Recalcular fletcher4 sobre ellos da un valor distinto del que hay en el puntero: ECKSUM.
      3. Escritura perdida: el disco A acusó recibo de la escritura del nuevo D2 pero nunca la realizó. Con la copia en escritura, esa escritura iba a una nueva dirección, no sobre el antiguo D2, así que la dirección conserva lo que hubiera antes: bytes obsoletos de un bloque anterior, ya liberado. Esos bytes pueden ser coherentes consigo mismos; solo la suma guardada en el padre, que describe el nuevo D2, los delata.
      4. Ante una discrepancia, el espejo lee el otro disco. Si esa copia coincide, se devuelve a la aplicación y se escribe sobre la copia mala: el pool se repara a sí mismo en una lectura normal.
      5. Si las dos copias están dañadas, la lectura falla con un error en lugar de devolver datos erróneos. Cada discrepancia se cuenta en el disco que la devolvió.

      La idea, a tres profundidades

      En una frase

      ZFS nunca sobrescribe datos vivos y guarda la suma de comprobación de cada bloque en el puntero que lleva a él, de modo que una caída no puede dejar el pool a medio escribir y un disco no puede devolver datos erróneos sin que se note.

      En un párrafo

      Cada escritura va a un lugar nuevo: el bloque recibe una nueva dirección, lo que cambia el puntero de bloque de su padre, y por tanto el padre, hasta la raíz. Nada de lo que alcanza el uberblock actual se modifica jamás, así que en todo instante los discos contienen un árbol completo y coherente, y escribir un solo uberblock nuevo confirma el siguiente. Cada puntero de bloque lleva además la suma de comprobación de su hijo, lo que convierte el árbol en un árbol de Merkle: una única raíz de confianza responde de todos los bloques que tiene debajo. Una discrepancia en la lectura es un error detectado, nunca una corrupción silenciosa, y con una segunda copia (un espejo, o hasta tres direcciones en un mismo puntero de bloque) ZFS devuelve la copia buena y repara la mala.

      En detalle

      Dónde una escritura se convierte en un bloque nuevo. En el pipeline de escritura, zio_write_compress decide entre una reescritura in situ y una asignación nueva.

      zio_write_compress Extracto de module/zfs/zio.c, líneas 2086–2105, OpenZFS zfs-2.4.4. Ver en GitHub
      	/*
      	 * The final pass of spa_sync() must be all rewrites, but the first
      	 * few passes offer a trade-off: allocating blocks defers convergence,
      	 * but newly allocated blocks are sequential, so they can be written
      	 * to disk faster.  Therefore, we allow the first few passes of
      	 * spa_sync() to allocate new blocks, but force rewrites after that.
      	 * There should only be a handful of blocks after pass 1 in any case.
      	 */
      	if (!BP_IS_HOLE(bp) && BP_GET_BIRTH(bp) == zio->io_txg &&
      	    BP_GET_PSIZE(bp) == psize &&
      	    pass >= zfs_sync_pass_rewrite) {
      		VERIFY3U(psize, !=, 0);
      		enum zio_stage gang_stages = zio->io_pipeline & ZIO_GANG_STAGES;
      
      		zio->io_pipeline = ZIO_REWRITE_PIPELINE | gang_stages;
      		zio->io_flags |= ZIO_FLAG_IO_REWRITE;
      	} else {
      		BP_ZERO(bp);
      		zio->io_pipeline = ZIO_WRITE_PIPELINE;
      	}
      1. L2094–L2101 La reescritura in situ solo se permite para un bloque nacido en el mismo txg que se está sincronizando (BP_GET_BIRTH(bp) == zio->io_txg), sin cambio de tamaño, a partir de la pasada de sincronización zfs_sync_pass_rewrite (2). Ningún uberblock confirmado hace aún referencia a un bloque así, de modo que sobrescribirlo no toca ningún dato vivo. Es la única excepción a la copia en escritura para los bloques del árbol.
      2. L2102–L2104 Cualquier otra escritura pone a cero el puntero de bloque y recorre el pipeline de escritura completo, cuya etapa DVA_ALLOCATE da normalmente al bloque una nueva dirección: zio_dva_allocate comprueba que el puntero es un hueco antes de llamar a metaslab_alloc. Justo debajo, tres casos cambian el pipeline: un bloque que se comprime en nada se convierte en un hueco, la deduplicación toma el pipeline DDT, y el nopwrite añade su propia etapa.

      Cuando la escritura termina, dbuf_write_done llama a dsl_dataset_block_kill sobre el puntero antiguo y a dsl_dataset_block_born sobre el nuevo. El bloque antiguo solo se libera si nació después de la instantánea más reciente; si no, pasa a la deadlist del dataset, y la instantánea lo conserva.

      Dónde se verifica la suma. Al escribir, zio_checksum_compute guarda el valor calculado en bp->blk_cksum. Al leer, el bloque se compara con ese campo.

      zio_checksum_error_impl Extracto de module/zfs/zio_checksum.c, líneas 501–538, OpenZFS zfs-2.4.4. Ver en GitHub
      	} else {
      		byteswap = BP_SHOULD_BYTESWAP(bp);
      		expected_cksum = bp->blk_cksum;
      		ci->ci_func[byteswap](abd, size,
      		    spa->spa_cksum_tmpls[checksum], &actual_cksum);
      	}
      
      	/*
      	 * 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;
      	}
      
      	if (info != NULL) {
      		info->zbc_checksum_name = ci->ci_name;
      		info->zbc_byteswapped = byteswap;
      		info->zbc_injected = 0;
      		info->zbc_has_cksum = 1;
      	}
      
      	if (!ZIO_CHECKSUM_EQUAL(actual_cksum, expected_cksum))
      		return (SET_ERROR(ECKSUM));
      
      	return (0);
      1. L502–L505 El valor esperado es bp->blk_cksum, tomado del puntero de bloque que siguió el lector; el valor real se recalcula sobre los bytes que se acaban de leer.
      2. L515–L526 Bloques cifrados que no son objsets: la mitad de los 256 bits es el MAC del cifrado, que el descifrado verifica, así que aquí solo se comparan los otros 128 bits.
      3. L535–L536 Una discrepancia devuelve ECKSUM. En un espejo, esto se ejecuta en la lectura de cada hijo, no en el padre: zio_vdev_child_io baja la etapa CHECKSUM_VERIFY al hijo. Cada error se cuenta en el vdev hoja.

      La cadena se detiene en el uberblock: ub_rootbp no tiene puntero padre. Cada ranura de uberblock está protegida en su lugar por una suma SHA-256 «label» integrada, cuyo verificador es el desplazamiento de la propia ranura en el disco. Las cabeceras gang y los bloques del ZIL (zilog, zilog2) también llevan sumas integradas, y checksum=off existe. El valor por defecto, checksum=on, significa fletcher4: cuatro sumas acumuladas de 64 bits sobre el bloque leído como palabras de 32 bits. Las figuras de arriba calculan esa suma de verdad.

      Dónde se repara el espejo. En vdev_mirror_io_done, si ningún hijo ha devuelto todavía datos buenos, vdev_mirror_child_select elige otro hijo y se emite una nueva lectura. Si no queda ninguno, la lectura falla con un error. La reparación exige al menos una copia buena, un pool en el que se pueda escribir, y además un error inesperado, un resilver (la resincronización de un disco) o un scrub que hace resilver (L883–L886). Entonces:

      vdev_mirror_io_done Extracto de module/zfs/vdev_mirror.c, líneas 900–934, OpenZFS zfs-2.4.4. Ver en GitHub
      			if (mc->mc_error == 0) {
      				vdev_ops_t *ops = mc->mc_vd->vdev_ops;
      
      				if (mc->mc_tried)
      					continue;
      				/*
      				 * We didn't try this child.  We need to
      				 * repair it if:
      				 * 1. it's a scrub (in which case we have
      				 * tried everything that was healthy)
      				 *  - or -
      				 * 2. it's an indirect or distributed spare
      				 * vdev (in which case it could point to any
      				 * other vdev, which might have a bad DTL)
      				 *  - or -
      				 * 3. the DTL indicates that this data is
      				 * missing from this vdev
      				 */
      				if (!(zio->io_flags & ZIO_FLAG_SCRUB) &&
      				    ops != &vdev_indirect_ops &&
      				    ops != &vdev_draid_spare_ops &&
      				    !vdev_dtl_contains(mc->mc_vd, DTL_PARTIAL,
      				    zio->io_txg, 1))
      					continue;
      				mc->mc_error = SET_ERROR(ESTALE);
      			}
      
      			zio_nowait(zio_vdev_child_io(zio, zio->io_bp,
      			    mc->mc_vd, mc->mc_offset,
      			    zio->io_abd, zio->io_size, ZIO_TYPE_WRITE,
      			    zio->io_priority == ZIO_PRIORITY_REBUILD ?
      			    ZIO_PRIORITY_REBUILD : ZIO_PRIORITY_ASYNC_WRITE,
      			    ZIO_FLAG_IO_REPAIR | (unexpected_errors ?
      			    ZIO_FLAG_SELF_HEAL : 0), NULL, NULL));
      		}
      1. L900–L904 Los hijos que devolvieron datos buenos nunca se reescriben: el comentario justo encima advierte que reescribir la única copia buena durante un corte de corriente podría perderla.
      2. L905–L924 Un hijo que no se ha probado solo se repara durante un scrub, en los vdevs indirectos o los repuestos distribuidos de dRAID, o cuando su DTL indica que ahí falta este txg.
      3. L927–L933 El búfer bueno zio->io_abd se escribe en cada hijo dañado como una escritura ZIO_FLAG_IO_REPAIR, marcada ZIO_FLAG_SELF_HEAL cuando la causa fue un error inesperado.

      Lo que las figuras dejan fuera

      Las figuras muestran un solo archivo, así que el árbol es corto: en un pool va desde el uberblock a través del MOS, el dataset y el dnode del objeto. Dejan fuera las pasadas de sincronización, los bloques gang, la deduplicación, el nopwrite, los MAC del cifrado y la elección, según la carga, del lado del espejo que se lee primero (aquí, siempre el disco A). El anillo de uberblocks tiene 32 ranuras, como en los discos con sectores de 4K. Las sumas son valores fletcher4 reales calculados sobre los bloques de demostración; las direcciones y los números de txg son inventados.