Salle 12

Compression, déduplication, chiffrement

Entre l’application et le disque, trois fonctions peuvent transformer un bloc. Chacune économise ou protège quelque chose, et chacune a un prix que le code paie à chaque écriture.

La compression économise de la place, mais compresser coûte du temps processeur, et un bloc qui rétrécit de quelques octets n’économise rien une fois arrondi à des secteurs entiers. La déduplication évite d’écrire un bloc déjà présent sur disque, mais pour le savoir elle doit chercher chaque bloc écrit dans une table, garder cette table sur disque et la mettre à jour. Le chiffrement cache les données, mais le pool doit pouvoir vérifier, réparer et envoyer des blocs dont il n’a peut-être pas la clé. Le code répond à chaque problème par une règle, et cette salle prend les trois règles l’une après l’autre. Dans le pipeline d’écriture, elles s’exécutent dans cet ordre : la compression, puis le chiffrement, puis la somme de contrôle de ce qui sera écrit, puis la recherche de déduplication par cette somme de contrôle.

1 Compression : gagner un huitième, ou stocker tel quel

Le compresseur reçoit une limite : sept huitièmes du bloc, arrondis vers le bas à un nombre entier de secteurs. Si sa sortie ne tient pas, le bloc est stocké non compressé. Deux cas n’atteignent jamais le compresseur ou jamais le disque : un bloc de zéros devient un trou, et une sortie de 112 octets ou moins est repliée dans le pointeur de bloc lui-même. Choisissez les données, la taille du bloc, celle du secteur et la compression, et suivez la branche qui décide.

  1. Compression LZ4 : le bloc passe par les branches suivantes. code
  2. Le bloc ne contient pas que des zéros. code
  3. La sortie (49 807 octets) tient sous la limite (112 Kio). code
  4. Plus de 112 octets : elle ne tient pas dans le pointeur de bloc. code
  5. Arrondi au secteur : 52 Kio, moins que 128 Kio. Stocké compressé, la fin remplie de zéros. code (cette branche a décidé)

Stocké compressé. Sortie de 49 807 octets, compressée avec LZ4, arrondie au secteur : PSIZE 52 Kio, 13 secteurs à ashift 12.

Pointeur de bloc : PSIZE 52 Kio, LSIZE 128 Kio, compression LZ4 ; ASIZE 52 Kio.

Les 16 mots d’un pointeur de bloc : non chiffré
MotContient
0DVA 1 : vdev, ASIZE
1DVA 1 : G, décalage
2DVA 2 : vdev, ASIZE
3DVA 2 : G, décalage
4DVA 3 : vdev, ASIZE
5DVA 3 : G, décalage
6B D X, niveau, type, somme de contrôle, E, compression, PSIZE, LSIZE
7R, remplissage
8remplissage
9txg de naissance physique
atxg de naissance logique
bnombre de blocs remplis
csomme de contrôle [0]
dsomme de contrôle [1]
esomme de contrôle [2]
fsomme de contrôle [3]

Les mêmes données, 128 Kio, LZ4, à chaque taille de secteur
ashiftLimiteRésultatPSIZEAlloué
9 (512 octets)114 688Stocké compressé.50 17650 176
12 (4 Kio)114 688Stocké compressé.53 24853 248
13 (8 Kio)114 688Stocké compressé.57 34457 344
Version texte de cette figure

Sortie de 49 807 octets, compressée avec LZ4, arrondie au secteur : PSIZE 52 Kio, 13 secteurs à ashift 12.

2 Déduplication : une table consultée à chaque écriture

Avec dedup=on, chaque bloc écrit est cherché dans la table de déduplication (DDT) par sa somme de contrôle. Si la table le connaît déjà, le nouveau pointeur de bloc reçoit l’adresse de la copie existante et un compteur de références augmente : rien n’est écrit. Sinon, le bloc est écrit et une nouvelle entrée note où. Écrivez des blocs, réécrivez le même, puis faites dépasser son quota à la table.

3 écritures3 recherches dans la DDT2 blocs de données écrits2 entrées

La dernière écriture

  1. Bloc A, txg 102 : somme de contrôle 5cf94b00…, cherchée dans la DDT.
  2. Présent dans la table : ddt_lookup renvoie son entrée, qui contient un DVA. code
  3. Présent : le nouveau pointeur de bloc reçoit 0:400000 de l’entrée, né au txg 102, naissance physique 100 ; compteur de références 2. Rien n’est écrit. code

Table de déduplication (DDT)

Une entrée par somme de contrôle
Clé (somme de contrôle)DonnéesRéférencesDVANaissance physique
5cf94b00…A20:400000100
fa9a4a00…B10:420000101

Pointeurs de bloc écrits

  1. bp0AD=10:400000né au txg 100, physique 100
  2. bp1BD=10:420000né au txg 101, physique 101
  3. bp2AD=10:400000né au txg 102, physique 100

Blocs de données sur disque

  1. 0:400000Aréférencé par bp0, bp2
  2. 0:420000Bréférencé par bp1

