Salle 11

send / receive

Garder une copie d’un système de fichiers sur une autre machine semble simple : comparer les deux côtés et copier ce qui diffère. Mais comparer, c’est lire. Pour trouver les quelques blocs modifiés depuis la veille, un outil qui procède fichier par fichier lit, ou au moins liste, chaque fichier des deux côtés, à chaque fois, et ne voit un changement qu’à travers les contenus qu’il compare ou les dates et tailles auxquelles il se fie.

ZFS ne compare pas. Chaque pointeur de bloc garde le txg où son bloc est né, et un instantané pris au txg F ne contient aucun bloc né après F. La différence entre deux instantanés, ce sont donc les blocs du plus récent nés après le txg du plus ancien. La copie sur écriture réécrit chaque bloc au-dessus d’un bloc modifié : un sous-arbre dont la racine est ancienne n’a pas changé en dessous, et une seule comparaison à son sommet le saute en entier, sans le lire. Ce que le parcours trouve devient un flux d’enregistrements, chacun, sauf le premier, portant une somme de contrôle de tout ce qui le précède, que le côté réception vérifie enregistrement par enregistrement, avant d’appliquer chacun.

1 Envoyer @s4, depuis l’instantané de votre choix

tank/fs a quatre instantanés, un par txg de son histoire. La figure envoie le dernier, @s4 : son arbre, chaque pointeur de bloc avec son txg de naissance logique. Choisissez l’instantané de départ que le récepteur détient déjà, ou aucun pour un flux complet, puis déroulez l’envoi pas à pas : à chaque pointeur de bloc, la naissance est comparée à F, le txg de l’instantané de départ, les enregistrements tombent dans le flux avec leur somme de contrôle, et le récepteur les vérifie et les applique jusqu’à ce que ses fichiers soient ceux de @s4. « Inverser un bit » abîme le flux en route.

Flux incrémental de @s2 à @s4 (F = 20) : visite de 12 pointeurs de bloc sur 19, 15 enregistrements, 6 728 octets.

Histoire de tank/fs
  1. txg 10 crée les fichiers a, b et c (objets 1 à 3), de quatre blocs de 512 octets chacun ; instantané @s1
  2. txg 20 réécrit le bloc 2 du fichier a ; instantané @s2
  3. txg 30 perce un trou dans les blocs 1 et 2 de b, crée d (objet 4) ; instantané @s3
  4. txg 40 supprime c ; instantané @s4

Côté envoi : l’arbre de @s4

12 pointeurs de bloc visités3 sautés4 en dessous, jamais lus15 enregistrements6 728 octets

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

Le flux

  1. 0BEGIN@s2 → @s4 (vérifié et appliqué)pas de somme
  2. 1OBJECTobjet 1, a (vérifié et appliqué)50788f0f
  3. 2FREEa 2 048–fin (vérifié et appliqué)43af0187
  4. 3OBJECTobjet 2, b (vérifié et appliqué)abe05a34
  5. 4FREEb 2 048–fin (vérifié et appliqué)e8b7bb43
  6. 5FREEb 512–1 536 (vérifié et appliqué)8c224c03
  7. 6FREEOBJECTSobjet 3 (vérifié et appliqué)356ecbe7
  8. 7OBJECTobjet 4, d (vérifié et appliqué)21559e25
  9. 8FREEd 2 048–fin (vérifié et appliqué)7331f09e
  10. 9WRITEd @0 « d0 » (vérifié et appliqué)753d4dba
  11. 10WRITEd @512 « d1 » (vérifié et appliqué)c439bd12
  12. 11WRITEd @1 024 « d2 » (vérifié et appliqué)a695a488
  13. 12WRITEd @1 536 « d3 » (vérifié et appliqué)cc0bcf92
  14. 13FREEOBJECTSobjets 5–7 (vérifié et appliqué)d3567f4c
  15. 14ENDsomme 58c87268 (vérifié et appliqué)b31db7bb

Côté réception

Le récepteur détient @s2 (txg 20).

  • aobjet 1a0a1a2′a3
  • bobjet 2b0troutroub3
  • dobjet 4d0d1d2d3

END vérifié (58c87268). Les fichiers reçus sont ceux de @s4.

