Sala 11

send / receive

Mantener una copia de un sistema de archivos en otra máquina parece sencillo: comparar los dos lados y copiar lo que difiere. Pero comparar significa leer. Para encontrar los pocos bloques que cambiaron desde ayer, una herramienta que trabaja archivo por archivo lee, o como mínimo enumera, todos los archivos de ambos lados, cada vez, y solo puede ver un cambio a través del contenido que compara o de las fechas y tamaños en los que confía.

ZFS no compara. Cada puntero de bloque guarda el txg en el que nació su bloque, y una instantánea tomada en el txg F no contiene ningún bloque nacido después de F. Así que la diferencia entre dos instantáneas son los bloques de la más reciente nacidos después del txg de la más antigua. La copia en escritura reescribe todos los bloques por encima de uno modificado, así que un subárbol con una raíz antigua no ha cambiado por debajo: una sola comparación en su cima lo salta entero, sin leerlo. Lo que encuentra el recorrido se convierte en un flujo de registros, cada uno salvo el primero con una suma de comprobación de todo lo anterior, que el lado receptor comprueba registro a registro, antes de aplicar cada uno.

1 Enviar @s4, desde la instantánea que elijas

tank/fs tiene cuatro instantáneas, una por cada txg de su historia. La figura envía la última, @s4: su árbol, cada puntero de bloque con su txg de nacimiento lógico. Elige la instantánea de origen que el receptor ya tiene, o ninguna para un flujo completo, y sigue el envío paso a paso: en cada puntero de bloque se compara el nacimiento con F, el txg de la instantánea de origen, los registros caen en el flujo con sus sumas de comprobación, y el receptor los comprueba y los aplica hasta que sus archivos son los de @s4. «Invertir un bit» daña el flujo por el camino.

Flujo incremental de @s2 a @s4 (F = 20): visitados 12 punteros de bloque de 19, 15 registros, 6728 bytes.

Historia de tank/fs
  1. txg 10 crea los archivos a, b y c (objetos 1 a 3), de cuatro bloques de 512 bytes cada uno; instantánea @s1
  2. txg 20 reescribe el bloque 2 de a; instantánea @s2
  3. txg 30 abre un hueco en los bloques 1 y 2 de b, crea d (objeto 4); instantánea @s3
  4. txg 40 elimina c; instantánea @s4

Lado emisor: el árbol de @s4

12 punteros de bloque visitados3 omitidos4 por debajo, nunca leídos15 registros6728 bytes

  • objset40> 20
    • L1 del meta-dnode40> 20
      • bloque de dnodes 040> 200meta-dnode1a2b3libre
        • a: L120≤ 20a010a110a2′20a310
        • b: L130> 20b010≤ 20hueco30> 20hueco30> 20b310≤ 20
      • bloque de dnodes 130> 204d5libre6libre7libre
        • d: L130> 20d030> 20d130> 20d230> 20d330> 20

El flujo

  1. 0BEGIN@s2 → @s4 (comprobado y aplicado)sin suma
  2. 1OBJECTobjeto 1, a (comprobado y aplicado)50788f0f
  3. 2FREEa 2048–final (comprobado y aplicado)43af0187
  4. 3OBJECTobjeto 2, b (comprobado y aplicado)abe05a34
  5. 4FREEb 2048–final (comprobado y aplicado)e8b7bb43
  6. 5FREEb 512–1536 (comprobado y aplicado)8c224c03
  7. 6FREEOBJECTSobjeto 3 (comprobado y aplicado)356ecbe7
  8. 7OBJECTobjeto 4, d (comprobado y aplicado)21559e25
  9. 8FREEd 2048–final (comprobado y aplicado)7331f09e
  10. 9WRITEd @0 «d0» (comprobado y aplicado)753d4dba
  11. 10WRITEd @512 «d1» (comprobado y aplicado)c439bd12
  12. 11WRITEd @1024 «d2» (comprobado y aplicado)a695a488
  13. 12WRITEd @1536 «d3» (comprobado y aplicado)cc0bcf92
  14. 13FREEOBJECTSobjetos 5–7 (comprobado y aplicado)d3567f4c
  15. 14ENDsuma 58c87268 (comprobado y aplicado)b31db7bb

Lado receptor