Version texte de cette figure
  1. Absent : A est écrit en 0:400000 ; l’entrée reçoit le DVA, compteur de références 1.
  2. Absent : B est écrit en 0:420000 ; l’entrée reçoit le DVA, compteur de références 1.
  3. Présent : le nouveau pointeur de bloc reçoit 0:400000 de l’entrée, né au txg 102, naissance physique 100 ; compteur de références 2. Rien n’est écrit.

3 écritures, 3 recherches dans la DDT, 2 blocs de données écrits, 2 entrées.

Ce que ça coûte. La figure compte une recherche dans la DDT par écriture, trouvée ou non : un dataset en dedup=on la paie pour chaque bloc, pas seulement pour les doublons. Chaque entrée est une clé de 40 octets (la somme de contrôle de 256 bits et un mot de tailles et d’indicateurs) et une valeur de 256 octets au format traditionnel, 72 au nouveau format « flat ». La table vit sur disque sous forme d’objets ZAP, et le code dit pourquoi la mettre à jour coûte cher : « each entry only takes up a tiny portion of a ZAP leaf node, and so required reading the whole node, updating the entry, and writing it back. On busy pools, this could add serious IO and memory overheads. » (chaque entrée n’occupe qu’une petite partie d’une feuille de ZAP : il fallait lire toute la feuille, modifier l’entrée et la réécrire. Sur un pool chargé, cela pouvait coûter beaucoup d’E/S et de mémoire). La fonctionnalité fast_dedup répond par un journal : les entrées modifiées sont ajoutées à un journal sur disque et gardées en mémoire, puis vidées vers le ZAP un peu à chaque txg. Seules les tables créées avec cette fonctionnalité reçoivent le journal ; une table créée avant garde la réécriture d’une feuille de ZAP à chaque txg. Un dedup_table_quota peut plafonner la taille de la table sur disque ; au-delà, les nouveaux blocs ne sont tout simplement pas dédupliqués. Sa valeur par défaut, auto, ne fixe aucune taille et ne plafonne la table que sur un pool doté d’un vdev dedup ou special : sur un pool de disques simples, la table grandit avec les données. Le code ne donne aucun chiffre de mémoire par téraoctet ni de taux de déduplication : ils dépendent des données, et cette salle ne les invente pas.

3 Chiffrement : les secrets dans le pointeur de bloc, les sommes de contrôle en clair

Un bloc chiffré a besoin d’un sel, d’un IV et d’un MAC, et ils voyagent là où le bloc est référencé : dans son pointeur de bloc, à la place de l’adresse de la troisième copie et de la moitié de la somme de contrôle. L’autre moitié de la somme de contrôle est calculée sur le texte chiffré, si bien que le pool peut vérifier chaque bloc sans la clé. Comparez les mots de quatre sortes de pointeurs de bloc, puis déchargez la clé d’un fichier chiffré et lancez un scrub.

Un bloc de niveau 0 d’un type chiffré : le troisième DVA contient le sel et le début de l’IV, le haut du nombre de blocs remplis la fin de l’IV, la moitié de la somme de contrôle le MAC. Copies, au plus : 2. code

Les 16 mots d’un pointeur de bloc : chiffré
MotContient
0DVA 1 : vdev, ASIZE
1DVA 1 : G, décalage
2DVA 2 : vdev, ASIZE
3DVA 2 : G, décalage
4sel (modifié)
5IV1 : 64 premiers bits de l’IV (modifié)
6B D X, niveau, type, somme de contrôle, E, compression, PSIZE, LSIZE
7R, remplissage
8remplissage
9txg de naissance physique
atxg de naissance logique
bIV2 : 32 derniers bits de l’IV | blocs remplis, 32 bits (modifié)
csomme de contrôle [0]
dsomme de contrôle [1]
eMAC [0] (modifié)
fMAC [1] (modifié)

Un fichier chiffré

Clé chargée : les blocs chiffrés sont déchiffrés à la lecture.

  • bloc de dnodestype chiffré, cœur en clair
    cœur du dnode, en clair : type, niveaux, pointeur de bloctampon bonus : « attributs du fichier »
    • bloc indirect (L1)indirect, somme de contrôle des MAC
      trois pointeurs de bloc, en clair : DVA, naissances, sommes de contrôle, sels, IV, MAC
      • bloc de données 0chiffré
        « Ce texte »
      • bloc de données 1chiffré
        « reste lisible »
      • bloc de données 2chiffré
        « avec la clé. »

Version texte de cette figure

Un pointeur de bloc ordinaire : trois DVA, une somme de contrôle de 256 bits. Copies, au plus : 3.

Un bloc de niveau 0 d’un type chiffré : le troisième DVA contient le sel et le début de l’IV, le haut du nombre de blocs remplis la fin de l’IV, la moitié de la somme de contrôle le MAC. Copies, au plus : 2.