Envoi de @s4 : les pointeurs de bloc visités diminuent quand F grandit
DepuisFVisitésSautés + dessousEnregistrementsOctets
aucun (complet)019 / 190 + 02111 672
@s11014 / 195 + 0167 552
@s22012 / 193 + 4156 728
@s3303 / 193 + 1372 184
Version texte de cette figure

Flux incrémental de @s2 à @s4 (F = 20) : visite de 12 pointeurs de bloc sur 19, 15 enregistrements, 6 728 octets.

Le parcours

  1. objset, né au txg 40 : 40 ≤ 20 est faux, visité. send_cb ne produit aucun enregistrement ; le bloc est lu pour atteindre ce qui est en dessous.
  2. L1 du meta-dnode, né au txg 40 : 40 ≤ 20 est faux, visité. send_cb ne produit aucun enregistrement ; le bloc est lu pour atteindre ce qui est en dessous.
  3. bloc de dnodes 0, né au txg 40 : 40 ≤ 20 est faux, visité. send_cb produit un OBJECT_RANGE, écrit seulement dans un flux brut.
  4. Dnode 0, le numéro du meta-dnode : send_cb ne produit rien.
  5. Dnode 1, fichier a : send_cb produit OBJECT ; dump_dnode l’écrit, puis un FREE depuis la fin du fichier. Écrits : OBJECT 1 (a), FREE a depuis 2 048 jusqu’à la fin.
  6. a : L1, né au txg 20 : 20 ≤ 20, sauté. Jamais lu, pas plus que ce qui est en dessous (4 pointeurs de bloc).
  7. Dnode 2, fichier b : send_cb produit OBJECT ; dump_dnode l’écrit, puis un FREE depuis la fin du fichier. Écrits : OBJECT 2 (b), FREE b depuis 2 048 jusqu’à la fin.
  8. b : L1, né au txg 30 : 30 ≤ 20 est faux, visité. send_cb ne produit aucun enregistrement ; le bloc est lu pour atteindre ce qui est en dessous.
  9. b, bloc 0, né au txg 10 : 10 ≤ 20, sauté. Jamais lu.
  10. b, bloc 1 (trou), né au txg 30 : 30 ≤ 20 est faux, visité. send_cb produit HOLE : un FREE de sa plage. Gardé en attente : dump_free et dump_freeobjects y fusionnent ce qui suit.
  11. b, bloc 2 (trou), né au txg 30 : 30 ≤ 20 est faux, visité. send_cb produit HOLE : un FREE de sa plage. Gardé en attente : dump_free et dump_freeobjects y fusionnent ce qui suit.
  12. b, bloc 3, né au txg 10 : 10 ≤ 20, sauté. Jamais lu.
  13. Dnode 3, libre : send_cb produit OBJECT ; dump_dnode change un dnode vide en FREEOBJECTS. Écrit : FREE b depuis 512, 1 024 octets.
  14. bloc de dnodes 1, né au txg 30 : 30 ≤ 20 est faux, visité. send_cb produit un OBJECT_RANGE, écrit seulement dans un flux brut.
  15. Dnode 4, fichier d : send_cb produit OBJECT ; dump_dnode l’écrit, puis un FREE depuis la fin du fichier. Écrits : FREEOBJECTS 3, OBJECT 4 (d), FREE d depuis 2 048 jusqu’à la fin.
  16. d : L1, né au txg 30 : 30 ≤ 20 est faux, visité. send_cb ne produit aucun enregistrement ; le bloc est lu pour atteindre ce qui est en dessous.
  17. d, bloc 0, né au txg 30 : 30 ≤ 20 est faux, visité. send_cb produit DATA : un WRITE avec les 512 octets du bloc. Écrit : WRITE d à 0 (d0).
  18. d, bloc 1, né au txg 30 : 30 ≤ 20 est faux, visité. send_cb produit DATA : un WRITE avec les 512 octets du bloc. Écrit : WRITE d à 512 (d1).
  19. d, bloc 2, né au txg 30 : 30 ≤ 20 est faux, visité. send_cb produit DATA : un WRITE avec les 512 octets du bloc. Écrit : WRITE d à 1 024 (d2).
  20. d, bloc 3, né au txg 30 : 30 ≤ 20 est faux, visité. send_cb produit DATA : un WRITE avec les 512 octets du bloc. Écrit : WRITE d à 1 536 (d3).
  21. Dnode 5, libre : send_cb produit OBJECT ; dump_dnode change un dnode vide en FREEOBJECTS. Gardé en attente : dump_free et dump_freeobjects y fusionnent ce qui suit.
  22. Dnode 6, libre : send_cb produit OBJECT ; dump_dnode change un dnode vide en FREEOBJECTS. Gardé en attente : dump_free et dump_freeobjects y fusionnent ce qui suit.
  23. Dnode 7, libre : send_cb produit OBJECT ; dump_dnode change un dnode vide en FREEOBJECTS. Gardé en attente : dump_free et dump_freeobjects y fusionnent ce qui suit.