El receptor tiene @s2 (txg 20).

  • aobjeto 1a0a1a2′a3
  • bobjeto 2b0huecohuecob3
  • dobjeto 4d0d1d2d3

END comprobado (58c87268). Los archivos recibidos son los de @s4.

Envío de @s4: los punteros de bloque visitados disminuyen a medida que F crece
DesdeFVisitadosOmitidos + debajoRegistrosBytes
ninguna (completo)019 / 190 + 02111 672
@s11014 / 195 + 0167552
@s22012 / 193 + 4156728
@s3303 / 193 + 1372184
Versión en texto de esta figura

Flujo incremental de @s2 a @s4 (F = 20): visitados 12 punteros de bloque de 19, 15 registros, 6728 bytes.

El recorrido

  1. objset, nacido en el txg 40: 40 ≤ 20 es falso, visitado. send_cb no produce ningún registro; el bloque se lee para llegar a lo que hay por debajo.
  2. L1 del meta-dnode, nacido en el txg 40: 40 ≤ 20 es falso, visitado. send_cb no produce ningún registro; el bloque se lee para llegar a lo que hay por debajo.
  3. bloque de dnodes 0, nacido en el txg 40: 40 ≤ 20 es falso, visitado. send_cb produce un OBJECT_RANGE, que solo se escribe en un flujo raw.
  4. Dnode 0, el número del meta-dnode: send_cb no produce nada.
  5. Dnode 1, archivo a: send_cb produce OBJECT; dump_dnode lo escribe, y luego un FREE desde el final del archivo. Escritos: OBJECT 1 (a), FREE a desde 2048 hasta el final.
  6. a: L1, nacido en el txg 20: 20 ≤ 20, omitido. Nunca leído, ni lo que hay por debajo (4 punteros de bloque).
  7. Dnode 2, archivo b: send_cb produce OBJECT; dump_dnode lo escribe, y luego un FREE desde el final del archivo. Escritos: OBJECT 2 (b), FREE b desde 2048 hasta el final.
  8. b: L1, nacido en el txg 30: 30 ≤ 20 es falso, visitado. send_cb no produce ningún registro; el bloque se lee para llegar a lo que hay por debajo.
  9. b, bloque 0, nacido en el txg 10: 10 ≤ 20, omitido. Nunca leído.
  10. b, bloque 1 (hueco), nacido en el txg 30: 30 ≤ 20 es falso, visitado. send_cb produce HOLE: un FREE de su intervalo. Retenido en espera: dump_free y dump_freeobjects fusionan en él lo que sigue.
  11. b, bloque 2 (hueco), nacido en el txg 30: 30 ≤ 20 es falso, visitado. send_cb produce HOLE: un FREE de su intervalo. Retenido en espera: dump_free y dump_freeobjects fusionan en él lo que sigue.
  12. b, bloque 3, nacido en el txg 10: 10 ≤ 20, omitido. Nunca leído.
  13. Dnode 3, libre: send_cb produce OBJECT; dump_dnode convierte un dnode vacío en FREEOBJECTS. Escrito: FREE b desde 512, 1024 bytes.
  14. bloque de dnodes 1, nacido en el txg 30: 30 ≤ 20 es falso, visitado. send_cb produce un OBJECT_RANGE, que solo se escribe en un flujo raw.
  15. Dnode 4, archivo d: send_cb produce OBJECT; dump_dnode lo escribe, y luego un FREE desde el final del archivo. Escritos: FREEOBJECTS 3, OBJECT 4 (d), FREE d desde 2048 hasta el final.
  16. d: L1, nacido en el txg 30: 30 ≤ 20 es falso, visitado. send_cb no produce ningún registro; el bloque se lee para llegar a lo que hay por debajo.
  17. d, bloque 0, nacido en el txg 30: 30 ≤ 20 es falso, visitado. send_cb produce DATA: un WRITE con los 512 bytes del bloque. Escrito: WRITE d en 0 (d0).
  18. d, bloque 1, nacido en el txg 30: 30 ≤ 20 es falso, visitado. send_cb produce DATA: un WRITE con los 512 bytes del bloque. Escrito: WRITE d en 512 (d1).
  19. d, bloque 2, nacido en el txg 30: 30 ≤ 20 es falso, visitado. send_cb produce DATA: un WRITE con los 512 bytes del bloque. Escrito: WRITE d en 1024 (d2).
  20. d, bloque 3, nacido en el txg 30: 30 ≤ 20 es falso, visitado. send_cb produce DATA: un WRITE con los 512 bytes del bloque. Escrito: WRITE d en 1536 (d3).
  21. Dnode 5, libre: send_cb produce OBJECT; dump_dnode convierte un dnode vacío en FREEOBJECTS. Retenido en espera: dump_free y dump_freeobjects fusionan en él lo que sigue.
  22. Dnode 6, libre: send_cb produce OBJECT; dump_dnode convierte un dnode vacío en FREEOBJECTS. Retenido en espera: dump_free y dump_freeobjects fusionan en él lo que sigue.
  23. Dnode 7, libre: send_cb produce OBJECT; dump_dnode convierte un dnode vacío en FREEOBJECTS. Retenido en espera: dump_free y dump_freeobjects fusionan en él lo que sigue.