Un bloc de niveau 0 d’un type qui doit rester lisible : ses données sont en clair, protégées par un MAC dans la moitié de la somme de contrôle. Copies, au plus : 3.

Un bloc indirect d’un dataset chiffré : en clair, ses mots de MAC contiennent une somme de contrôle des MAC du niveau du dessous. Copies, au plus : 3.

Clé déchargée : les blocs de données et le tampon bonus se lisent comme du texte chiffré ; le cœur du dnode et le bloc indirect sont en clair sur disque.

L’idée, à trois profondeurs

En une phrase

La compression ne garde un bloc compressé que s’il économise au moins un huitième, compté en secteurs entiers ; la déduplication remplace une écriture par une référence quand la somme de contrôle du bloc est déjà dans sa table, au prix d’une recherche à chaque écriture ; le chiffrement range son sel, son IV et son MAC dans le pointeur de bloc et laisse une somme de contrôle tronquée du texte chiffré, si bien que le scrub n’a pas besoin de clé.

En un paragraphe

Dans zio_write_compress, un bloc de zéros devient un trou ; sinon le compresseur reçoit une limite de sept huitièmes du bloc, arrondie vers le bas au secteur, et une sortie qui ne tient pas laisse le bloc non compressé ; une sortie d’au plus 112 octets va dans le pointeur de bloc, qui contient alors des données au lieu d’adresses ; le reste est arrondi à des secteurs entiers. Ensuite, dans un dataset chiffré, le chiffrement prend le troisième DVA pour le sel et le début de l’IV, le haut du nombre de blocs remplis pour la fin de l’IV, et la moitié de la somme de contrôle pour le MAC ; seuls les blocs de niveau 0 des types chiffrés sont chiffrés. La somme de contrôle vient après, calculée sur ce qui sera écrit, texte chiffré compris, et le scrub, qui lit les blocs bruts, en vérifie les 128 bits restants. Enfin, avec la déduplication, zio_ddt_write cherche cette somme de contrôle dans la DDT : si elle y est, le DVA de l’entrée est copié dans le nouveau pointeur de bloc et une référence est ajoutée, sans écriture ; sinon le bloc est écrit et son DVA noté dans une nouvelle entrée ; une table au-dessus de son quota refuse les nouvelles entrées, et le bloc est écrit sans son bit D.

En détail

La limite. Avant de compresser, le chemin d’écriture demande quelle taille la sortie peut avoir.

zio_get_compression_max_size Extrait de module/zfs/zio.c, lignes 1791–1809, OpenZFS zfs-2.4.4. Voir sur GitHub
size_t
zio_get_compression_max_size(enum zio_compress compress, uint64_t gcd_alloc,
    uint64_t min_alloc, size_t s_len)
{
	size_t d_len;

	/* minimum 12.5% must be saved (legacy value, may be changed later) */
	d_len = s_len - (s_len >> 3);

	/* ZLE can't use exactly d_len bytes, it needs more, so ignore it */
	if (compress == ZIO_COMPRESS_ZLE)
		return (d_len);

	d_len = d_len - d_len % gcd_alloc;

	if (d_len < min_alloc)
		return (BPE_PAYLOAD_SIZE);
	return (d_len);
}
  1. L1797–L1798 Sept huitièmes de la source : au moins 12,5 % doivent être gagnés, une « legacy value » (valeur héritée), dit le commentaire.
  2. L1800–L1802 ZLE, qui ne compresse que les suites de zéros, échappe à ce qui suit.
  3. L1804 Arrondi vers le bas à un multiple de spa_gcd_alloc, qui pour un pool de disques simples d’un seul ashift est le secteur (le plus grand diviseur commun des tailles d’allocation des vdev) : une économie inférieure à un secteur ne rapporte rien.
  4. L1806–L1807 S’il reste moins d’un secteur, la seule sortie qui vaille est celle qui tient dans un pointeur de bloc : BPE_PAYLOAD_SIZE, 14 mots de 8 octets, 112 octets.

Sept huitièmes de 128 Kio font 112 Kio, déjà un multiple de 512 octets, de 4 Kio et de 8 Kio : pour la taille d’enregistrement par défaut, l’arrondi ne change rien. Il compte pour les petits blocs : à ashift 12, sept huitièmes de 8 Kio s’arrondissent à 4 Kio, et sept huitièmes de 4 Kio s’arrondissent à zéro, si bien qu’un bloc de 4 Kio sur des secteurs de 4 Kio n’est compressé que s’il tombe à 112 octets. Le tableau de la figure montre les mêmes données aux trois tailles de secteur.

Les quatre issues.

