Salle 2

L’arbre et l’uberblock

ZFS n’a pas de fsck. Après un plantage, le pool doit s’ouvrir soit dans l’état d’avant le dernier groupe de transactions, soit dans l’état d’après, jamais dans un mélange des deux, alors qu’un seul txg écrit des milliers de blocs sur plusieurs disques.

Tout le changement d’état doit donc se ramener à une seule petite écriture, qui a eu lieu ou non. Et cette écriture ne doit pas pouvoir détruire l’état précédent si le courant part en plein milieu. Le code y arrive avec un arbre suspendu à un seul pointeur, une règle qui interdit d’écraser un bloc vivant, et un ordre d’écriture à la fin de chaque txg.

1 Tout est suspendu à un pointeur

L’uberblock contient un seul pointeur de bloc, ub_rootbp. Il pointe vers le meta objset du pool, le MOS, qui commence par un dnode, le meta-dnode, dont les blocs sont des blocs de dnodes. Un dnode est la racine d’un objet : ses dn_blkptr[] pointent vers dn_nlevels − 1 niveaux de blocs indirects, et le dernier niveau pointe vers les données. Chaque pointeur est un blkptr_t de 128 octets qui contient jusqu’à trois adresses et la somme de contrôle du bloc d’en dessous. Depuis l’uberblock, il n’en faut pas plus pour trouver chaque bloc du pool. La figure dessine un fichier à trois niveaux et quatre blocs de données sous l’objset.

uberblock · txg 4210case 114 · ub_rootbpobjset4210objset4211dnodes4210dnodes4211L2 n°04210L2 n°04211L1 n°04190L1 n°14210L1 n°14211D04190D14120D24180D24211D34210
  • ancienne copie
  • en mémoire, pas écrite

Vue 3D du même état. Faites glisser, ou utilisez les flèches sur la vue active, pour la tourner ; + et − pour zoomer. Toutes les commandes ci-dessus marchent de même en 3D.

L04200L14200L24200L34200démarrageblocs

La validation

Le txg 4211 est ouvert : 5 blocs à écrire. Étape suivante : spa_sync les écrit.

  1. suivant spa_sync écrit les nouveaux blocs du txg 4211, chacun à un nouvel endroit. code
  2. à faire Vidage : chaque disque écrit dans ce txg vide son cache d’écriture. code
  3. à faire Labels pairs : L0 et L2 reçoivent la configuration du txg 4211. code
  4. à faire Uberblocks : le txg 4211 va dans la case 115 des quatre anneaux. code
  5. à faire Labels impairs : L1 et L3 reçoivent la configuration du txg 4211. code

Dernier import

Pas encore d’import. Débranchez la prise à n’importe quelle étape, puis importez le pool.

Sur le disque

LabelDébut codeConfigurationCase 114 (+242 Kio)Case 115 (+243 Kio)
L00 Miotxg 4200txg 4210txg 4083
L10,25 Miotxg 4200txg 4210txg 4083
L21023,5 Miotxg 4200txg 4210txg 4083
L31023,75 Miotxg 4200txg 4210txg 4083

Un disque de 1 Gio, ashift 9 : chaque anneau a 128 cases de 1 Kio. Pas à l’échelle : un label fait 256 Kio, la zone de démarrage 3,5 Mio, le reste porte les blocs.

Version texte de cette figure

L’uberblock le plus récent sur le disque est celui du txg 4210, dans la case 114. Il pointe vers l’objset du txg 4210. L’arbre qu’il atteint : objset (txg 4210), dnodes (txg 4210), L2 n°0 (txg 4210), L1 n°0 (txg 4190), L1 n°1 (txg 4210), D0 (txg 4190), D1 (txg 4120), D2 (txg 4180), D3 (txg 4210).

Le txg 4211 a de nouvelles copies de objset, dnodes, L2 n°0, L1 n°1 et D2 ; Le txg 4211 est ouvert : 5 blocs à écrire. Étape suivante : spa_sync les écrit.

Labels : L0 configuration txg 4200, case 115 txg 4083 ; L1 configuration txg 4200, case 115 txg 4083 ; L2 configuration txg 4200, case 115 txg 4083 ; L3 configuration txg 4200, case 115 txg 4083.