Les enregistrements

  1. BEGIN
  2. OBJECT 1 (a), drr_checksum 50788f0f
  3. FREE a depuis 2 048 jusqu’à la fin, drr_checksum 43af0187
  4. OBJECT 2 (b), drr_checksum abe05a34
  5. FREE b depuis 2 048 jusqu’à la fin, drr_checksum e8b7bb43
  6. FREE b depuis 512, 1 024 octets, drr_checksum 8c224c03
  7. FREEOBJECTS 3, drr_checksum 356ecbe7
  8. OBJECT 4 (d), drr_checksum 21559e25
  9. FREE d depuis 2 048 jusqu’à la fin, drr_checksum 7331f09e
  10. WRITE d à 0 (d0), drr_checksum 753d4dba
  11. WRITE d à 512 (d1), drr_checksum c439bd12
  12. WRITE d à 1 024 (d2), drr_checksum a695a488
  13. WRITE d à 1 536 (d3), drr_checksum cc0bcf92
  14. FREEOBJECTS 5 à 7, drr_checksum d3567f4c
  15. END, drr_checksum b31db7bb

END vérifié (58c87268). Les fichiers reçus sont ceux de @s4.

L’idée, à trois profondeurs

En une phrase

Un envoi incrémental parcourt l’arbre de l’instantané le plus récent et saute chaque pointeur de bloc né au txg du plus ancien ou avant, avec tout ce qui est en dessous, puis écrit le reste en un flux d’enregistrements dont le récepteur vérifie la somme de contrôle courante avant de les rejouer.

En un paragraphe

zfs send -i @from @to parcourt l’arbre de @to avec une borne basse, F, le txg où @from a été créé. Un pointeur de bloc né à F ou avant était déjà dans @from, et tout le sous-arbre en dessous aussi, puisque la copie sur écriture donne une nouvelle naissance à chaque bloc au-dessus d’une modification : le parcours s’arrête là, sans le lire. Tout ce qu’il visite devient des enregistrements : un OBJECT pour chaque dnode rencontré, un WRITE pour chaque bloc de données, un FREE pour chaque trou, un FREEOBJECTS pour chaque emplacement de dnode vide. Le flux commence par BEGIN, qui nomme les deux instantanés par leur GUID, et finit par END. Chaque enregistrement sauf BEGIN porte la somme fletcher-4 de tous les octets du flux jusqu’à cette somme ; le récepteur tient la même somme et refuse le flux au premier en-tête en désaccord, avant d’appliquer l’enregistrement qu’il suit. Sans instantané de départ, F vaut 0, tous les blocs sont visités et le flux reconstruit le système de fichiers entier.

En détail

Un flux d’enregistrements. Un flux d’envoi est une suite de dmu_replay_record_t, chacun un en-tête de 312 octets d’un des onze types, parfois suivi d’une charge utile : les données d’un WRITE suivent son en-tête. BEGIN porte le nombre magique 0x2F5bacbacULL, le GUID de l’instantané envoyé et celui de l’instantané de départ.

La borne. Le parcours qui alimente un envoi commence ici.

send_traverse_thread Extrait de module/zfs/dmu_send.c, lignes 1203–1221, OpenZFS zfs-2.4.4. Voir sur 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 Le dataset est parcouru depuis fromtxg, le txg de création de l’instantané de départ, 0 pour un flux complet. TRAVERSE_LOGICAL rejoint les drapeaux de l’envoi, dont TRAVERSE_PRE : le rappel d’un bloc passe avant ses enfants.
  2. L1217–L1218 Un marqueur de fin de flux suit la dernière plage. Les plages que send_cb met en file rejoignent le fil principal par d’autres files, dans l’ordre, et y deviennent des enregistrements.