Los registros

  1. BEGIN
  2. OBJECT 1 (a), drr_checksum 50788f0f
  3. FREE a desde 2048 hasta el final, drr_checksum 43af0187
  4. OBJECT 2 (b), drr_checksum abe05a34
  5. FREE b desde 2048 hasta el final, drr_checksum e8b7bb43
  6. FREE b desde 512, 1024 bytes, drr_checksum 8c224c03
  7. FREEOBJECTS 3, drr_checksum 356ecbe7
  8. OBJECT 4 (d), drr_checksum 21559e25
  9. FREE d desde 2048 hasta el final, drr_checksum 7331f09e
  10. WRITE d en 0 (d0), drr_checksum 753d4dba
  11. WRITE d en 512 (d1), drr_checksum c439bd12
  12. WRITE d en 1024 (d2), drr_checksum a695a488
  13. WRITE d en 1536 (d3), drr_checksum cc0bcf92
  14. FREEOBJECTS 5 a 7, drr_checksum d3567f4c
  15. END, drr_checksum b31db7bb

END comprobado (58c87268). Los archivos recibidos son los de @s4.

La idea, a tres profundidades

En una frase

Un envío incremental recorre el árbol de la instantánea más reciente y se salta cada puntero de bloque nacido en el txg de la instantánea más antigua o antes, con todo lo que tiene por debajo, y escribe lo que queda como un flujo de registros cuya suma de comprobación acumulada verifica el receptor antes de reproducirlos.

En un párrafo

zfs send -i @from @to recorre el árbol de @to con una cota inferior, F, el txg en el que se creó @from. Un puntero de bloque nacido en F o antes ya estaba en @from, y también todo el subárbol que tiene debajo, ya que la copia en escritura da un nacimiento nuevo a todos los bloques por encima de un cambio: el recorrido vuelve sin leerlo. Todo lo que sí visita se convierte en registros: un OBJECT por cada dnode que encuentra, un WRITE por cada bloque de datos, un FREE por cada hueco, FREEOBJECTS por cada ranura de dnode vacía. El flujo empieza con BEGIN, que nombra las dos instantáneas por su GUID, y termina con END. Cada registro salvo BEGIN lleva la suma fletcher-4 de todos los bytes del flujo hasta esa suma; el receptor lleva la misma suma y rechaza el flujo en la primera cabecera que no coincide, antes de aplicar el registro al que sigue. Sin instantánea de origen, F vale 0, se visitan todos los bloques, y el flujo reconstruye el sistema de archivos entero.

En detalle

Un flujo de registros. Un flujo de send es una secuencia de dmu_replay_record_t, cada uno una cabecera de 312 bytes con uno de once tipos, a veces seguida de una carga útil: los datos de un WRITE siguen a su cabecera. BEGIN lleva el número mágico 0x2F5bacbacULL, y el GUID de la instantánea enviada y el de aquella desde la que parte.

La cota. El recorrido que alimenta un envío empieza aquí.