2 Une écriture copie tout son chemin

Un bloc modifié n’est jamais réécrit sur place : il reçoit une nouvelle adresse. Son parent contient son pointeur, adresse et somme de contrôle, donc le parent change aussi : dbuf_dirty marque le parent comme modifié, et ainsi de suite jusqu’au dnode, marqué à son tour dans son bloc de dnodes, jusqu’à l’objset. Un bloc de données modifié donne une nouvelle copie de lui-même et de chaque bloc de son chemin vers la racine ; tous les autres blocs sont partagés par l’ancien arbre et le nouveau. L’ancien arbre n’est pas touché : le dernier uberblock synchronisé l’atteint encore tout entier. Dans la figure, écrivez D2, puis D3 dans le même txg : leurs ancêtres communs ne sont copiés qu’une fois. La salle suivante suit les sommes de contrôle le long de ce chemin.

3 La validation, en quatre temps

À la fin d’un txg, une fois que spa_sync a écrit les nouveaux blocs, vdev_config_sync enchaîne quatre étapes dans un ordre fixe. D’abord, il vide le cache d’écriture de chaque disque écrit dans le txg, pour que le nouvel arbre soit sur un support stable avant que quoi que ce soit pointe vers lui. Puis il écrit les labels pairs, L0 et L2, avec la nouvelle configuration. Puis l’uberblock, dans la case ub_txg modulo le nombre de cases des quatre labels, que l’uberblock du txg précédent n’occupe pas : cette écriture est la validation, l’instant où le pointeur racine bascule vers le nouvel arbre. Enfin, les labels impairs, L1 et L3. Chaque disque porte quatre labels de 256 Kio, deux à son début et deux à sa fin, et chaque label se termine par un anneau de 128 Kio de cases d’uberblock : 128 cases sur les secteurs de 512 octets de la figure, 32 sur des disques à secteurs de 4 Kio.

Les labels ne sont écrits que pour les vdevs dont la configuration a changé dans ce txg. Dans la plupart des txg, aucune n’a changé : les temps 2 et 4 n’écrivent rien, et seul l’uberblock est nouveau. Désactivez « La configuration des vdevs change dans ce txg » et refaites les étapes.

4 Débrancher la prise, puis importer

À l’import, ZFS lit chaque case de chaque label. Une case ne compte que si sa somme de contrôle et son nombre magique sont justes, donc une écriture déchirée est ignorée ; parmi les autres, l’ub_txg le plus élevé gagne. Puis la configuration est lue dans le label dont la configuration est la plus récente sans être plus récente que cet uberblock. Débranchez la prise à chaque étape et importez. Avant le temps 3, aucun uberblock du nouveau txg n’existe : l’ancien gagne, les labels pairs déjà écrits sont plus récents que lui et sont écartés, les labels impairs lui correspondent encore. Dès qu’une copie du nouvel uberblock est arrivée, elle gagne, et les labels pairs lui correspondent. L’arbre est l’ancien ou le nouveau, jamais un mélange. Réglez le nombre d’écritures d’uberblock qui arrivent avant une coupure au temps 3 : zéro donne le cas (1) du commentaire du code, une ou plus le cas (2).

L’idée, à trois profondeurs

En une phrase

Chaque bloc du pool est suspendu au seul pointeur de l’uberblock, une écriture copie son chemin au lieu de l’écraser, et le nouvel uberblock va dans une case que l’ancien n’occupe pas, une fois le nouvel arbre sur le disque : le pool est donc l’ancien arbre ou le nouveau, quel que soit l’instant où le courant part.

En un paragraphe