Le test qui saute un sous-arbre. Chaque pointeur de bloc de l’instantané y passe.

traverse_visitbp Extrait de module/zfs/dmu_traverse.c, lignes 249–279, OpenZFS zfs-2.4.4. Voir sur 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 trou de naissance 0 n’a jamais été écrit, ou date d’avant la fonctionnalité hole_birth. Ces lignes le sauteraient dans certains cas, mais seulement si send_holes_without_birth_time vaut 0, et il vaut 1 par défaut : ces trous sont visités. La figure n’en a pas ; un trou percé dans b porte le txg de la libération, et passe par le test ci-dessous.
  2. L277–L278 La règle. td_min_txg est la borne, F. Né à F ou avant, le bloc est dans l’instantané de départ : la fonction retourne avant de le lire ou d’appeler le rappel, et rien en dessous n’est atteint. Dans la figure, la puce passe en pointillés et son sous-arbre s’efface.

Naissance logique. get_birth_time renvoie la naissance logique quand TRAVERSE_LOGICAL est posé, ce qui est le cas pour un envoi. Un pointeur de bloc a deux naissances, et le commentaire qui les définit dit que la physique peut être plus petite que la logique pour la déduplication et le clonage de blocs, plus grande pour les blocs déplacés et réécrits, et que la logique est celle de la réplication incrémentale. L’envoi compare la logique, le txg où l’utilisateur a modifié la donnée : un bloc dédupliqué ou cloné sur une copie plus ancienne est bien envoyé, puisque l’utilisateur l’a écrit après F, et un bloc seulement réécrit ou déplacé sur le disque n’est pas renvoyé.

Chaque dnode d’un bloc de dnodes. Quand un bloc de dnodes est visité, chacun de ses dnodes est confié à ceci.

traverse_dnode Extrait de module/zfs/dmu_traverse.c, lignes 534–550, OpenZFS zfs-2.4.4. Voir sur 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 Le rappel est appelé pour le dnode lui-même. Aucune naissance n’est testée ici : chaque dnode d’un bloc de dnodes visité arrive à send_cb, modifié ou non.
  2. L545–L550 Puis chacun des pointeurs de bloc du dnode passe par traverse_visitbp, et par son test.

Voilà pourquoi, en envoyant @s4 depuis @s3, le flux contient des OBJECT pour a et b, dont les données n’ont pas changé : la suppression de c a réécrit le bloc de dnodes qu’ils partagent avec lui. Leurs blocs de données restent sautés, un niveau plus bas.

Des blocs visités aux enregistrements. Le rappel décide de ce que produit chaque visite.

send_cb Extrait de module/zfs/dmu_send.c, lignes 1098–1120, OpenZFS zfs-2.4.4. Voir sur 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 devient une plage OBJECT, sauf le meta-dnode, l’objet 0.
  2. L1109–L1116 Un bloc de dnodes devient un OBJECT_RANGE, que seul un flux brut écrit.
  3. L1117–L1120 Le bloc objset et chaque bloc indirect qui n’est pas un trou ne produisent aucun enregistrement : ils ne sont lus que pour atteindre ce qui est en dessous.
send_cb Extrait de module/zfs/dmu_send.c, lignes 1130–1146, OpenZFS zfs-2.4.4. Voir sur 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 Rien au-delà de la fin de l’objet.
  2. L1140–L1146 Il reste un bloc de données (un WRITE) ou un trou (un FREE de sa plage ; un trou parmi les blocs de dnodes libère des objets). REDACT relève des envois expurgés.

Une plage OBJECT va à dump_dnode. Un emplacement de dnode sans objet devient FREEOBJECTS ; un fichier devient OBJECT, avec son tampon bonus en charge utile, suivi aussitôt d’un FREE de la fin du fichier jusqu’à DMU_OBJECT_END, pour qu’un récepteur dont la copie était plus longue la coupe. dump_free garde un FREE en attente et y fusionne le suivant quand il le prolonge, d’où un seul FREE pour les deux trous de b ; un FREE jusqu’à la fin est écrit aussitôt, et tout autre enregistrement écrit d’abord celui en attente. Les FREEOBJECTS sont fusionnés de même, et les objets au-delà du dernier bloc de dnodes sont abandonnés. Un flux complet porte aussi des libérations, pour pouvoir être reçu comme un clone.

