Salle 3

Copie sur écriture, sommes de contrôle et autoréparation

Les disques rendent des données fausses sans signaler d’erreur. Un bit s’altère en silence ; une écriture atterrit au mauvais endroit, ou n’atterrit jamais ; un bogue de micrologiciel répond avec des secteurs périmés. Une somme de contrôle rangée à côté des données ne peut pas détecter une écriture perdue ou égarée, car le bloc périmé porte une somme qui lui correspond. Et un système de fichiers qui écrase les blocs vivants sur place peut rester, après une panne, à moitié ancien et à moitié nouveau.

ZFS répond aux deux problèmes avec une règle et un emplacement.

1 Ne jamais écraser un bloc vivant

Quand un bloc change, ZFS écrit la nouvelle version ailleurs et laisse l’ancienne à sa place. Le pointeur vers le bloc change, donc son parent change et s’écrit ailleurs lui aussi, et ainsi de suite jusqu’à la racine. L’ancien arbre reste entier jusqu’à ce qu’une seule écriture, le nouvel uberblock, fasse passer le pool d’un arbre à l’autre. Réécrivez un bloc pas à pas, et coupez le courant avant la validation.

Version texte de cette figure
  1. L’arbre a un uberblock au sommet, un bloc indirect racine R, deux blocs indirects I0 et I1, et quatre blocs de données D0 à D3. Chaque pointeur de bloc contient l’adresse de l’enfant, son txg de naissance et sa somme de contrôle.
  2. Réécrire un bloc de données écrit une nouvelle copie à une nouvelle adresse ; l’ancienne copie reste en place.
  3. Son parent a alors besoin d’un nouveau pointeur (nouvelle adresse, nouvelle somme) : le parent est copié à son tour, et ainsi de suite jusqu’à R.
  4. Écrire le nouvel uberblock dans la case suivante de l’anneau est la validation. Avant elle, une coupure de courant laisse l’ancien arbre intact et courant ; après elle, le nouvel arbre est le pool.
  5. Les blocs remplacés sont libérés, sauf si un instantané les référence encore : ils restent alors dans la deadlist.

2 La somme de contrôle habite chez le parent

Chaque pointeur de bloc, le blkptr_t de 128 octets, porte la somme de contrôle de 256 bits du bloc vers lequel il pointe. Un bloc ne se porte jamais garant de lui-même : son parent s’en charge, et le parent du parent se porte garant du parent, jusqu’à l’uberblock. Lire un bloc, c’est recalculer sa somme et la comparer à celle du pointeur qui y mène. Sur un miroir, un écart envoie la lecture vers l’autre disque, et la bonne copie est réécrite sur la mauvaise.

