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.
- Compression LZ4 : le bloc passe par les branches suivantes. code
- Le bloc ne contient pas que des zéros. code
- La sortie (49 807 octets) tient sous la limite (112 Kio). code
- Plus de 112 octets : elle ne tient pas dans le pointeur de bloc. code
- 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.
| Mot | Contient |
|---|---|
| 0 | DVA 1 : vdev, ASIZE |
| 1 | DVA 1 : G, décalage |
| 2 | DVA 2 : vdev, ASIZE |
| 3 | DVA 2 : G, décalage |
| 4 | DVA 3 : vdev, ASIZE |
| 5 | DVA 3 : G, décalage |
| 6 | B D X, niveau, type, somme de contrôle, E, compression, PSIZE, LSIZE |
| 7 | R, remplissage |
| 8 | remplissage |
| 9 | txg de naissance physique |
| a | txg de naissance logique |
| b | nombre de blocs remplis |
| c | somme de contrôle [0] |
| d | somme de contrôle [1] |
| e | somme de contrôle [2] |
| f | somme de contrôle [3] |
| ashift | Limite | Résultat | PSIZE | Alloué |
|---|---|---|---|---|
| 9 (512 octets) | 114 688 | Stocké compressé. | 50 176 | 50 176 |
| 12 (4 Kio) | 114 688 | Stocké compressé. | 53 248 | 53 248 |
| 13 (8 Kio) | 114 688 | Stocké compressé. | 57 344 | 57 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
- Bloc A, txg 102 : somme de contrôle
5cf94b00…, cherchée dans la DDT. - Présent dans la table :
ddt_lookuprenvoie son entrée, qui contient un DVA. code - Présent : le nouveau pointeur de bloc reçoit
0:400000de 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)
| Clé (somme de contrôle) | Données | Références | DVA | Naissance physique |
|---|---|---|---|---|
5cf94b00… | A | 2 | 0:400000 | 100 |
fa9a4a00… | B | 1 | 0:420000 | 101 |
Pointeurs de bloc écrits
- bp0AD=1
0:400000né au txg 100, physique 100 - bp1BD=1
0:420000né au txg 101, physique 101 - bp2AD=1
0:400000né au txg 102, physique 100
Blocs de données sur disque
0:400000Aréférencé par bp0, bp20:420000Bréférencé par bp1
Version texte de cette figure
- Absent : A est écrit en
0:400000; l’entrée reçoit le DVA, compteur de références 1. - Absent : B est écrit en
0:420000; l’entrée reçoit le DVA, compteur de références 1. - Présent : le nouveau pointeur de bloc reçoit
0:400000de 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
| Mot | Contient |
|---|---|
| 0 | DVA 1 : vdev, ASIZE |
| 1 | DVA 1 : G, décalage |
| 2 | DVA 2 : vdev, ASIZE |
| 3 | DVA 2 : G, décalage |
| 4 | sel (modifié) |
| 5 | IV1 : 64 premiers bits de l’IV (modifié) |
| 6 | B D X, niveau, type, somme de contrôle, E, compression, PSIZE, LSIZE |
| 7 | R, remplissage |
| 8 | remplissage |
| 9 | txg de naissance physique |
| a | txg de naissance logique |
| b | IV2 : 32 derniers bits de l’IV | blocs remplis, 32 bits (modifié) |
| c | somme de contrôle [0] |
| d | somme de contrôle [1] |
| e | MAC [0] (modifié) |
| f | MAC [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 claircœur du dnode, en clair : type, niveaux, pointeur de bloctampon bonus : « attributs du fichier »
- bloc indirect (L1)indirect, somme de contrôle des MACtrois 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);
}- 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.
- L1800–L1802 ZLE, qui ne compresse que les suites de zéros, échappe à ce qui suit.
-
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. -
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);- 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).
- L1986–L1987 Que des zéros : PSIZE 0, qui devient un trou plus bas.
-
L1991–L1995
Le compresseur tourne avec la limite comme taille de sortie ;
zio_compress_datarenvoie la taille source quand la sortie ne tient pas. - L1996–L1997 Un trou : la compression est notée comme désactivée, et PSIZE reste à 0.
- L1998–L2001 Une sortie qui n’a rien gagné : la compression est désactivée pour ce bloc, stocké à sa taille logique.
-
L2002–L2017
Au plus 112 octets, non chiffré, au niveau 0, d’un type sans nombre de blocs remplis, avec
embedded_dataactivé : 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);
}
}-
L2027–L2028
zio_roundup_alloc_sizearrondit à l’allocation minimale, ou au multiple du secteur au-dessus. - 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é.
-
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);
}- L3823 Chaque écriture dédupliquée cherche son pointeur de bloc, sous le verrou de la DDT.
- 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;-
L1376–L1384
NULL vient d’ici : la clé n’est dans aucune table (un bloc jamais vu) et
ddt_over_quotadit 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);
}- L3962 L’entrée contient autant de DVA que l’écriture en demande.
- L3971–L3981 Une réécriture garde sa naissance d’origine si cela donne le même pointeur de bloc : alors rien ne change.
-
L3983–L3986
ddt_bp_fillcopie 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_addrefajoute 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);- 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.
-
L4165
L’écriture fille part ; quand elle a son DVA,
zio_ddt_child_write_readyle 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.- 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.
-
L144–L150
Avec
fast_dedup, sur une table créée avec cette fonctionnalité (seules celles-là reçoiventDDT_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. -
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] |
* +-------+-------+-------+-------+-------+-------+-------+-------+- L194–L201 Les deux premiers DVA, comme dans tout pointeur de bloc : un bloc chiffré garde au plus deux copies.
-
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). - 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.
-
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);
}- 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.
- L5137–L5144 Le bloc d’objset porte ses deux MAC en lui-même.
- 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;
}
}- L2523–L2526 Dans un dataset chiffré, un type chiffré garde au plus deux copies, puisque le troisième DVA contient sel et IV.
- 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().- 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).
- L94–L100 Les types d’objets qui doivent rester lisibles, dont le master node, sont seulement authentifiés, et peuvent garder jusqu’à trois copies.
- 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;
}- L515–L516 Un pointeur de bloc qui porte le bit X, sauf un bloc d’objset.
- 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.
- 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.