Salle 10
ZIL et SLOG
Un programme qui appelle fsync() demande une seule promesse : quand l’appel rend la main, ses données
survivent à une coupure de courant. Une base de données la demande à chaque validation.
ZFS écrit ses changements par groupes de transactions, et un txg n’est
synchronisé que toutes les quelques secondes. Forcer la synchronisation d’un txg à chaque fsync()
réécrirait de grandes parties de l’arbre pour quelques octets de données, et ferait attendre l’appelant jusqu’au
bout. ZFS fait autrement. Chaque dataset tient un journal d’intention, le ZIL : chaque changement y est
noté sous forme d’un petit enregistrement, et à fsync() les enregistrements sont écrits tout de suite
dans une chaîne de blocs de journal. Une fois synchronisé le txg qui contient le vrai changement, ils ne servent
plus et sont jetés. Le journal n’est relu qu’après une panne. Un périphérique de journal séparé, un SLOG,
donne à ces blocs un emplacement à eux.
1 Écrire, fsync, couper le courant
Ci-dessous, un pool neuf et un fichier, déjà long de plusieurs enregistrements, dont les blocs font 128 Kio ; le txg 100 est ouvert, et le
dataset n’a pas encore de journal. Choisissez un
périphérique de journal, logbias, la façon dont le fichier est ouvert et une taille : le premier
panneau dit ce que le code décidera pour le prochain write(), et pourquoi. write()
laisse un enregistrement en mémoire ; fsync() l’écrit dans le journal, bloc par bloc, et rend la main
une fois le disque vidé. Synchronisez le txg et les blocs du journal sont libérés. Coupez le courant quand vous
voulez, puis importez le pool et montez le dataset.
Le premier fsync() crée le journal, et cette première validation attend la synchronisation du txg,
si bien qu’elle ne journalise rien ; à partir de la deuxième, les enregistrements vont dans le journal. Commencez
donc par une écriture et fsync(), puis essayez une écriture de 4 Kio et fsync() sans SLOG,
puis coupez le courant ; la même chose avec une synchronisation avant la coupure ; 128 Kio avec et sans SLOG ;
logbias=throughput ; une écriture sans fsync() avant la coupure.
Txg 100 ouvert, txg 99 synchronisé. 0 écriture : aucune. Journal : 0 bloc dans la chaîne, zh_log trou.
Ce que décide zil_write_state pour le prochain write()
Le fichier de l’application
Aucune écriture pour l’instant.
En mémoire
Sur disque
Ce qu’a fait la dernière action
- Rien pour l’instant. Le pool est neuf, le txg 100 est ouvert, le fichier a des blocs de 128 Kio.
Version texte de cette figure
Un pool sans SLOG, logbias=latency. Un fichier aux blocs de 128 Kio. Txg ouvert 100, dernier synchronisé 99.
- Écritures : aucune.
- En mémoire, en attente d’une validation : aucun.
- Blocs d’écriture du journal : aucun.
- Sur disque, depuis zh_log : vide.
- En-tête : zh_log trou, zh_claim_txg 0, ZIL_REPLAY_NEEDED non.
Txg 100 ouvert, txg 99 synchronisé. 0 écriture : aucune. Journal : 0 bloc dans la chaîne, zh_log trou.
L’idée, à trois profondeurs
En une phrase
ZFS fait rendre la main à fsync() plus tôt en n’écrivant tout de suite qu’un enregistrement du
changement, dans un journal tenu par dataset, et le changement lui-même plus tard avec son txg ; le journal est jeté
une fois ce txg synchronisé, et relu seulement après une panne.
En un paragraphe
Chaque write() crée en mémoire un enregistrement, un itx, sous le txg ouvert.
fsync(), ou une écriture dans un fichier ouvert avec O_DSYNC, valide les enregistrements
du fichier : ils sont rangés dans des blocs d’écriture du journal, les lwb, chacun écrit comme un bloc dont
la fin pointe vers le bloc suivant, si bien que le journal est une chaîne ; l’appel rend la main quand le bloc qui
contient son dernier enregistrement est écrit et les caches des disques vidés. La part des données qui va dans le
journal se décide écriture par écriture : copiée dans l’enregistrement (WR_COPIED), copiée quand le bloc est écrit
(WR_NEED_COPY), ou écrite en place dans le pool, l’enregistrement ne faisant que pointer dessus (WR_INDIRECT),
pour ne pas écrire deux fois de grosses données. Avec un SLOG, sauf avec logbias=throughput ou en
Direct I/O, les données vont toujours dans le journal. Les blocs
du journal sont pris d’abord sur le SLOG ; un pool qui n’en a pas les prend dans un espace réservé sur ses vdevs
ordinaires. Quand un txg est synchronisé, les blocs du journal dont tous les enregistrements appartiennent à des
txg synchronisés sont libérés. Après une panne, l’import du pool réclame la chaîne, et le montage du dataset rejoue
chaque enregistrement dont le txg n’avait pas été synchronisé.
En détail
Un enregistrement par écriture. Dans zfs_write, chaque écriture est journalisée par
zfs_log_write, qui crée un itx et le met en file
sous le txg de l’écriture, lrc_txg. Rien n’est encore écrit. Si le fichier
a été ouvert avec O_SYNC ou O_DSYNC, ou si le dataset a sync=always,
l’écriture est validée tout de suite : zfs_write
appelle zil_commit avant de rendre la main. Sinon l’enregistrement attend
fsync(), qui appelle la même fonction. Ce que contient l’enregistrement est
décidé d’abord :
zil_write_state
Extrait de module/zfs/zil.c, lignes
2212–2246, OpenZFS zfs-2.4.4.
Voir sur GitHub
itx_wr_state_t
zil_write_state(zilog_t *zilog, uint64_t size, uint32_t blocksize,
boolean_t o_direct, boolean_t commit)
{
if (zilog->zl_logbias == ZFS_LOGBIAS_THROUGHPUT || o_direct)
return (WR_INDIRECT);
/*
* Don't use indirect for too small writes to reduce overhead.
* Don't use indirect if written less than a half of a block if
* we are going to commit it immediately, since next write might
* rewrite the same block again, causing inflation. If commit
* is not planned, then next writes might coalesce, and so the
* indirect may be perfect.
*/
boolean_t indirect = (size >= zfs_immediate_write_sz &&
(size >= blocksize / 2 || !commit));
if (spa_has_slogs(zilog->zl_spa)) {
/* Dedicated slogs: never use indirect */
indirect = B_FALSE;
} else if (spa_has_special(zilog->zl_spa)) {
/* Special vdevs: only when beneficial */
boolean_t on_special = (blocksize <=
zilog->zl_os->os_zpl_special_smallblock);
indirect &= (on_special || !zil_special_is_slog);
}
if (indirect)
return (WR_INDIRECT);
else if (commit)
return (WR_COPIED);
else
return (WR_NEED_COPY);
}-
L2216–L2217
logbias=throughput, ou une écriture passée par le chemin Direct I/O : toujours indirect, SLOG ou pas. Le bloc de journal qui porte l’enregistrement vient quand même d’abord du SLOG :logbiasdécide où vont les données, pas le journal. - L2219–L2228 Indirect seulement à partir de 32 Kio et, pour une écriture validée tout de suite, seulement à partir d’un demi-bloc, 64 Kio pour le fichier de la figure : le commentaire dit pourquoi, l’écriture suivante pourrait réécrire le même bloc.
-
L2230–L2232
Un pool qui a un SLOG ne journalise jamais en indirect.
spa_has_slogsdemande si la classe log a un périphérique, pas s’il reste de la place : un SLOG plein compte encore. -
L2233–L2238
Des vdevs special sans SLOG : indirect seulement si les blocs du fichier vont dans la classe special (ou si
zil_special_is_slogvaut 0). Le pool de la figure n’a pas de vdev special. - L2240–L2245 Indirect : l’enregistrement porte un pointeur. Sinon WR_COPIED, les données copiées dans l’enregistrement maintenant, quand l’écriture est validée tout de suite ; ou WR_NEED_COPY, les données lues dans le fichier au remplissage du bloc de journal.
Puis zfs_log_write change WR_COPIED en WR_NEED_COPY quand les données
dépassent 7680 octets, car un enregistrement copié doit tenir dans un seul bloc de
journal, et coupe une écriture indirecte à chaque bloc du fichier. O_DIRECT ne prend le chemin Direct I/O que pour une écriture d’au moins un bloc du fichier : une écriture plus courte passe par l’ARC, si bien que dans la figure seule l’écriture de 128 Kio avec O_DIRECT est en Direct I/O. Pour WR_INDIRECT, le
commentaire de zfs_get_data donne la raison : « for large writes it’s cheaper to
sync the data and get a pointer to it (indirect) so that we don’t have to write the data twice ». Le bloc est
écrit en place par dmu_sync, ou l’a déjà été par
l’écriture Direct I/O. Dans la figure, le panneau du haut exécute cette fonction pour
chaque combinaison des boutons ; l’étiquette de chaque écriture en est le résultat.
La validation. zil_commit_impl, appelée par fsync() :
zil_commit_impl
Extrait de module/zfs/zil.c, lignes
4097–4122, OpenZFS zfs-2.4.4.
Voir sur GitHub
zil_commit_waiter_t *zcw = zil_alloc_commit_waiter();
zil_commit_itx_assign(zilog, zcw);
uint64_t wtxg = zil_commit_writer(zilog, zcw);
zil_commit_waiter(zilog, zcw);
int err = 0;
if (zcw->zcw_error != 0) {
/*
* If there was an error writing out the ZIL blocks that
* this thread is waiting on, then we fallback to
* relying on spa_sync() to write out the data this
* thread is waiting on. Obviously this has performance
* implications, but the expectation is for this to be
* an exceptional case, and shouldn't occur often.
*/
ZIL_STAT_BUMP(zilog, zil_commit_error_count);
DTRACE_PROBE2(zil__commit__io__error,
zilog_t *, zilog, zil_commit_waiter_t *, zcw);
err = txg_wait_synced_flags(zilog->zl_dmu_pool, 0,
TXG_WAIT_SUSPEND);
} else if (wtxg != 0) {
ZIL_STAT_BUMP(zilog, zil_commit_suspend_count);
err = txg_wait_synced_flags(zilog->zl_dmu_pool, wtxg,
TXG_WAIT_SUSPEND);
}- L4097–L4098 Une attente de validation est mise en file comme un itx spécial, derrière chaque enregistrement du fichier (les autres enregistrements du fichier ont été passés dans la liste synchrone juste avant).
- L4100 Les enregistrements en attente sont rangés dans des lwb, et les lwb émis.
-
L4101
L’appelant dort jusqu’à ce que le lwb qui porte son attente soit écrit et vidé (
zil_commit_waiter_done). - L4104–L4117 Sur une erreur d’écriture du journal, repli sur l’attente d’une synchronisation complète du txg : aussi durable, seulement plus lent. La figure n’a pas d’erreur d’entrée-sortie.
La vie d’un lwb. Chaque lwb passe par sept états, dans cet ordre :
lwb_state_t
Extrait de include/sys/zil_impl.h, lignes
83–92, OpenZFS zfs-2.4.4.
Voir sur GitHub
typedef enum {
LWB_STATE_NEW,
LWB_STATE_OPENED,
LWB_STATE_CLOSED,
LWB_STATE_READY,
LWB_STATE_ISSUED,
LWB_STATE_WRITE_DONE,
LWB_STATE_FLUSH_DONE,
LWB_NUM_STATES
} lwb_state_t;- L84–L85 Nouveau, puis ouvert à son premier enregistrement.
- L86 Fermé quand il est plein, ou à la fin d’une validation, qui crée aussi le lwb suivant, encore vide.
- L87–L88 Prêt une fois rempli de ses données ; émis une fois le bloc suivant alloué.
-
L89–L90
Écrit, puis vidé quand les caches des disques le sont : les attentes posées sur lui sont réveillées et
fsync()rend la main.
Le commentaire au-dessus de l’énumération décrit les transitions et les verrous qu’elles demandent. Avec un seul écrivain et rien d’autre en vol, comme dans la figure, le lwb qui porte une validation est fermé et émis tout de suite au lieu de rester ouvert pour d’autres enregistrements. La figure dessine chaque état sur chaque lwb, dans le panneau de la mémoire.
Le rangement. Comment les enregistrements entrent dans les lwb :
zil_lwb_assign
Extrait de module/zfs/zil.c, lignes
2334–2369, OpenZFS zfs-2.4.4.
Voir sur GitHub
lwb_sp = lwb->lwb_nmax - lwb->lwb_nused;
max_log_data = zil_max_log_data(zilog, sizeof (lr_write_t));
if (reclen > lwb_sp || (reclen + dlen > lwb_sp &&
lwb_sp < zil_max_waste_space(zilog) &&
(dlen % max_log_data == 0 ||
lwb_sp < reclen + dlen % max_log_data))) {
list_insert_tail(ilwbs, lwb);
lwb = zil_lwb_write_close(zilog, lwb);
if (lwb == NULL)
return (NULL);
zil_lwb_write_open(zilog, lwb);
lwb_sp = lwb->lwb_nmax - lwb->lwb_nused;
}
/*
* There must be enough space in the log block to hold reclen.
* For WR_COPIED, we need to fit the whole record in one block,
* and reclen is the write record header size + the data size.
* For WR_NEED_COPY, we can create multiple records, splitting
* the data into multiple blocks, so we only need to fit one
* word of data per block; in this case reclen is just the header
* size (no data).
*/
ASSERT3U(reclen + MIN(dlen, sizeof (uint64_t)), <=, lwb_sp);
dnow = MIN(dlen, lwb_sp - reclen);
if (dlen > dnow) {
ASSERT3U(lr->lrc_txtype, ==, TX_WRITE);
ASSERT3U(itx->itx_wr_state, ==, WR_NEED_COPY);
citx = zil_itx_clone(itx);
clr = &citx->itx_lr;
lr_write_t *clrw = (lr_write_t *)clr;
clrw->lr_length = dnow;
lrw->lr_offset += dnow;
lrw->lr_length -= dnow;
zilog->zl_cur_left -= dnow;- L2334–L2346 Un enregistrement qui ne tient pas ferme le lwb et ouvre le suivant. Le long test ne laisse un enregistrement WR_NEED_COPY commencer dans un bloc presque plein que si cela évite une coupure.
- L2348–L2357 Un enregistrement WR_COPIED doit tenir entier : il n’est jamais coupé.
- L2359–L2369 Un enregistrement WR_NEED_COPY plus grand que la place restante est coupé : un clone prend ce qui tient, le reste passe au bloc suivant.
Chaque enregistrement reçoit ensuite le numéro de séquence suivant, lrc_seq, et
le lwb garde le txg le plus haut de ses enregistrements. L’en-tête d’un enregistrement
d’écriture, lr_write_t, fait 192 octets ; la fin d’un bloc de journal,
zil_chain_t, 184 octets ; un bloc de journal, au plus
128 Kio. Un bloc de 128 Kio contient donc 130 888 octets d’enregistrements, et une écriture
WR_NEED_COPY de 128 Kio fait 130 696 octets dans un lwb et 376 octets dans le suivant :
la figure le montre avec un SLOG.
La chaîne, et où intervient le SLOG. Quand un lwb est émis, il alloue le bloc du lwb suivant :
zil_lwb_write_issue
Extrait de module/zfs/zil.c, lignes
2073–2092, OpenZFS zfs-2.4.4.
Voir sur GitHub
error = zio_alloc_zil(spa, zilog->zl_os, txg, bp,
min_size, max_size, &slog, flexible);
if (error == 0) {
if (closed_slim)
ASSERT3U(BP_GET_LSIZE(bp), ==, max_size);
else if (flexible)
nlwb->lwb_sz = BP_GET_LSIZE(bp);
else
ASSERT3U(BP_GET_LSIZE(bp), ==, nlwb->lwb_sz);
}
if (flexible)
mutex_exit(&nlwb->lwb_lock);
}
if (error == 0) {
ASSERT3U(BP_GET_BIRTH(bp), ==, txg);
BP_SET_CHECKSUM(bp, (nlwb->lwb_flags & LWB_FLAG_SLIM) ?
ZIO_CHECKSUM_ZILOG2 : ZIO_CHECKSUM_ZILOG);
bp->blk_cksum = lwb->lwb_blk.blk_cksum;
bp->blk_cksum.zc_word[ZIL_ZC_SEQ]++;
}-
L2073–L2074
Le bloc suivant est alloué maintenant, et son pointeur va dans la fin de ce bloc,
zc_next_blk: chaque bloc pointe vers le suivant, à partir dezh_logdans l’en-tête. - L2087 Le nouveau bloc naît dans un txg ouvert.
- L2088–L2091 Il hérite de la graine de somme de contrôle de ce bloc, mot de séquence incrémenté : au rejeu, c’est ainsi qu’un bloc suivant valide se distingue de vieilles données.
Le premier bloc d’une chaîne, de 4 Kio, vient de zil_create,
par le même allocateur, zio_alloc_zil :
zio_alloc_zil
Extrait de module/zfs/zio.c, lignes
4561–4595, OpenZFS zfs-2.4.4.
Voir sur GitHub
/* Try log class (dedicated slog devices) first */
error = metaslab_alloc_range(spa, spa_log_class(spa), min_size,
max_size, new_bp, 1, txg, NULL, flags, &io_alloc_list, allocator,
NULL, &alloc_size);
*slog = (error == 0);
/* Try special_embedded_log class (reserved on special vdevs) */
if (error != 0) {
error = metaslab_alloc_range(spa,
spa_special_embedded_log_class(spa), min_size, max_size,
new_bp, 1, txg, NULL, flags, &io_alloc_list, allocator,
NULL, &alloc_size);
}
/* Try special class (general special vdev allocation) */
if (error != 0) {
error = metaslab_alloc_range(spa, spa_special_class(spa),
min_size, max_size, new_bp, 1, txg, NULL, flags,
&io_alloc_list, allocator, NULL, &alloc_size);
}
/* Try embedded_log class (reserved on normal vdevs) */
if (error != 0) {
error = metaslab_alloc_range(spa, spa_embedded_log_class(spa),
min_size, max_size, new_bp, 1, txg, NULL, flags,
&io_alloc_list, allocator, NULL, &alloc_size);
}
/* Finally fall back to normal class */
if (error != 0) {
ZIOSTAT_BUMP(ziostat_alloc_class_fallbacks);
error = metaslab_alloc_range(spa, spa_normal_class(spa),
min_size, max_size, new_bp, 1, txg, NULL, flags,
&io_alloc_list, allocator, NULL, &alloc_size);
}-
L4561–L4565
La classe log, les périphériques SLOG, d’abord. Le succès pose
slog, qui devientLWB_FLAG_SLOGsur le lwb. - L4567–L4580 Puis l’espace de journal réservé sur les vdevs special, et la classe special.
- L4582–L4587 Puis le journal intégré : un espace que chaque vdev ordinaire de premier niveau réserve au ZIL. Un pool sans SLOG, ou avec un SLOG plein, et sans vdev special, journalise là.
- L4589–L4595 En dernier, la classe normale.
La première validation attend un txg. La validation appelle zil_create
quand le dataset n’a pas de journal. Cette fonction ne peut pas inscrire elle-même le
nouveau bloc dans zh_log : l’en-tête ne change qu’en contexte de synchronisation. Elle alloue donc le
bloc dans une transaction et attend la synchronisation de ce txg. Les écritures de
ce txg sont alors dans l’arbre : la validation trouve leur txg synchronisé et
détruit leurs itx au lieu de les journaliser ; seule l’attente de
validation entre dans le premier lwb. La première validation sur un nouveau journal, un fsync()
ou une écriture O_DSYNC, coûte donc une synchronisation de txg, et c’est pourquoi un journal sans rien à rejouer garde son premier
bloc.
Dans la figure, chaque allocation liste les classes essayées. Le code dit où va un bloc de journal, pas à quelle vitesse va le périphérique : ce qu’apporte un SLOG dépend du périphérique.
Quand le txg est synchronisé. Pendant la synchronisation, pour chaque dataset dont le journal a changé :
zil_sync
Extrait de module/zfs/zil.c, lignes
4206–4224, OpenZFS zfs-2.4.4.
Voir sur GitHub
while ((lwb = list_head(&zilog->zl_lwb_list)) != NULL) {
zh->zh_log = lwb->lwb_blk;
if (lwb->lwb_state != LWB_STATE_FLUSH_DONE ||
lwb->lwb_alloc_txg > txg || lwb->lwb_max_txg > txg)
break;
list_remove(&zilog->zl_lwb_list, lwb);
if (!BP_IS_HOLE(&lwb->lwb_blk))
zio_free(spa, txg, &lwb->lwb_blk);
zil_free_lwb(zilog, lwb);
/*
* If we don't have anything left in the lwb list then
* we've had an allocation failure and we need to zero
* out the zil_header blkptr so that we don't end
* up freeing the same block twice.
*/
if (list_is_empty(&zilog->zl_lwb_list))
BP_ZERO(&zh->zh_log);
}-
L4207
Le
zh_logde l’en-tête suit la tête de la liste. - L4208–L4210 Un lwb est libéré une fois vidé, quand chacun de ses enregistrements appartient à un txg synchronisé et que son propre bloc a été alloué dans ce txg ou avant. Le premier qui ne l’est pas arrête la boucle.
- L4211–L4214 Son bloc retourne à l’allocateur.
Les enregistrements d’un txg synchronisé qui n’ont jamais été écrits dans le journal sont simplement abandonnés
par zil_clean : le txg les a rendus durables. Dans la figure, les blocs libérés
sont grisés et barrés.
Après une panne : la réclamation. À l’import, dans le premier txg du pool, pour chaque dataset :
zil_claim
Extrait de module/zfs/zil.c, lignes
1232–1253, OpenZFS zfs-2.4.4.
Voir sur GitHub
* Claim all log blocks if we haven't already done so, and remember
* the highest claimed sequence number. This ensures that if we can
* read only part of the log now (e.g. due to a missing device),
* but we can read the entire log later, we will not try to replay
* or destroy beyond the last block we successfully claimed.
*/
ASSERT3U(zh->zh_claim_txg, <=, first_txg);
if (zh->zh_claim_txg == 0) {
(void) zil_parse(zilog, zil_claim_log_block,
zil_claim_log_record, tx, first_txg, B_FALSE);
zh->zh_claim_txg = first_txg;
zh->zh_claim_blk_seq = zilog->zl_parse_blk_seq;
zh->zh_claim_lr_seq = zilog->zl_parse_lr_seq;
if (zilog->zl_parse_lr_count || zilog->zl_parse_blk_count > 1)
zh->zh_flags |= ZIL_REPLAY_NEEDED;
zh->zh_flags |= ZIL_CLAIM_LR_SEQ_VALID;
if (os->os_encrypted)
os->os_next_write_raw[tx->tx_txg & TXG_MASK] = B_TRUE;
dsl_dataset_dirty(dmu_objset_ds(os), tx);
}
ASSERT3U(first_txg, ==, (spa_last_synced_txg(zilog->zl_spa) + 1));-
L1239–L1241
zil_parseparcourt la chaîne depuiszh_loget réclame chaque bloc, pour que l’allocateur ne le redonne pas, ainsi que les blocs vers lesquels pointent les enregistrements WR_INDIRECT. Le parcours s’arrête au bloc alloué mais jamais écrit : sa somme de contrôle ne correspond pas. - L1242 Le txg de réclamation : le premier txg du pool.
-
L1245–L1246
ZIL_REPLAY_NEEDEDsi un enregistrement a été trouvé, ou si plus d’un bloc a été suivi. Le parcours compte le dernier bloc, jamais écrit : un journal vide en compte un. - L1253 Ce premier txg est le dernier txg synchronisé plus un.
Le rejeu. Au montage, zil_replay fait passer chaque enregistrement par :
zil_replay_log_record
Extrait de module/zfs/zil.c, lignes
4696–4712, OpenZFS zfs-2.4.4.
Voir sur GitHub
static int
zil_replay_log_record(zilog_t *zilog, const lr_t *lr, void *zra,
uint64_t claim_txg)
{
zil_replay_arg_t *zr = zra;
const zil_header_t *zh = zilog->zl_header;
uint64_t reclen = lr->lrc_reclen;
uint64_t txtype = lr->lrc_txtype;
int error = 0;
zilog->zl_replaying_seq = lr->lrc_seq;
if (lr->lrc_seq <= zh->zh_replay_seq) /* already replayed */
return (0);
if (lr->lrc_txg < claim_txg) /* already committed */
return (0);- L4708–L4709 Un enregistrement déjà rejoué par un rejeu antérieur interrompu est ignoré.
- L4711–L4712 Un enregistrement d’un txg antérieur au txg de réclamation est ignoré : ce txg a été synchronisé, le changement est déjà dans l’arbre. Tous les autres sont appliqués de nouveau.
Sans ZIL_REPLAY_NEEDED, zil_replay se contente de détruire le
journal, en gardant son premier bloc : l’en-tête vidé le désigne de nouveau, sous un
nouveau GUID de chaîne, et la validation suivante écrit là sans rien allouer. Sinon il rejoue,
puis détruit le journal. L’en-tête qui garde tout cela,
zil_header_t, est dans le dataset : zh_claim_txg,
zh_replay_seq, zh_log, les numéros de séquence réclamés et les drapeaux. Une panne avant
la synchronisation ramène donc chaque enregistrement du txg non synchronisé ; une panne après trouve un journal
sans rien à rejouer, et une écriture jamais passée par fsync() ni synchronisée est perdue, comme son
programme l’a permis. Après un rejeu, tout le journal est détruit : la validation suivante en crée un nouveau et
attend de nouveau un txg.
Ce que la figure laisse de côté
La figure exécute les fonctions ci-dessus sur un fichier d’un dataset, avec un seul écrivain et un seul txg en vol,
synchronisé d’un coup en une passe. Le fichier est déjà long de plusieurs enregistrements, ses blocs font donc 128 Kio ;
un fichier d’un seul bloc plus petit que recordsize a un bloc plus petit, et la règle du
demi-bloc l’utilise. Le premier bloc d’une nouvelle chaîne fait 4 Kio, comme dans le code ; chaque bloc de journal
suivant fait 128 Kio, là où le code choisit la taille de chaque bloc d’après la rafale d’enregistrements
(zil_lwb_plan). Dans une vraie chaîne, les enregistrements se coupent donc
ailleurs, et les nombres d’octets de la figure sont ceux de blocs de 128 Kio. Chaque write() commence au début d’un bloc du fichier. Il n’y a pas d’erreur
d’entrée-sortie, donc pas de repli sur txg_wait_synced ; le vidage est une seule
étape pour tout le lwb, sans l’arbre de vidage par vdev ; le délai de validation ne se déclenche jamais, puisqu’un seul
écrivain ferme son lwb tout de suite. Au-delà de zil_slog_bulk par validation,
64 Mio, les écritures vont au SLOG avec une priorité plus basse ; la disposition Slim ZIL et le
chiffrement ne sont pas dessinés. Rien de cela ne change un état d’écriture, une classe, ni les écritures rejouées. Pour le
moment où un txg est synchronisé, voir Groupes de transactions ; pour la façon
dont les classes se partagent l’espace du pool, Metaslabs et space maps.