ub_rootbp pointe vers l’objset_phys_t du MOS ; les blocs de son meta-dnode contiennent des dnodes ; les dn_blkptr[] d’un dnode mènent, à travers dn_nlevels − 1 niveaux de blocs indirects, jusqu’aux données. dbuf_dirty marque le parent d’un bloc, récursivement, si bien qu’une feuille modifiée donne à la synchronisation de nouvelles copies de la feuille et de tous ses ancêtres, chacune à une nouvelle adresse. vdev_config_sync vide ensuite le cache des disques écrits dans le txg, écrit la configuration des labels pairs, écrit l’uberblock dans la case ub_txg % VDEV_UBERBLOCK_COUNT des quatre labels et le vide du cache, puis écrit les labels impairs. À l’import, vdev_uberblock_load garde l’uberblock valide au txg le plus élevé, et vdev_label_read_config prend le label dont le txg de configuration est le plus élevé sans dépasser le sien. Une coupure avant l’arrivée de tout nouvel uberblock laisse l’ancien uberblock et les labels impairs ; une coupure après l’arrivée d’un seul laisse le nouvel uberblock et les labels pairs. Chacune de ces paires est cohérente.

En détail

Le sommet de l’arbre. L’uberblock désigne un txg et un pointeur de bloc.

struct uberblock Extrait de include/sys/uberblock_impl.h, lignes 122–139, OpenZFS zfs-2.4.4. Voir sur GitHub
struct uberblock {
	uint64_t	ub_magic;	/* UBERBLOCK_MAGIC		*/
	uint64_t	ub_version;	/* SPA_VERSION			*/
	uint64_t	ub_txg;		/* txg of last sync		*/
	uint64_t	ub_guid_sum;	/* sum of all vdev guids	*/
	uint64_t	ub_timestamp;	/* UTC time of last sync	*/
	blkptr_t	ub_rootbp;	/* MOS objset_phys_t		*/

	/* highest SPA_VERSION supported by software that wrote this txg */
	uint64_t	ub_software_version;