send_traverse_thread Extracto de module/zfs/dmu_send.c, líneas 1203–1221, OpenZFS zfs-2.4.4. Ver en GitHub
static __attribute__((noreturn)) void
send_traverse_thread(void *arg)
{
	struct send_thread_arg *st_arg = arg;
	int err = 0;
	struct send_range *data;
	fstrans_cookie_t cookie = spl_fstrans_mark();

	err = traverse_dataset_resume(st_arg->os->os_dsl_dataset,
	    st_arg->fromtxg, &st_arg->resume,
	    st_arg->flags | TRAVERSE_LOGICAL, send_cb, st_arg);

	if (err != EINTR)
		st_arg->error_code = err;
	data = range_alloc(DATA, 0, 0, 0, B_TRUE);
	bqueue_enqueue_flush(&st_arg->q, data, sizeof (*data));
	spl_fstrans_unmark(cookie);
	thread_exit();
}
  1. L1211–L1213 El dataset se recorre desde fromtxg, el txg de creación de la instantánea de origen, 0 para un flujo completo. TRAVERSE_LOGICAL se suma a los indicadores del envío, entre ellos TRAVERSE_PRE: la función de retorno de un bloque se ejecuta antes que sus hijos.
  2. L1217–L1218 Tras el último intervalo va una marca de fin de flujo. Los intervalos que send_cb pone en cola llegan al hilo principal a través de más colas, en orden, y allí se convierten en registros.

La prueba que salta un subárbol. Todos los punteros de bloque de la instantánea pasan por aquí.

traverse_visitbp Extracto de module/zfs/dmu_traverse.c, líneas 249–279, OpenZFS zfs-2.4.4. Ver en GitHub
	if (BP_GET_LOGICAL_BIRTH(bp) == 0) {
		/*
		 * Since this block has a birth time of 0 it must be one of
		 * two things: a hole created before the
		 * SPA_FEATURE_HOLE_BIRTH feature was enabled, or a hole
		 * which has always been a hole in an object.
		 *
		 * If a file is written sparsely, then the unwritten parts of
		 * the file were "always holes" -- that is, they have been
		 * holes since this object was allocated.  However, we (and
		 * our callers) can not necessarily tell when an object was
		 * allocated.  Therefore, if it's possible that this object
		 * was freed and then its object number reused, we need to
		 * visit all the holes with birth==0.
		 *
		 * If it isn't possible that the object number was reused,
		 * then if SPA_FEATURE_HOLE_BIRTH was enabled before we wrote
		 * all the blocks we will visit as part of this traversal,
		 * then this hole must have always existed, so we can skip
		 * it.  We visit blocks born after (exclusive) td_min_txg.
		 *
		 * Note that the meta-dnode cannot be reallocated.
		 */
		if (!send_holes_without_birth_time &&
		    (!td->td_realloc_possible ||
		    zb->zb_object == DMU_META_DNODE_OBJECT) &&
		    td->td_hole_birth_enabled_txg <= td->td_min_txg)
			return (0);
	} else if (get_birth_time(td, bp) <= td->td_min_txg) {
		return (0);
	}
  1. L249–L276 Un hueco con fecha de nacimiento 0 nunca se escribió, o se creó antes de la funcionalidad hole_birth. Estas líneas lo saltarían en algunos casos, pero solo cuando send_holes_without_birth_time vale 0, y vale 1 por defecto: esos huecos se visitan. La figura no tiene ninguno; un hueco abierto en b lleva el txg de la liberación, y pasa por la prueba de abajo.
  2. L277–L278 La regla. td_min_txg es la cota, F. Si nació en F o antes, el bloque está en la instantánea de origen: la función vuelve antes de leerlo o de llamar a la función de retorno, así que no se alcanza nada de lo que tiene debajo. En la figura la ficha pasa a borde discontinuo y su subárbol se atenúa.

El nacimiento lógico. get_birth_time devuelve el nacimiento lógico cuando TRAVERSE_LOGICAL está activado, como en un envío. Un puntero de bloque tiene dos nacimientos, y el comentario que los define dice que el físico puede ser menor que el lógico con la deduplicación y la clonación de bloques, y mayor con los bloques reasignados y reescritos, y que el lógico es el que sirve para la replicación incremental. Send compara el lógico, el txg en el que el usuario cambió los datos: un bloque deduplicado o clonado sobre una copia más antigua se envía igual, ya que el usuario lo escribió después de F, y un bloque que solo se reescribió o se reasignó en disco no se vuelve a enviar.

Cada dnode de un bloque de dnodes. Cuando se visita un bloque de dnodes, cada uno de sus dnodes se entrega a esta función.