Pointeurs dans le bloc parent : les sommes attendues

      Version texte de cette figure
      1. Un fichier de quatre blocs, D0 à D3, est sur un miroir à deux voies : le disque A et le disque B en gardent chacun une copie complète. Le bloc parent contient, dans chaque pointeur de bloc, la somme fletcher4 du bloc pointé.
      2. Dégradation silencieuse : un bit de D1 s’inverse sur le disque A. Le disque rend les octets sans erreur. Recalculer fletcher4 sur ces octets donne une autre valeur que celle du pointeur : ECKSUM.
      3. Écriture perdue : le disque A a confirmé l’écriture du nouveau D2 sans la faire. Avec la copie sur écriture, cette écriture visait une nouvelle adresse, pas l’ancien D2 ; l’adresse garde donc ce qu’elle contenait avant, les octets périmés d’un ancien bloc, libéré depuis. Ces octets peuvent être cohérents avec eux-mêmes ; seule la somme rangée dans le parent, qui décrit le nouveau D2, les trahit.
      4. En cas d’écart, le miroir lit l’autre disque. Si cette copie correspond, elle est rendue à l’application et réécrite sur la mauvaise copie : le pool se répare lors d’une simple lecture.
      5. Si les deux copies sont abîmées, la lecture échoue avec une erreur plutôt que de rendre des données fausses. Chaque écart est compté contre le disque qui l’a rendu.

      L’idée, à trois profondeurs

      En une phrase

      ZFS n’écrase jamais de données vivantes et range la somme de contrôle de chaque bloc dans le pointeur qui y mène : une panne ne peut pas laisser le pool à moitié écrit, et un disque ne peut pas rendre de données fausses sans être pris.

      En un paragraphe

      Chaque écriture va à un nouvel endroit : le bloc reçoit une nouvelle adresse, ce qui change le pointeur de bloc de son parent, donc le parent, jusqu’à la racine. Rien de ce qu’atteint l’uberblock courant n’est jamais modifié : à chaque instant, les disques contiennent un arbre complet et cohérent, et l’écriture d’un seul nouvel uberblock valide le suivant. Chaque pointeur de bloc porte aussi la somme de contrôle de son enfant, ce qui fait de l’arbre un arbre de Merkle : une racine de confiance se porte garante de chaque bloc en dessous d’elle. Un écart à la lecture est une erreur détectée, jamais une corruption silencieuse, et avec une seconde copie (un miroir, ou jusqu’à trois adresses dans un même pointeur de bloc) ZFS rend la bonne copie et répare la mauvaise.

      En détail

      Où une écriture devient un nouveau bloc. Dans le pipeline d’écriture, zio_write_compress choisit entre une réécriture sur place et une nouvelle allocation.

      zio_write_compress Extrait de module/zfs/zio.c, lignes 2086–2105, OpenZFS zfs-2.4.4. Voir sur 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 réécriture sur place n’est permise que pour un bloc né dans le txg même en cours de synchronisation (BP_GET_BIRTH(bp) == zio->io_txg), de taille inchangée, à partir de la passe zfs_sync_pass_rewrite (2). Aucun uberblock validé ne référence encore un tel bloc : l’écraser ne touche aucune donnée vivante. C’est l’unique exception à la copie sur écriture pour les blocs de l’arbre.
      2. L2102–L2104 Toute autre écriture remet le pointeur de bloc à zéro et suit le pipeline d’écriture complet, dont l’étape DVA_ALLOCATE donne normalement au bloc une nouvelle adresse : zio_dva_allocate vérifie que le pointeur est un trou avant d’appeler metaslab_alloc. Juste après, trois cas changent de pipeline : un bloc qui se compresse en rien devient un trou, la déduplication prend le pipeline DDT, et le nopwrite ajoute son étape.

      Quand l’écriture se termine, dbuf_write_done appelle dsl_dataset_block_kill sur l’ancien pointeur et dsl_dataset_block_born sur le nouveau. L’ancien bloc n’est libéré que s’il est né après le dernier instantané ; sinon il passe dans la deadlist du dataset, et l’instantané le garde.

      Où la somme est vérifiée. À l’écriture, zio_checksum_compute range la valeur calculée dans bp->blk_cksum. À la lecture, le bloc est comparé à ce champ.

      zio_checksum_error_impl Extrait de module/zfs/zio_checksum.c, lignes 501–538, OpenZFS zfs-2.4.4. Voir sur 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 La valeur attendue est bp->blk_cksum, prise dans le pointeur de bloc que le lecteur a suivi ; la valeur réelle est recalculée sur les octets qui viennent d’être lus.
      2. L515–L526 Blocs chiffrés autres que les objsets : la moitié des 256 bits est le MAC du chiffrement, vérifié au déchiffrement ; seuls les 128 autres bits sont comparés ici.
      3. L535–L536 Un écart renvoie ECKSUM. Sur un miroir, ce contrôle a lieu dans la lecture de chaque enfant, pas dans le parent : zio_vdev_child_io descend l’étape CHECKSUM_VERIFY vers l’enfant. Chaque erreur est comptée sur le vdev feuille.

      La chaîne s’arrête à l’uberblock : ub_rootbp n’a pas de pointeur parent. Chaque case d’uberblock est protégée à la place par une somme SHA-256 « label » intégrée, dont le vérificateur est la position de la case sur le disque. Les en-têtes gang et les blocs du ZIL (zilog, zilog2) portent eux aussi une somme intégrée, et checksum=off existe. La valeur par défaut, checksum=on, signifie fletcher4 : quatre sommes cumulées de 64 bits sur le bloc lu en mots de 32 bits. Les figures ci-dessus la calculent pour de vrai.

      Où le miroir se répare. Dans vdev_mirror_io_done, si aucun enfant n’a encore rendu de bonnes données, vdev_mirror_child_select choisit un autre enfant et une nouvelle lecture part. S’il n’en reste aucun, la lecture échoue avec une erreur. La réparation exige au moins une bonne copie, un pool accessible en écriture, et soit une erreur inattendue, soit un resilver, soit un scrub de resilver (L883–L886). Alors :

      vdev_mirror_io_done Extrait de module/zfs/vdev_mirror.c, lignes 900–934, OpenZFS zfs-2.4.4. Voir sur 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 Les enfants qui ont rendu de bonnes données ne sont jamais réécrits : le commentaire juste au-dessus avertit qu’une coupure de courant pendant la réécriture de la seule bonne copie pourrait la perdre.
      2. L905–L924 Un enfant qui n’a pas été essayé n’est réparé que pendant un scrub, pour les vdevs indirects ou les spares dRAID, ou quand sa DTL indique que ce txg y manque.
      3. L927–L933 Le bon tampon zio->io_abd est écrit sur chaque enfant abîmé par une écriture ZIO_FLAG_IO_REPAIR, marquée ZIO_FLAG_SELF_HEAL quand la cause était une erreur inattendue.

      Ce que les figures laissent de côté

      Les figures montrent un seul fichier, d’où un arbre court : dans un pool, il descend de l’uberblock par le MOS, le dataset et le dnode de l’objet. Elles laissent de côté les passes de synchronisation, les blocs gang, la déduplication, le nopwrite, les MAC du chiffrement, et le choix, selon la charge, du côté du miroir lu en premier (ici, toujours le disque A). L’anneau d’uberblocks a 32 cases, comme sur des disques à secteurs de 4 Kio. Les sommes sont de vraies valeurs fletcher4 calculées sur les blocs de démonstration ; les adresses et les numéros de txg sont inventés.