zio_write_compress Extrait de module/zfs/zio.c, lignes 1982–2017, OpenZFS zfs-2.4.4. Voir sur GitHub
	/* If it's a compressed write that is not raw, compress the buffer. */
	if (compress != ZIO_COMPRESS_OFF &&
	    !(zio->io_flags & ZIO_FLAG_RAW_COMPRESS)) {
		abd_t *cabd = NULL;
		if (abd_cmp_zero(zio->io_abd, lsize) == 0)
			psize = 0;
		else if (compress == ZIO_COMPRESS_EMPTY)
			psize = lsize;
		else
			psize = zio_compress_data(compress, zio->io_abd, &cabd,
			    lsize,
			    zio_get_compression_max_size(compress,
			    spa->spa_gcd_alloc, spa->spa_min_alloc, lsize),
			    zp->zp_complevel);
		if (psize == 0) {
			compress = ZIO_COMPRESS_OFF;
		} else if (psize >= lsize) {
			compress = ZIO_COMPRESS_OFF;
			if (cabd != NULL)
				abd_free(cabd);
		} else if (psize <= BPE_PAYLOAD_SIZE && !zp->zp_encrypt &&
		    zp->zp_level == 0 && !DMU_OT_HAS_FILL(zp->zp_type) &&
		    spa_feature_is_enabled(spa, SPA_FEATURE_EMBEDDED_DATA)) {
			void *cbuf = abd_borrow_buf_copy(cabd, lsize);
			encode_embedded_bp_compressed(bp,
			    cbuf, compress, lsize, psize);
			BPE_SET_ETYPE(bp, BP_EMBEDDED_TYPE_DATA);
			BP_SET_TYPE(bp, zio->io_prop.zp_type);
			BP_SET_LEVEL(bp, zio->io_prop.zp_level);
			abd_return_buf(cabd, cbuf, lsize);
			abd_free(cabd);
			BP_SET_LOGICAL_BIRTH(bp, zio->io_txg);
			zio->io_pipeline = ZIO_INTERLOCK_PIPELINE;
			ASSERT(spa_feature_is_active(spa,
			    SPA_FEATURE_EMBEDDED_DATA));
			return (zio);
  1. L1983–L1984 Rien de ceci ne s’exécute sans compression : un bloc de zéros est alors écrit comme un autre, sauf si la déduplication est active : elle active la détection des zéros parce que « storing many references to an all zeros block in the dedup table would be expensive » (garder beaucoup de références à un bloc de zéros dans la table coûterait cher).
  2. L1986–L1987 Que des zéros : PSIZE 0, qui devient un trou plus bas.
  3. L1991–L1995 Le compresseur tourne avec la limite comme taille de sortie ; zio_compress_data renvoie la taille source quand la sortie ne tient pas.
  4. L1996–L1997 Un trou : la compression est notée comme désactivée, et PSIZE reste à 0.
  5. L1998–L2001 Une sortie qui n’a rien gagné : la compression est désactivée pour ce bloc, stocké à sa taille logique.
  6. L2002–L2017 Au plus 112 octets, non chiffré, au niveau 0, d’un type sans nombre de blocs remplis, avec embedded_data activé : les données sont encodées dans le pointeur de bloc, dont les mots contiennent alors la charge utile, et l’écriture s’arrête là, sans allocation.
zio_write_compress Extrait de module/zfs/zio.c, lignes 2018–2039, OpenZFS zfs-2.4.4. Voir sur GitHub
		} else {
			/*
			 * Round compressed size up to the minimum allocation
			 * size of the smallest-ashift device, and zero the
			 * tail. This ensures that the compressed size of the
			 * BP (and thus compressratio property) are correct,
			 * in that we charge for the padding used to fill out
			 * the last sector.
			 */
			size_t rounded = (size_t)zio_roundup_alloc_size(spa,
			    psize);
			if (rounded >= lsize) {
				compress = ZIO_COMPRESS_OFF;
				abd_free(cabd);
				psize = lsize;
			} else {
				abd_zero_off(cabd, psize, rounded - psize);
				psize = rounded;
				zio_push_transform(zio, cabd,
				    psize, lsize, NULL);
			}
		}
  1. L2027–L2028 zio_roundup_alloc_size arrondit à l’allocation minimale, ou au multiple du secteur au-dessus.
  2. L2029–L2032 Si le résultat n’est pas plus petit que le bloc, il est stocké non compressé. La limite arrondie rend ce cas rare ; ZLE, non arrondi, l’atteint, comme une sortie de 112 octets d’un bloc chiffré, qui ne peut pas être embarqué.
  3. L2034–L2035 La fin du dernier secteur est remplie de zéros et PSIZE est la taille arrondie, pour que « the compressed size of the BP (and thus compressratio property) are correct » (la taille compressée du pointeur, donc la propriété compressratio, soient justes). Un trou n’alloue rien ; si le bloc existait déjà, il garde sa taille, son type et son niveau, et naît dans ce txg (hole_birth), si bien qu’un envoi incrémental le voit ; les autres blocs reçoivent PSIZE, compression, somme de contrôle et bit D.

Sur un disque seul, l’espace alloué est PSIZE arrondi au secteur : un bloc est compté en secteurs entiers, compressé ou non, et c’est pourquoi la limite se compte en secteurs.

Déduplication : la recherche. Un bloc qui porte le bit D passe par zio_ddt_write.

zio_ddt_write Extrait de module/zfs/zio.c, lignes 3816–3832, OpenZFS zfs-2.4.4. Voir sur GitHub
	ddt_enter(ddt);
	/*
	 * Search DDT for matching entry.  Skip DVAs verification here, since
	 * they can go only from override, and once we get here the override
	 * pointer can't have "D" flag to be confused with pruned DDT entries.
	 */
	IMPLY(zio->io_bp_override, !BP_GET_DEDUP(zio->io_bp_override));
	dde = ddt_lookup(ddt, bp, B_FALSE);
	if (dde == NULL) {
		/* DDT size is over its quota so no new entries */
		ddt_exit(ddt);
		zp->zp_dedup = B_FALSE;
		BP_SET_DEDUP(bp, B_FALSE);
		if (zio->io_bp_override == NULL)
			zio->io_pipeline = ZIO_WRITE_PIPELINE;
		return (zio);
	}
  1. L3823 Chaque écriture dédupliquée cherche son pointeur de bloc, sous le verrou de la DDT.
  2. L3824–L3832 NULL : la table dépasse son quota. Le bit D est effacé et le bloc passe par le pipeline d’écriture ordinaire, sans déduplication.
ddt_lookup Extrait de module/zfs/ddt.c, lignes 1374–1387, OpenZFS zfs-2.4.4. Voir sur GitHub
	boolean_t valid = B_TRUE;

	if (dde->dde_type == DDT_TYPES &&
	    dde->dde_class == DDT_CLASSES &&
	    ddt_over_quota(spa)) {
		/* Over quota. If no one is waiting, clean up right now. */
		if (dde->dde_waiters == 0) {
			avl_remove(&ddt->ddt_tree, dde);
			ddt_free(ddt, dde);
			return (NULL);
		}

		/* Flag cleanup required */
		dde->dde_flags |= DDE_FLAG_OVERQUOTA;
  1. L1376–L1384 NULL vient d’ici : la clé n’est dans aucune table (un bloc jamais vu) et ddt_over_quota dit que la table est trop grosse. Une clé trouvée est toujours renvoyée : au-dessus du quota, un doublon est encore dédupliqué ; seules les nouvelles entrées sont refusées.

Un bloc déjà connu. Une entrée qui a assez de DVA pour les copies du bloc :

zio_ddt_write Extrait de module/zfs/zio.c, lignes 3958–3987, OpenZFS zfs-2.4.4. Voir sur GitHub
		/*
		 * If there are enough DVAs in the entry to service our request,
		 * then we can just use them as-is.
		 */
		if (have_dvas >= need_dvas) {
			if (dde_io != NULL)
				mutex_exit(&dde_io->dde_io_lock);

			/*
			 * For rewrite operations, try preserving the original
			 * logical birth time.  If the result matches the
			 * original BP, this becomes a NOP.
			 */
			if (zp->zp_rewrite) {
				uint64_t orig_logical_birth =
				    BP_GET_LOGICAL_BIRTH(&zio->io_bp_orig);
				ddt_bp_fill(ddp, v, bp, orig_logical_birth);
				if (BP_EQUAL(bp, &zio->io_bp_orig)) {
					/* We can skip accounting. */
					ddt_exit(ddt);
					zio->io_flags |= ZIO_FLAG_NOPWRITE;
					return (zio);
				}
			}

			ddt_bp_fill(ddp, v, bp, txg);
			ddt_phys_addref(ddp, v);
			ddt_exit(ddt);
			return (zio);
		}
  1. L3962 L’entrée contient autant de DVA que l’écriture en demande.
  2. L3971–L3981 Une réécriture garde sa naissance d’origine si cela donne le même pointeur de bloc : alors rien ne change.
  3. L3983–L3986 ddt_bp_fill copie les DVA de l’entrée dans le nouveau pointeur de bloc, avec le txg courant pour naissance logique et celle de l’entrée pour naissance physique ; ddt_phys_addref ajoute une référence ; la fonction rend la main sans aucune écriture.

Les deux naissances expliquent le « né au txg 102, physique 100 » de la figure : les données ont été écrites au txg 100, et le pointeur de bloc créé au txg 102. La salle send / receive envoie selon la naissance logique, si bien qu’un bloc dédupliqué après un instantané est quand même envoyé.

Un bloc inconnu. Une entrée neuve n’a pas de DVA : la branche précédente n’est pas prise, et une écriture fille est lancée avec l’entrée attachée.

zio_ddt_write Extrait de module/zfs/zio.c, lignes 4156–4167, OpenZFS zfs-2.4.4. Voir sur GitHub
	/*
	 * Optimistically increment the refcount for this dedup write.
	 * If the write fails, zio_ddt_child_write_done() will decrement
	 * for all non-DDT-child parents.
	 */
	ddt_phys_addref(ddp, v);

	ddt_exit(ddt);

	zio_nowait(cio);

	return (zio);
  1. L4161 Le compteur de références de l’entrée neuve passe de 0 à 1 tout de suite, avant la fin de l’écriture ; c’est défait si l’écriture échoue.
  2. L4165 L’écriture fille part ; quand elle a son DVA, zio_ddt_child_write_ready le copie dans l’entrée, pour la prochaine fois.

La table sur disque. Les entrées modifiées dans un txg doivent rejoindre les objets de stockage de la DDT.

DDT : le journal de déduplication Extrait de module/zfs/ddt.c, lignes 136–156, OpenZFS zfs-2.4.4. Voir sur GitHub
 * ## Dedup log
 *
 * Historically, all entries modified on a txg were written back to dedup
 * storage objects at the end of every txg. This could cause significant
 * overheads, as each entry only takes up a tiny portion of a ZAP leaf node,
 * and so required reading the whole node, updating the entry, and writing it
 * back. On busy pools, this could add serious IO and memory overheads.
 *
 * To address this, the dedup log was added. If the "fast_dedup" feature is
 * enabled, at the end of each txg, modified entries will be copied to an
 * in-memory "log" object (ddt_log_t), and appended to an on-disk log. If the
 * same block is requested again, the in-memory object will be checked first,
 * and if its there, the entry inflated back onto the live tree without going
 * to storage. The on-disk log is only read at pool import time, to reload the
 * in-memory log.
 *
 * Each txg, some amount of the in-memory log will be flushed out to a DDT
 * storage object (ie ZAP) as normal. OpenZFS will try hard to flush enough to
 * keep up with the rate of change on dedup entries, but not so much that it
 * would impact overall throughput, and not using too much memory. See the
 * zfs_dedup_log_* tunables in zfs(4) for more details.
  1. L138–L142 Sans le journal, chaque entrée modifiée était réécrite à la fin de son txg, et une entrée n’est qu’une petite partie d’une feuille de ZAP : lire toute la feuille, modifier une entrée, réécrire la feuille.
  2. L144–L150 Avec fast_dedup, sur une table créée avec cette fonctionnalité (seules celles-là reçoivent DDT_FLAG_LOG), les entrées modifiées vont dans un journal en mémoire et sont ajoutées à un journal sur disque, relu seulement à l’import. Par défaut, les journaux sont échangés au bout de 8 txg au plus.
  3. L152–L156 Une partie du journal est vidée vers le ZAP à chaque txg, autant qu’il faut pour suivre sans nuire au débit, comme en décident les réglages zfs_dedup_log_*, « and not using too much memory » (sans trop de mémoire). Le commentaire ne donne pas de chiffre ici.

Deux autres mécanismes complètent le tableau, dans le même commentaire : l’élagage retire les vieilles entrées jamais dupliquées, pour faire de la place sous le quota, et le scrub parcourt d’abord la DDT, si bien qu’un bloc référencé de nombreuses fois n’est vérifié qu’une fois.

Le pointeur de bloc chiffré.

blkptr_t, chiffré Extrait de include/sys/spa.h, lignes 188–225, OpenZFS zfs-2.4.4. Voir sur GitHub
/*
 * The blkptr_t's of encrypted blocks also need to store the encryption
 * parameters so that the block can be decrypted. This layout is as follows:
 *
 *	64	56	48	40	32	24	16	8	0
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 0	|  pad  |	  vdev1         | pad   |	  ASIZE		|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 1	|G|			 offset1				|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 2	|  pad  |	  vdev2         | pad   |	  ASIZE		|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 3	|G|			 offset2				|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 4	|			salt					|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 5	|			IV1					|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 6	|BDX|lvl| type	| cksum |E| comp|    PSIZE	|     LSIZE	|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 7	|R|			padding					|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 8	|			padding					|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * 9	|			physical birth txg			|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * a	|			logical birth txg			|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * b	|		IV2		|	    fill count		|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * c	|			checksum[0]				|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * d	|			checksum[1]				|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * e	|			MAC[0]					|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
 * f	|			MAC[1]					|
 *	+-------+-------+-------+-------+-------+-------+-------+-------+
  1. L194–L201 Les deux premiers DVA, comme dans tout pointeur de bloc : un bloc chiffré garde au plus deux copies.
  2. L202–L205 Les deux mots du troisième DVA contiennent le sel et IV1, les 64 premiers bits de l’IV de 96 bits (zio_crypt_encode_params_bp).
  3. L216 IV2, les 32 derniers bits de l’IV, dans la moitié haute du nombre de blocs remplis, dont un bloc chiffré de niveau 0 n’a pas besoin.
  4. L218–L225 128 bits de somme de contrôle, puis le MAC de 128 bits (zio_crypt_encode_mac_bp).

Le bit X, bit 61 du mot 6, a trois sens : sur un bloc de niveau 0 d’un type chiffré, il signifie chiffré ; sur un bloc de niveau 0 d’un type non chiffré, authentifié ; sur un bloc indirect, que les mots de MAC contiennent une somme de contrôle des MAC du dessous. Un pointeur de bloc embarqué ne peut pas être chiffré : il n’a plus de mots pour le sel, l’IV et le MAC. Le chiffrement est AES-GCM ou AES-CCM, avec des clés de 128, 192 ou 256 bits, l’authentification SHA512-HMAC, avec un sel de 8 octets, un IV de 12 octets et un MAC de 16 octets.

Ce que reçoit chaque sorte de bloc.

zio_encrypt Extrait de module/zfs/zio.c, lignes 5123–5153, OpenZFS zfs-2.4.4. Voir sur GitHub
	/* indirect blocks only maintain a cksum of the lower level MACs */
	if (BP_GET_LEVEL(bp) > 0) {
		BP_SET_CRYPT(bp, B_TRUE);
		VERIFY0(zio_crypt_do_indirect_mac_checksum_abd(B_TRUE,
		    zio->io_orig_abd, BP_GET_LSIZE(bp), BP_SHOULD_BYTESWAP(bp),
		    mac));
		zio_crypt_encode_mac_bp(bp, mac);
		return (zio);
	}

	/*
	 * Objset blocks are a special case since they have 2 256-bit MACs
	 * embedded within them.
	 */
	if (ot == DMU_OT_OBJSET) {
		ASSERT0(DMU_OT_IS_ENCRYPTED(ot));
		ASSERT3U(BP_GET_COMPRESS(bp), ==, ZIO_COMPRESS_OFF);
		BP_SET_CRYPT(bp, B_TRUE);
		VERIFY0(spa_do_crypt_objset_mac_abd(B_TRUE, spa, dsobj,
		    zio->io_abd, psize, BP_SHOULD_BYTESWAP(bp)));
		return (zio);
	}

	/* unencrypted object types are only authenticated with a MAC */
	if (!DMU_OT_IS_ENCRYPTED(ot)) {
		BP_SET_CRYPT(bp, B_TRUE);
		VERIFY0(spa_do_crypt_mac_abd(B_TRUE, spa, dsobj,
		    zio->io_abd, psize, mac));
		zio_crypt_encode_mac_bp(bp, mac);
		return (zio);
	}
  1. L5123–L5131 Un bloc indirect n’est pas chiffré : son pointeur de bloc reçoit, dans les mots de MAC, une somme de contrôle des MAC des blocs qu’il désigne.
  2. L5137–L5144 Le bloc d’objset porte ses deux MAC en lui-même.
  3. L5146–L5153 Un bloc de niveau 0 d’un type non chiffré est seulement authentifié : un MAC, les données en clair. Ce qui reste est chiffré, et sel, IV et MAC sont encodés dans son pointeur de bloc.
dmu_write_policy Extrait de module/zfs/dmu.c, lignes 2511–2535, OpenZFS zfs-2.4.4. Voir sur GitHub
	/*
	 * All objects in an encrypted objset are protected from modification
	 * via a MAC. Encrypted objects store their IV and salt in the last DVA
	 * in the bp, so we cannot use all copies. Encrypted objects are also
	 * not subject to nopwrite since writing the same data will still
	 * result in a new ciphertext. Only encrypted blocks can be dedup'd
	 * to avoid ambiguity in the dedup code since the DDT does not store
	 * object types.
	 */
	if (os->os_encrypted && (wp & WP_NOFILL) == 0) {
		encrypt = B_TRUE;

		if (DMU_OT_IS_ENCRYPTED(type)) {
			copies = MIN(copies, SPA_DVAS_PER_BP - 1);
			gang_copies = MIN(gang_copies, SPA_DVAS_PER_BP - 1);
			nopwrite = B_FALSE;
		} else {
			dedup = B_FALSE;
		}

		if (level <= 0 &&
		    (type == DMU_OT_DNODE || type == DMU_OT_OBJSET)) {
			compress = ZIO_COMPRESS_EMPTY;
		}
	}
  1. L2523–L2526 Dans un dataset chiffré, un type chiffré garde au plus deux copies, puisque le troisième DVA contient sel et IV.
  2. L2527–L2529 Les blocs des types non chiffrés ne sont pas dédupliqués : la DDT ne note pas les types d’objets.

Ce qui reste en clair, et pourquoi le scrub n’a pas besoin de clé.

zio_crypt.c, commentaire de tête Extrait de module/os/linux/zfs/zio_crypt.c, lignes 85–120, OpenZFS zfs-2.4.4. Voir sur GitHub
 * Message Authentication Code (MAC)
 * The MAC is an output of authenticated encryption modes such as AES-GCM and
 * AES-CCM. Its purpose is to ensure that an attacker cannot modify encrypted
 * data on disk and return garbage to the application. Effectively, it is a
 * checksum that can not be reproduced by an attacker. We store the MAC in the
 * second 128 bits of blk_cksum, leaving the first 128 bits for a truncated
 * regular checksum of the ciphertext which can be used for scrubbing.
 *
 * OBJECT AUTHENTICATION:
 * Some object types, such as DMU_OT_MASTER_NODE cannot be encrypted because
 * they contain some info that always needs to be readable. To prevent this
 * data from being altered, we authenticate this data using SHA512-HMAC. This
 * will produce a MAC (similar to the one produced via encryption) which can
 * be used to verify the object was not modified. HMACs do not require key
 * rotation or IVs, so we can keep up to the full 3 copies of authenticated
 * data.
 *
 * ZIL ENCRYPTION:
 * ZIL blocks have their bp written to disk ahead of the associated data, so we
 * cannot store the MAC there as we normally do. For these blocks the MAC is
 * stored in the embedded checksum within the zil_chain_t header. The salt and
 * IV are generated for the block on bp allocation instead of at encryption
 * time. In addition, ZIL blocks have some pieces that must be left in plaintext
 * for claiming even though all of the sensitive user data still needs to be
 * encrypted. The function zio_crypt_init_uios_zil() handles parsing which
 * pieces of the block need to be encrypted. All data that is not encrypted is
 * authenticated using the AAD mechanisms that the supported encryption modes
 * provide for. In order to preserve the semantics of the ZIL for encrypted
 * datasets, the ZIL is not protected at the objset level as described below.
 *
 * DNODE ENCRYPTION:
 * Similarly to ZIL blocks, the core part of each dnode_phys_t needs to be left
 * in plaintext for scrubbing and claiming, but the bonus buffers might contain
 * sensitive user data. The function zio_crypt_init_uios_dnode() handles parsing
 * which pieces of the block need to be encrypted. For more details about
 * dnode authentication and encryption, see zio_crypt_init_uios_dnode().
  1. L89–L91 Le MAC prend la seconde moitié de la somme de contrôle, « leaving the first 128 bits for a truncated regular checksum of the ciphertext which can be used for scrubbing » (laissant les 128 premiers bits à une somme de contrôle tronquée du texte chiffré, utilisable par le scrub).
  2. L94–L100 Les types d’objets qui doivent rester lisibles, dont le master node, sont seulement authentifiés, et peuvent garder jusqu’à trois copies.
  3. L115–L120 Le cœur de chaque dnode reste en clair « for scrubbing and claiming » (pour le scrub et la réclamation) ; seuls les tampons bonus sont chiffrés.
zio_checksum_error_impl Extrait de module/zfs/zio_checksum.c, lignes 508–526, OpenZFS zfs-2.4.4. Voir sur GitHub
	/*
	 * 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;
	}
  1. L515–L516 Un pointeur de bloc qui porte le bit X, sauf un bloc d’objset.
  2. L517–L520 Pour une somme de contrôle qui n’est pas faite pour la déduplication (fletcher-4, par exemple), les mots 2 et 3 sont repliés sur 0 et 1, comme à l’écriture.
  3. L522–L525 Les mots 2 et 3 sont ignorés des deux côtés : le MAC est laissé au déchiffrement.

Le scrub lit avec ZIO_FLAG_RAW, qui saute le déchiffrement, si bien que cette vérification est tout ce qu’il fait : le scrub de la figure vérifie chaque bloc clé déchargée, et attrape un bit inversé. Une lecture qui doit déchiffrer cherche la clé et échoue avec EACCES quand elle n’est pas chargée. La clé elle-même est une clé maîtresse tirée au hasard, stockée sur disque chiffrée par la clé d’enveloppe de l’utilisateur, si bien que changer de phrase de passe ne rechiffre aucune donnée ; instantanés et clones la partagent. Des blocs chiffrés ne peuvent être dédupliqués que s’ils ont le même texte chiffré : avec la déduplication, leur sel et leur IV viennent d’un HMAC du texte en clair au lieu d’être aléatoires, et la déduplication ne fonctionne qu’au sein d’une famille de clones.

Ce que les figures laissent de côté

La sortie du compresseur est une entrée de la figure, pas le calcul d’un vrai compresseur : le code ne donne aucun taux, et les préréglages sont des exemples. LZ4 tient lieu de tous les compresseurs sauf ZLE ; tous suivent la même limite. Les niveaux de compression ne sont pas montrés, ni la règle qui cesse de compresser les réécritures à partir de la passe de synchronisation 8. Le pool est fait de disques simples d’un seul ashift, où l’allocation minimale et le secteur sont égaux ; raidz et pools mixtes ne le sont pas. La déduplication écrit un bloc par txg avec copies=1 ; la vérification, les collisions, les copies multiples, l’extension partielle des DVA de fast dedup et le journal sont racontés, pas animés ; les DVA sont inventés. Le texte chiffré, le sel, l’IV et le MAC de la figure sont des substituts, pas de l’AES ; la somme de contrôle est le fletcher-4 des octets de la figure. La rotation de clé, qui prend un nouveau sel après 400 000 000 blocs, le chiffrement du ZIL et les MAC d’objset ne sont pas montrés, pas plus qu’un chiffre de débit, de mémoire ou de taux : le code n’en donne pas.