traverse_dnode Extracto de module/zfs/dmu_traverse.c, líneas 534–550, OpenZFS zfs-2.4.4. Ver en GitHub
	if (td->td_flags & TRAVERSE_PRE) {
		SET_BOOKMARK(&czb, objset, object, ZB_DNODE_LEVEL,
		    ZB_DNODE_BLKID);
		err = td->td_func(td->td_spa, NULL, bp, &czb, dnp,
		    td->td_arg);
		if (err == TRAVERSE_VISIT_NO_CHILDREN)
			return (0);
		if (err != 0)
			return (err);
	}

	for (j = 0; j < dnp->dn_nblkptr; j++) {
		SET_BOOKMARK(&czb, objset, object, dnp->dn_nlevels - 1, j);
		err = traverse_visitbp(td, dnp, &dnp->dn_blkptr[j], &czb);
		if (err != 0)
			break;
	}
  1. L534–L543 La función de retorno se ejecuta para el propio dnode. Aquí no se prueba ningún nacimiento: todos los dnodes de un bloque de dnodes visitado llegan a send_cb, hayan cambiado o no.
  2. L545–L550 Después cada uno de los punteros de bloque del dnode pasa por traverse_visitbp, y por su prueba.

Por eso, al enviar @s4 desde @s3, el flujo contiene registros OBJECT para a y b, cuyos datos no cambiaron: borrar c reescribió el bloque de dnodes que comparten con él. Sus bloques de datos se siguen saltando, un nivel más abajo.

De los bloques visitados a los registros. La función de retorno decide lo que produce cada visita.

send_cb Extracto de module/zfs/dmu_send.c, líneas 1098–1120, OpenZFS zfs-2.4.4. Ver en GitHub
	if (zb->zb_level == ZB_DNODE_LEVEL) {
		if (zb->zb_object == DMU_META_DNODE_OBJECT)
			return (0);
		record = range_alloc(OBJECT, zb->zb_object, 0, 0, B_FALSE);
		record->sru.object.bp = *bp;
		size_t size  = sizeof (*dnp) * (dnp->dn_extra_slots + 1);
		record->sru.object.dnp = kmem_alloc(size, KM_SLEEP);
		memcpy(record->sru.object.dnp, dnp, size);
		bqueue_enqueue(&sta->q, record, sizeof (*record));
		return (0);
	}
	if (zb->zb_level == 0 && zb->zb_object == DMU_META_DNODE_OBJECT &&
	    !BP_IS_HOLE(bp)) {
		record = range_alloc(OBJECT_RANGE, 0, zb->zb_blkid,
		    zb->zb_blkid + 1, B_FALSE);
		record->sru.object_range.bp = *bp;
		bqueue_enqueue(&sta->q, record, sizeof (*record));
		return (0);
	}
	if (zb->zb_level < 0 || (zb->zb_level > 0 && !BP_IS_HOLE(bp)))
		return (0);
	if (zb->zb_object == DMU_META_DNODE_OBJECT && !BP_IS_HOLE(bp))
		return (0);
  1. L1098–L1108 Un dnode se convierte en un intervalo OBJECT, salvo el meta-dnode, el objeto 0.
  2. L1109–L1116 Un bloque de dnodes se convierte en un OBJECT_RANGE, que solo escribe un flujo raw.
  3. L1117–L1120 El bloque del objset y cada bloque indirecto que no es un hueco no producen ningún registro: solo se leen para llegar a lo que tienen debajo.
send_cb Extracto de module/zfs/dmu_send.c, líneas 1130–1146, OpenZFS zfs-2.4.4. Ver en GitHub
	if (!overflow_multiply(span, zb->zb_blkid, &start) || (!(zb->zb_blkid ==
	    DMU_SPILL_BLKID || DMU_OT_IS_METADATA(dnp->dn_type)) &&
	    span * zb->zb_blkid > dnp->dn_maxblkid)) {
		ASSERT(BP_IS_HOLE(bp));
		return (0);
	}

	if (zb->zb_blkid == DMU_SPILL_BLKID)
		ASSERT3U(BP_GET_TYPE(bp), ==, DMU_OT_SA);

	enum type record_type = DATA;
	if (BP_IS_HOLE(bp))
		record_type = HOLE;
	else if (BP_IS_REDACTED(bp))
		record_type = REDACT;
	else
		record_type = DATA;
  1. L1130–L1135 Nada más allá del final del objeto.
  2. L1140–L1146 Lo que queda es un bloque de datos (un WRITE) o un hueco (un FREE de su intervalo; un hueco entre los bloques de dnodes libera objetos). REDACT pertenece a los envíos redactados.