La somme de contrôle qui court le long du flux.

dump_record Extrait de module/zfs/dmu_send.c, lignes 257–283, OpenZFS zfs-2.4.4. Voir sur 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 somme fletcher-4 courante est prolongée sur l’enregistrement, jusqu’à ses 32 derniers octets.
  2. L271–L277 Sauf pour BEGIN, cette valeur va dans ces 32 octets, drr_checksum, superposé à tous les types d’enregistrement.
  3. L281–L283 Puis les 32 octets sont sommés, et la charge utile après eux. Le drr_checksum de chaque enregistrement est donc le fletcher-4 de tous les octets du flux qui le précèdent, y compris son propre en-tête jusqu’à la somme. La figure montre les 32 bits de poids faible de son dernier mot.

END en porte un de plus : drr_end.drr_checksum, la somme de tout ce qui le précède.

Le côté réception vérifie avant d’appliquer.

receive_read_payload_and_next_header Extrait de module/zfs/dmu_recv.c, lignes 2843–2868, OpenZFS zfs-2.4.4. Voir sur 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 Le récepteur tient sa propre somme : elle commence sur tout l’enregistrement BEGIN, se prolonge sur chaque charge utile, et ici sur l’en-tête suivant, jusqu’à sa somme de contrôle.
  2. L2861–L2866 Un en-tête dont le drr_checksum diffère arrête la réception : ECKSUM. Une somme entièrement nulle n’est pas vérifiée.
  3. L2868 La valeur stockée est ensuite sommée, comme l’a fait l’émetteur.

Cela se passe quand la charge utile d’un enregistrement et l’en-tête suivant ont été lus, et c’est seulement alors que l’enregistrement est mis en file pour l’écrivain : un enregistrement dont les octets ont été abîmés n’est jamais appliqué. END est vérifié une fois de plus, son drr_end.drr_checksum contre la somme qui le précède. En cas d’erreur, ce que la réception a créé est détruit : le récepteur détient toujours l’instantané de départ. Avant tout cela, le BEGIN d’un flux incrémental nomme l’instantané de départ par son GUID, et le récepteur le refuse (ENODEV) s’il ne détient pas d’instantané à ce GUID.

Des enregistrements répartis par type. Le fil d’écriture sort chaque enregistrement de la file et le confie ici.

receive_process_record Extrait de module/zfs/dmu_recv.c, lignes 3188–3213, OpenZFS zfs-2.4.4. Voir sur 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 à receive_object, qui crée l’objet, ou garde celui qui existe.
  2. L3197–L3203 FREEOBJECTS va à receive_freeobjects, qui libère chaque objet existant de la plage : c disparaît.
  3. L3204–L3206 WRITE va à receive_process_write_record. FREE, plus bas, va à receive_free, qui libère la plage dans l’objet.

Ce que le récepteur obtient. Parti des fichiers de l’instantané de départ, il retrouve par les enregistrements du flux exactement @s4 : la figure compare les deux à la fin, pour chaque instantané de départ, et ses tests le vérifient contre les enregistrements de chaque flux établis à la main.

Ce que la figure laisse de côté

La figure est petite : des fichiers de quatre blocs de 512 octets sous un bloc indirect, des blocs de quatre dnodes (un vrai bloc de dnodes de 16 Kio en contient 32), un meta-dnode dessiné avec ses deux blocs de dnodes, pas de tampon bonus. L’envoi s’y déroule d’une traite là où le code relie plusieurs fils (parcours, fusion, lecture, fil principal) par des files qui gardent l’ordre, et le récepteur démarre une fois le flux complet, là où le code reçoit par un tube au fil de l’écriture. Les en-têtes suivent la disposition de dmu_replay_record_t, seuls les champs utilisés par la figure étant remplis. Les flux expurgés, bruts (chiffrés), compressés et reprenables, les données embarquées, WRITE_BYREF et les blocs spill ne sont pas montrés ; pas plus que des débits, qui dépendent des disques et du réseau, pas de ce code.