	/* Maybe missing in uberblocks we read, but always written */
	uint64_t	ub_mmp_magic;	/* MMP_MAGIC			*/
	/*
	 * If ub_mmp_delay == 0 and ub_mmp_magic is valid, MMP is off.
	 * Otherwise, nanosec since last MMP write.
	 */
	uint64_t	ub_mmp_delay;
  1. L125 ub_txg : le txg que cet uberblock valide. À l’import, le plus élevé des valides gagne.
  2. L127 ub_timestamp : départage deux uberblocks du même txg.
  3. L128 ub_rootbp : le seul pointeur. dsl_pool_sync_mos le fixe une fois le MOS écrit.
objset_phys_t Extrait de include/sys/dmu_objset.h, lignes 80–95, OpenZFS zfs-2.4.4. Voir sur GitHub
typedef struct objset_phys {
	dnode_phys_t os_meta_dnode;
	zil_header_t os_zil_header;
	uint64_t os_type;
	uint64_t os_flags;
	uint8_t os_portable_mac[ZIO_OBJSET_MAC_LEN];
	uint8_t os_local_mac[ZIO_OBJSET_MAC_LEN];
	char os_pad0[OBJSET_PHYS_SIZE_V2 - sizeof (dnode_phys_t)*3 -
	    sizeof (zil_header_t) - sizeof (uint64_t)*2 -
	    2*ZIO_OBJSET_MAC_LEN];
	dnode_phys_t os_userused_dnode;
	dnode_phys_t os_groupused_dnode;
	dnode_phys_t os_projectused_dnode;
	char os_pad1[OBJSET_PHYS_SIZE_V3 - OBJSET_PHYS_SIZE_V2 -
	    sizeof (dnode_phys_t)];
} objset_phys_t;
  1. L81 Un objset commence par un dnode entier, le meta-dnode. Ses blocs sont les blocs de dnodes de l’objset : l’objet n est le dnode n. L’objset propre d’un système de fichiers est suspendu au MOS de la même façon, par le ds_bp de son dataset.
dnode_phys_t Extrait de include/sys/dnode.h, lignes 213–229, OpenZFS zfs-2.4.4. Voir sur GitHub
typedef struct dnode_phys {
	uint8_t dn_type;		/* dmu_object_type_t */
	uint8_t dn_indblkshift;		/* ln2(indirect block size) */
	uint8_t dn_nlevels;		/* 1=dn_blkptr->data blocks */
	uint8_t dn_nblkptr;		/* length of dn_blkptr */
	uint8_t dn_bonustype;		/* type of data in bonus buffer */
	uint8_t	dn_checksum;		/* ZIO_CHECKSUM type */
	uint8_t	dn_compress;		/* ZIO_COMPRESS type */
	uint8_t dn_flags;		/* DNODE_FLAG_* */
	uint16_t dn_datablkszsec;	/* data block size in 512b sectors */
	uint16_t dn_bonuslen;		/* length of dn_bonus */
	uint8_t dn_extra_slots;		/* # of subsequent slots consumed */
	uint8_t dn_pad2[3];

	/* accounting is protected by dn_dirty_mtx */
	uint64_t dn_maxblkid;		/* largest allocated block ID */
	uint64_t dn_used;		/* bytes (or sectors) of disk space */
  1. L215 La taille des blocs indirects de l’objet, 128 Kio au plus : 1024 pointeurs de bloc de 128 octets.
  2. L216 La hauteur de l’arbre de l’objet ; 1 quand le dnode pointe directement vers les données.
  3. L217 Combien des pointeurs racines, dn_blkptr[], sont utilisés.

Une écriture remonte jusqu’à la racine.

dbuf_dirty Extrait de module/zfs/dbuf.c, lignes 2537–2554, OpenZFS zfs-2.4.4. Voir sur GitHub
	if (db->db_level+1 < dn->dn_nlevels) {
		dmu_buf_impl_t *parent = db->db_parent;
		dbuf_dirty_record_t *di;
		int parent_held = FALSE;

		if (db->db_parent == NULL || db->db_parent == dn->dn_dbuf) {
			int epbs = dn->dn_indblkshift - SPA_BLKPTRSHIFT;
			parent = dbuf_hold_level(dn, db->db_level + 1,
			    db->db_blkid >> epbs, FTAG);
			ASSERT(parent != NULL);
			parent_held = TRUE;
		}
		if (drop_struct_rwlock)
			rw_exit(&dn->dn_struct_rwlock);
		ASSERT3U(db->db_level + 1, ==, parent->db_level);
		di = dbuf_dirty(parent, tx);
		if (parent_held)
			dbuf_rele(parent, FTAG);
  1. L2537 Un bloc sous le niveau le plus haut de son objet a un bloc indirect parent.
  2. L2542–L2545 Le parent est au niveau + 1, bloc numéro blkid >> epbs, avec 2epbs pointeurs par bloc indirect (1024 pour 128 Kio). La figure en met 2, pour rester petite.
  3. L2552 Marquer un bloc marque son parent, et ainsi de suite ; le niveau le plus haut est tenu par le dnode, marqué à son tour. Chaque bloc marqué est écrit à un nouvel endroit à la synchronisation.

Les quatre temps. D’abord le vidage du cache.

vdev_config_sync Extrait de module/zfs/vdev_label.c, lignes 2085–2098, OpenZFS zfs-2.4.4. Voir sur GitHub
	/*
	 * Flush the write cache of every disk that's been written to
	 * in this transaction group.  This ensures that all blocks
	 * written in this txg will be committed to stable storage
	 * before any uberblock that references them.
	 */
	zio_t *zio = zio_root(spa, NULL, NULL, flags);

	for (vdev_t *vd =
	    txg_list_head(&spa->spa_vdev_txg_list, TXG_CLEAN(txg)); vd != NULL;
	    vd = txg_list_next(&spa->spa_vdev_txg_list, vd, TXG_CLEAN(txg)))
		zio_flush(zio, vd);

	(void) zio_wait(zio);
  1. L2085–L2090 La raison du vidage, dans les mots du code : les blocs de ce txg atteignent un support stable « before any uberblock that references them », avant tout uberblock qui les référence.
  2. L2093–L2098 Chaque vdev écrit dans ce txg est vidé ; l’attente les couvre tous.
vdev_config_sync Extrait de module/zfs/vdev_label.c, lignes 2100–2139, OpenZFS zfs-2.4.4. Voir sur GitHub
	/*
	 * Sync out the even labels (L0, L2) for every dirty vdev.  If the
	 * system dies in the middle of this process, that's OK: all of the
	 * even labels that made it to disk will be newer than any uberblock,
	 * and will therefore be considered invalid.  The odd labels (L1, L3),
	 * which have not yet been touched, will still be valid.  We flush
	 * the new labels to disk to ensure that all even-label updates
	 * are committed to stable storage before the uberblock update.
	 */
	if ((error = vdev_label_sync_list(spa, 0, txg, flags)) != 0) {
		if ((flags & ZIO_FLAG_IO_RETRY) != 0) {
			zfs_dbgmsg("vdev_label_sync_list() returned error %d "
			    "for pool '%s' when syncing out the even labels "
			    "of dirty vdevs", error, spa_name(spa));
		}
		goto retry;
	}

	/*
	 * Sync the uberblocks to all vdevs in svd[].
	 * If the system dies in the middle of this step, there are two cases
	 * to consider, and the on-disk state is consistent either way:
	 *
	 * (1)	If none of the new uberblocks made it to disk, then the
	 *	previous uberblock will be the newest, and the odd labels
	 *	(which had not yet been touched) will be valid with respect
	 *	to that uberblock.
	 *
	 * (2)	If one or more new uberblocks made it to disk, then they
	 *	will be the newest, and the even labels (which had all
	 *	been successfully committed) will be valid with respect
	 *	to the new uberblocks.
	 */
	if ((error = vdev_uberblock_sync_list(svd, svdcount, ub, flags)) != 0) {
		if ((flags & ZIO_FLAG_IO_RETRY) != 0) {
			zfs_dbgmsg("vdev_uberblock_sync_list() returned error "
			    "%d for pool '%s'", error, spa_name(spa));
		}
		goto retry;
	}
  1. L2100–L2108 Le temps 2, et ce que veut dire une coupure pendant celui-ci : les labels pairs plus récents que tout uberblock sont invalides, les impairs restent valides.
  2. L2109 Les labels pairs : vdev_label_sync écrit les labels l et l + 2, L0 et L2.
  3. L2118–L2131 Le temps 3, et les deux cas d’une coupure pendant celui-ci : aucun nouvel uberblock n’est arrivé, ou au moins un. La figure rejoue les deux.
  4. L2133 Les uberblocks ; vdev_uberblock_sync_list les vide ensuite du cache, et réussit si au moins une écriture a réussi.
vdev_uberblock_sync Extrait de module/zfs/vdev_label.c, lignes 1768–1797, OpenZFS zfs-2.4.4. Voir sur GitHub
	/*
	 * We chose a slot based on the txg.  If this uberblock has a special
	 * RAIDZ expansion state, then it is essentially an update of the
	 * current uberblock (it has the same txg).  However, the current
	 * state is committed, so we want to write it to a different slot. If
	 * we overwrote the same slot, and we lose power during the uberblock
	 * write, and the disk does not do single-sector overwrites
	 * atomically (even though it is required to - i.e. we should see
	 * either the old or the new uberblock), then we could lose this
	 * txg's uberblock. Rewinding to the previous txg's uberblock may not
	 * be possible because RAIDZ expansion may have already overwritten
	 * some of the data, so we need the progress indicator in the
	 * uberblock.
	 */
	int m = spa_multihost(vd->vdev_spa) ? MMP_BLOCKS_PER_LABEL : 0;
	int n = (ub->ub_txg - (RRSS_GET_STATE(ub) == RRSS_SCRATCH_VALID)) %
	    (VDEV_UBERBLOCK_COUNT(vd) - m);

	/* Copy the uberblock_t into the ABD */
	abd_t *ub_abd = abd_alloc_for_io(VDEV_UBERBLOCK_SIZE(vd), B_TRUE);
	abd_copy_from_buf(ub_abd, ub, sizeof (uberblock_t));
	abd_zero_off(ub_abd, sizeof (uberblock_t),
	    VDEV_UBERBLOCK_SIZE(vd) - sizeof (uberblock_t));

	for (int l = 0; l < VDEV_LABELS; l++)
		vdev_label_write(zio, vd, l, ub_abd,
		    VDEV_UBERBLOCK_OFFSET(vd, n), VDEV_UBERBLOCK_SIZE(vd),
		    vdev_uberblock_sync_done, good_writes,
		    flags | ZIO_FLAG_DONT_PROPAGATE);
  1. L1768–L1781 Pourquoi la case doit différer de la case courante : un écrasement déchiré de l’uberblock validé pourrait le perdre. Le commentaire parle de l’extension RAID-Z, qui réutilise un txg ; la règle de la case, plus bas, vaut pour chaque txg.
  2. L1782–L1784 La case : ub_txg modulo le nombre de cases (une de moins avec multihost). Des txg consécutifs prennent des cases consécutives ; le txg suivant ne tombe jamais sur la case du précédent.
  3. L1792–L1796 La même case dans les quatre labels du disque.
vdev_config_sync Extrait de module/zfs/vdev_label.c, lignes 2144–2161, OpenZFS zfs-2.4.4. Voir sur GitHub
	/*
	 * Sync out odd labels for every dirty vdev.  If the system dies
	 * in the middle of this process, the even labels and the new
	 * uberblocks will suffice to open the pool.  The next time
	 * the pool is opened, the first thing we'll do -- before any
	 * user data is modified -- is mark every vdev dirty so that
	 * all labels will be brought up to date.  We flush the new labels
	 * to disk to ensure that all odd-label updates are committed to
	 * stable storage before the next transaction group begins.
	 */
	if ((error = vdev_label_sync_list(spa, 1, txg, flags)) != 0) {
		if ((flags & ZIO_FLAG_IO_RETRY) != 0) {
			zfs_dbgmsg("vdev_label_sync_list() returned error %d "
			    "for pool '%s' when syncing out the odd labels of "
			    "dirty vdevs", error, spa_name(spa));
		}
		goto retry;
	}
  1. L2145–L2152 Le temps 4 : les labels impairs en dernier. Une coupure ici laisse les labels pairs et les nouveaux uberblocks, qui suffisent.
  2. L2154 La même fonction qu’au temps 2, avec l = 1 : L1 et L3.

Quels disques reçoivent labels et uberblocks : seuls les vdevs de la liste des modifiés reçoivent des labels ; si cette liste est vide, l’uberblock va à trois vdevs de premier niveau au plus.

spa_sync_rewrite_vdev_config Extrait de module/zfs/spa.c, lignes 10792–10819, OpenZFS zfs-2.4.4. Voir sur GitHub
		if (list_is_empty(&spa->spa_config_dirty_list)) {
			vdev_t *svd[SPA_SYNC_MIN_VDEVS] = { NULL };
			int svdcount = 0;
			int children = rvd->vdev_children;
			int c0 = random_in_range(children);

			for (int c = 0; c < children; c++) {
				vdev_t *vd =
				    rvd->vdev_child[(c0 + c) % children];

				/* Stop when revisiting the first vdev */
				if (c > 0 && svd[0] == vd)
					break;

				if (vd->vdev_ms_array == 0 ||
				    vd->vdev_islog ||
				    !vdev_is_concrete(vd))
					continue;

				svd[svdcount++] = vd;
				if (svdcount == SPA_SYNC_MIN_VDEVS)
					break;
			}
			error = vdev_config_sync(svd, svdcount, txg);
		} else {
			error = vdev_config_sync(rvd->vdev_child,
			    rvd->vdev_children, txg);
		}
  1. L10792–L10815 Pas de changement de configuration : trois vdevs de premier niveau au plus, à partir d’un vdev tiré au hasard, sans les logs ni les vdevs sans metaslab. Un pool d’un seul disque reçoit chaque uberblock sur ce disque.
  2. L10816–L10818 Un changement de configuration : chaque vdev de premier niveau.

Où sont les labels.

vdev_label_offset Extrait de module/zfs/vdev_label.c, lignes 158–170, OpenZFS zfs-2.4.4. Voir sur GitHub
/*
 * Basic routines to read and write from a vdev label.
 * Used throughout the rest of this file.
 */
uint64_t
vdev_label_offset(uint64_t psize, int l, uint64_t offset)
{
	ASSERT(offset < sizeof (vdev_label_t));
	ASSERT0(P2PHASE_TYPED(psize, sizeof (vdev_label_t), uint64_t));

	return (offset + l * sizeof (vdev_label_t) + (l < VDEV_LABELS / 2 ?
	    0 : psize - VDEV_LABELS * sizeof (vdev_label_t)));
}
  1. L165–L166 Le décalage tombe dans un label, et la taille du périphérique est un multiple d’un label.
  2. L168–L169 Les labels 0 et 1 au début, 2 et 3 dans les 512 derniers Kio. Sur le disque de 1 Gio de la figure : 0, 256 Kio, 1023,5 Mio et 1023,75 Mio. Après les deux labels du début vient une zone de démarrage de 3,5 Mio.

L’import. Chaque case de chaque label est lue, et une case valide est gardée si elle bat la meilleure trouvée jusque-là.

vdev_uberblock_compare Extrait de module/zfs/vdev_label.c, lignes 1495–1505, OpenZFS zfs-2.4.4. Voir sur GitHub
int
vdev_uberblock_compare(const uberblock_t *ub1, const uberblock_t *ub2)
{
	int cmp = TREE_CMP(ub1->ub_txg, ub2->ub_txg);

	if (likely(cmp))
		return (cmp);

	cmp = TREE_CMP(ub1->ub_timestamp, ub2->ub_timestamp);
	if (likely(cmp))
		return (cmp);
  1. L1498 Le txg le plus élevé gagne.
  2. L1503 À txg égal, l’horodatage le plus tardif ; puis la séquence multihost, que la figure laisse de côté.
vdev_label_read_config Extrait de module/zfs/vdev_label.c, lignes 832–863, OpenZFS zfs-2.4.4. Voir sur GitHub
	for (int l = 0; l < VDEV_LABELS; l++) {
		nvlist_t *label = NULL;

		if (zio_wait(zio[l]) == 0 &&
		    nvlist_unpack(vp[l]->vp_nvlist, sizeof (vp[l]->vp_nvlist),
		    &label, 0) == 0) {
			/*
			 * Auxiliary vdevs won't have txg values in their
			 * labels and newly added vdevs may not have been
			 * completely initialized so just return the
			 * configuration from the first valid label we
			 * encounter.
			 */
			error = nvlist_lookup_uint64(label,
			    ZPOOL_CONFIG_POOL_TXG, &label_txg);
			if ((error || label_txg == 0) && !config) {
				config = label;
				for (l++; l < VDEV_LABELS; l++)
					zio_wait(zio[l]);
				break;
			} else if (label_txg <= txg && label_txg > best_txg) {
				best_txg = label_txg;
				nvlist_free(config);
				config = fnvlist_dup(label);
			}
		}

		if (label != NULL) {
			nvlist_free(label);
			label = NULL;
		}
	}
  1. L835–L837 Un label ne compte que si sa lecture réussit, et la lecture vérifie la somme de contrôle propre au label : un label déchiré est ignoré.
  2. L852–L856 Le label au txg de configuration le plus élevé sans dépasser txg, celui de l’uberblock gagnant (l’appel). C’est ainsi que les labels pairs d’un txg qui n’a pas été validé sont écartés.

Après l’import, le txg suivant est celui du gagnant + 1 : quand l’ancien arbre a gagné, le txg perdu est réécrit, et son uberblock va dans la même case, par-dessus la case déchirée. L’import demande aussi une mise à jour de la configuration, qui marque chaque vdev comme modifié pour ce txg : il réécrit les quatre labels, déchirés compris, quoi qu’il change d’autre.

Ce que la figure laisse de côté

Un seul dataset et très peu de blocs : deux pointeurs par bloc indirect au lieu de 1024 au plus, les niveaux indirects propres au meta-dnode et le passage du MOS à l’objset d’un système de fichiers repliés en un seul bloc de dnodes. Un seul disque, si bien que chaque uberblock et chaque label y arrivent ; ses 128 cases sont celles de secteurs de 512 octets. Le code lance ensemble les écritures d’un même temps ; la figure les fait arriver dans l’ordre (L0 puis L2, L1 puis L3, les uberblocks de L0 à L3) et déchire celle en cours quand la prise est débranchée. Le contenu de la configuration, la nouvelle tentative avec ZIO_FLAG_IO_RETRY, multihost, l’extension RAID-Z, les passes de synchronisation multiples d’un txg et la libération des anciens blocs (la salle des instantanés) ne sont pas montrés. La vue 3D dessine le même état que la vue 2D.