Un intervalo OBJECT va a dump_dnode. Una ranura de dnode sin ningún objeto se convierte en FREEOBJECTS; un archivo se convierte en OBJECT, con su búfer bonus como carga útil, seguido de inmediato por un FREE desde el final del archivo hasta DMU_OBJECT_END, para que un receptor cuya copia era más larga la recorte. dump_free mantiene un FREE pendiente y fusiona en él el siguiente cuando lo continúa, y por eso los dos huecos de b viajan como un solo FREE; un FREE hasta el final se escribe de inmediato, y cualquier otro registro escribe primero el pendiente. Los FREEOBJECTS se fusionan del mismo modo, y los objetos más allá del último bloque de dnodes se descartan. Un flujo completo también lleva liberaciones, para poder recibirse como un clon.

La suma de comprobación que recorre el flujo.

dump_record Extracto de module/zfs/dmu_send.c, líneas 257–283, OpenZFS zfs-2.4.4. Ver en GitHub
/*
 * For all record types except BEGIN, fill in the checksum (overlaid in
 * drr_u.drr_checksum.drr_checksum).  The checksum verifies everything
 * up to the start of the checksum itself.
 */
static int
dump_record(dmu_send_cookie_t *dscp, void *payload, int payload_len)
{
	dmu_send_outparams_t *dso = dscp->dsc_dso;
	ASSERT3U(offsetof(dmu_replay_record_t, drr_u.drr_checksum.drr_checksum),
	    ==, sizeof (dmu_replay_record_t) - sizeof (zio_cksum_t));
	(void) fletcher_4_incremental_native(dscp->dsc_drr,
	    offsetof(dmu_replay_record_t, drr_u.drr_checksum.drr_checksum),
	    &dscp->dsc_zc);
	if (dscp->dsc_drr->drr_type == DRR_BEGIN) {
		dscp->dsc_sent_begin = B_TRUE;
	} else {
		ASSERT(ZIO_CHECKSUM_IS_ZERO(&dscp->dsc_drr->drr_u.
		    drr_checksum.drr_checksum));
		dscp->dsc_drr->drr_u.drr_checksum.drr_checksum = dscp->dsc_zc;
	}
	if (dscp->dsc_drr->drr_type == DRR_END) {
		dscp->dsc_sent_end = B_TRUE;
	}
	(void) fletcher_4_incremental_native(&dscp->dsc_drr->
	    drr_u.drr_checksum.drr_checksum,
	    sizeof (zio_cksum_t), &dscp->dsc_zc);
  1. L268–L270 La suma fletcher-4 acumulada continúa sobre el registro, hasta sus últimos 32 bytes.
  2. L271–L277 Salvo para BEGIN, ese valor va en esos 32 bytes, drr_checksum, superpuestos a todos los tipos de registro.
  3. L281–L283 Después se suman los 32 bytes, y la carga útil que los sigue. Así, el drr_checksum de cada registro es la fletcher-4 de todos los bytes del flujo anteriores a él, incluida su propia cabecera hasta la suma. La figura muestra los 32 bits bajos de su última palabra.

END lleva una más: drr_end.drr_checksum, la suma de comprobación de todo lo anterior.

El lado receptor comprueba antes de aplicar.

receive_read_payload_and_next_header Extracto de module/zfs/dmu_recv.c, líneas 2843–2868, OpenZFS zfs-2.4.4. Ver en GitHub
	/*
	 * Note: checksum is of everything up to but not including the
	 * checksum itself.
	 */
	ASSERT3U(offsetof(dmu_replay_record_t, drr_u.drr_checksum.drr_checksum),
	    ==, sizeof (dmu_replay_record_t) - sizeof (zio_cksum_t));
	receive_cksum(drc,
	    offsetof(dmu_replay_record_t, drr_u.drr_checksum.drr_checksum),
	    &drc->drc_next_rrd->header);

	zio_cksum_t cksum_orig =
	    drc->drc_next_rrd->header.drr_u.drr_checksum.drr_checksum;
	zio_cksum_t *cksump =
	    &drc->drc_next_rrd->header.drr_u.drr_checksum.drr_checksum;

	if (drc->drc_byteswap)
		byteswap_record(&drc->drc_next_rrd->header);

	if ((!ZIO_CHECKSUM_IS_ZERO(cksump)) &&
	    !ZIO_CHECKSUM_EQUAL(drc->drc_cksum, *cksump)) {
		kmem_free(drc->drc_next_rrd, sizeof (*drc->drc_next_rrd));
		drc->drc_next_rrd = NULL;
		return (SET_ERROR(ECKSUM));
	}

	receive_cksum(drc, sizeof (cksum_orig), &cksum_orig);
  1. L2843–L2851 El receptor lleva su propia suma: empieza sobre todo el registro BEGIN, continúa sobre cada carga útil, y aquí sobre la cabecera siguiente, hasta su suma.
  2. L2861–L2866 Una cabecera cuyo drr_checksum no coincide detiene la recepción: ECKSUM. Una suma de comprobación formada solo por ceros no se comprueba.
  3. L2868 Después se suma el valor almacenado, como hizo el emisor.

Esto se ejecuta cuando se han leído la carga útil de un registro y la cabecera que lo sigue, y solo entonces el registro se pone en la cola del escritor: un registro cuyos bytes se dañaron nunca se aplica. END se comprueba una vez más, su drr_end.drr_checksum contra la suma anterior a él. Ante un error, lo que la recepción creó se destruye: el receptor sigue teniendo la instantánea de origen. Antes de todo esto, el BEGIN de un flujo incremental nombra la instantánea de origen por su GUID, y el receptor lo rechaza (ENODEV) a menos que tenga una instantánea con ese GUID.

Registros despachados por tipo. El hilo escritor saca cada registro de la cola y lo entrega aquí.

receive_process_record Extracto de module/zfs/dmu_recv.c, líneas 3188–3213, OpenZFS zfs-2.4.4. Ver en GitHub
	switch (rrd->header.drr_type) {
	case DRR_OBJECT:
	{
		struct drr_object *drro = &rrd->header.drr_u.drr_object;
		err = receive_object(rwa, drro, rrd->payload);
		vmem_free(rrd->payload, rrd->payload_size);
		rrd->payload = NULL;
		break;
	}
	case DRR_FREEOBJECTS:
	{
		struct drr_freeobjects *drrfo =
		    &rrd->header.drr_u.drr_freeobjects;
		err = receive_freeobjects(rwa, drrfo);
		break;
	}
	case DRR_WRITE:
	{
		err = receive_process_write_record(rwa, rrd);
		if (rwa->heal) {
			/*
			 * If healing - always free the abd after processing
			 */
			abd_free(rrd->abd);
			rrd->abd = NULL;
		} else if (err != EAGAIN) {
  1. L3189–L3196 OBJECT va a receive_object, que crea el objeto, o conserva uno que ya existe.
  2. L3197–L3203 FREEOBJECTS va a receive_freeobjects, que libera cada objeto del intervalo que existe: c desaparece.
  3. L3204–L3206 WRITE va a receive_process_write_record. FREE, más abajo, va a receive_free, que libera el intervalo en el objeto.

Con qué termina el receptor. Partiendo de los archivos de la instantánea de origen, los registros del flujo devuelven exactamente @s4: la figura compara los dos al final, para cada instantánea de origen, y sus pruebas lo contrastan con los registros calculados a mano de cada flujo.

Lo que la figura deja fuera

La figura es pequeña: archivos de cuatro bloques de 512 bytes bajo un solo bloque indirecto, bloques de dnodes de cuatro dnodes (un bloque de dnodes real de 16 KiB contiene 32), un meta-dnode dibujado con sus dos bloques de dnodes, sin búfer bonus. El envío se ejecuta como una sola secuencia, mientras que el código une varios hilos (recorrido, fusión, lector, el hilo principal) mediante colas que conservan el orden, y el receptor empieza una vez completo el flujo, mientras que el código recibe a través de una tubería a medida que se escribe el flujo. Las cabeceras de los registros siguen la estructura de dmu_replay_record_t, con solo los campos que usa la figura rellenados. No se muestran los flujos redactados, raw (cifrados), comprimidos y reanudables, los datos integrados, WRITE_BYREF ni los bloques spill; tampoco cifras de rendimiento, que dependen de los discos y de la red, no de este código.