Sala 10
ZIL y SLOG
Un programa que llama a fsync() pide una sola promesa: cuando la llamada retorne, sus datos sobrevivirán
a un corte de corriente. Una base de datos la pide en cada confirmación.
ZFS escribe sus cambios en grupos de transacciones, y un txg solo se sincroniza
cada pocos segundos. Forzar la sincronización de un txg en cada fsync() reescribiría grandes partes del
árbol por unos pocos bytes de datos, y haría esperar a quien llama a que todo terminara. ZFS hace otra cosa. Cada dataset
mantiene un registro de intención, el ZIL: cada cambio se anota como una pequeña entrada, y con
fsync() las entradas se escriben de inmediato en una cadena de bloques de registro. Una vez
sincronizado el txg que contiene el cambio real, ya no hacen falta y se desechan. El registro solo se lee después
de un fallo. Un dispositivo de registro separado, un SLOG, da a esos bloques un sitio propio.
1 Escribir, fsync, cortar la corriente
Abajo, un pool nuevo y un archivo, que ya ocupa varios registros, cuyos bloques son de 128 KiB; el txg 100 está
abierto, y el dataset aún no tiene registro. Elige un dispositivo de registro, logbias, cómo se abre
el archivo y un tamaño: el primer panel dice lo que el código decidirá para el próximo write(), y por
qué. write() deja una entrada en memoria; fsync() la escribe en el registro, bloque a
bloque, y retorna una vez que el disco ha volcado su caché. Sincroniza el txg y los bloques del registro se liberan.
Corta la corriente cuando quieras, y luego importa el pool y monta el dataset.
El primer fsync() crea el registro, y esa primera confirmación espera a que se sincronice el txg, así
que no registra nada; a partir de la segunda, las entradas van al registro. Así que empieza con una escritura y
fsync(), luego prueba una escritura de 4 KiB y fsync() sin SLOG, y corta la corriente;
lo mismo con una sincronización antes del corte; 128 KiB con y sin SLOG; logbias=throughput; una
escritura sin fsync() antes del corte.
Txg 100 abierto, txg 99 sincronizado. 0 escrituras: ninguna. Registro: 0 bloques en la cadena, zh_log hueco.
Lo que decide zil_write_state para el próximo write()
El archivo de la aplicación
Aún no hay escrituras.
En memoria
En disco
Lo que hizo la última acción
- Nada por ahora. El pool es nuevo, el txg 100 está abierto, el archivo tiene bloques de 128 KiB.
Versión en texto de esta figura
Un pool sin SLOG, logbias=latency. Un archivo con bloques de 128 KiB. Txg abierto 100, último sincronizado 99.
- Escrituras: ninguna.
- En memoria, a la espera de una confirmación: ninguno.
- Bloques de escritura del registro: ninguno.
- En disco, desde zh_log: vacía.
- Cabecera: zh_log hueco, zh_claim_txg 0, ZIL_REPLAY_NEEDED no.
Txg 100 abierto, txg 99 sincronizado. 0 escrituras: ninguna. Registro: 0 bloques en la cadena, zh_log hueco.
La idea, a tres profundidades
En una frase
ZFS hace que fsync() retorne pronto escribiendo de inmediato solo una entrada que describe el cambio,
en un registro propio de cada dataset, y el cambio en sí más tarde con su txg; el registro se desecha una vez
sincronizado ese txg, y solo se lee después de un fallo.
En un párrafo
Cada write() crea una entrada en memoria, un itx, bajo el txg abierto.
fsync(), o una escritura en un archivo abierto con O_DSYNC, confirma las entradas del
archivo: se empaquetan en bloques de escritura del registro, los lwb, cada uno escrito como un bloque
cuyo final apunta al bloque siguiente, de modo que el registro es una cadena; la llamada retorna cuando el bloque
que contiene su última entrada está escrito y las cachés de los discos volcadas. Cuántos datos van al registro
se decide escritura a escritura: copiados en la entrada (WR_COPIED), copiados cuando se escribe el bloque
(WR_NEED_COPY), o escritos en su sitio en el pool con la entrada limitada a apuntar a ellos (WR_INDIRECT), para
que los datos grandes no se escriban dos veces. Con un SLOG, salvo con logbias=throughput o Direct
I/O, los datos siempre van al registro. Los bloques del registro salen primero del SLOG; un pool sin SLOG los toma
de un espacio reservado en sus vdevs ordinarios. Cuando un txg se sincroniza, se liberan los bloques del registro
cuyas entradas pertenecen todas a txg sincronizados. Tras un fallo, importar el pool reclama la cadena, y montar
el dataset reproduce cada entrada cuyo txg no se había sincronizado.
En detalle
Una entrada por escritura. En zfs_write, cada escritura queda registrada por
zfs_log_write, que crea un itx y lo pone en cola
bajo el txg de la escritura, lrc_txg. Todavía no se escribe nada. Si el
archivo se abrió con O_SYNC u O_DSYNC, o el dataset tiene sync=always, la
escritura se confirma de inmediato: zfs_write llama
a zil_commit antes de retornar. Si no, la entrada espera a fsync(), que
llama a la misma función. Lo que contiene la entrada se decide primero:
zil_write_state
Extracto de module/zfs/zil.c, líneas
2212–2246, OpenZFS zfs-2.4.4.
Ver en 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, o una escritura que tomó la ruta Direct I/O: siempre indirecta, con SLOG o sin él. El bloque de registro que contiene la entrada sigue saliendo primero del SLOG:logbiasdecide adónde van los datos, no el registro. - L2219–L2228 Indirecta solo a partir de 32 KiB y, para una escritura confirmada de inmediato, solo a partir de medio bloque, 64 KiB para el archivo de la figura, porque, como dice el comentario de arriba, la escritura siguiente podría reescribir el mismo bloque.
-
L2230–L2232
Un pool con un SLOG nunca registra en indirecto.
spa_has_slogspregunta si la clase log tiene un dispositivo, no si tiene sitio: un SLOG lleno sigue contando. -
L2233–L2238
Vdevs special sin SLOG: indirecta solo cuando los bloques del archivo van a la clase special (o
zil_special_is_slogvale 0). El pool de la figura no tiene vdev special. - L2240–L2245 Indirecta: la entrada lleva un puntero. Si no, WR_COPIED, los datos copiados en la entrada ahora, cuando la escritura se confirma de inmediato; o WR_NEED_COPY, los datos leídos del archivo cuando se llena el bloque de registro.
Después zfs_log_write convierte WR_COPIED en WR_NEED_COPY cuando los datos
superan 7680 bytes, ya que una entrada copiada debe caber en un solo bloque de registro,
y corta una escritura indirecta en cada bloque del archivo. O_DIRECT toma la
ruta Direct I/O solo para una escritura de al menos un bloque del archivo: una más corta
pasa por el ARC, así que en la figura solo la escritura de 128 KiB con O_DIRECT es Direct I/O.
Para WR_INDIRECT, el comentario de zfs_get_data da la razón: «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». El
bloque se escribe en su sitio con dmu_sync, o ya lo escribió la
escritura Direct I/O. En la figura, el panel de arriba ejecuta esta función para cada
combinación de los botones; el distintivo de cada escritura es su resultado.
La confirmación. zil_commit_impl, llamada por fsync():
zil_commit_impl
Extracto de module/zfs/zil.c, líneas
4097–4122, OpenZFS zfs-2.4.4.
Ver en 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 Una espera de confirmación (commit waiter) se pone en cola como un itx especial, detrás de todas las entradas del archivo (las demás entradas del archivo se pasaron a la lista síncrona justo antes).
- L4100 Las entradas pendientes se empaquetan en lwb y los lwb se emiten.
-
L4101
Quien llama duerme hasta que el lwb que contiene su espera está escrito y volcado (
zil_commit_waiter_done). - L4104–L4117 Ante un error de escritura del registro, se recurre a esperar una sincronización completa del txg: igual de duradera, solo más lenta. La figura no tiene errores de E/S.
La vida de un lwb. Cada lwb pasa por siete estados, en este orden:
lwb_state_t
Extracto de include/sys/zil_impl.h, líneas
83–92, OpenZFS zfs-2.4.4.
Ver en 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 Nuevo, y luego abierto con su primera entrada.
- L86 Cerrado cuando está lleno, o al final de una confirmación, que también crea el lwb siguiente, todavía vacío.
- L87–L88 Listo cuando está lleno con sus datos; emitido una vez asignado el bloque siguiente.
-
L89–L90
Escritura terminada, y luego volcado terminado cuando las cachés de los discos se han volcado: los que esperan en él se despiertan y
fsync()retorna.
El comentario sobre el enum describe las transiciones y los bloqueos que necesitan. Con un solo escritor y nada más en curso, como en la figura, el lwb que contiene una confirmación se cierra y se emite de inmediato en lugar de quedar abierto para más entradas. La figura dibuja cada estado de cada lwb, en el panel de memoria.
El empaquetado. Cómo entran las entradas en los lwb:
zil_lwb_assign
Extracto de module/zfs/zil.c, líneas
2334–2369, OpenZFS zfs-2.4.4.
Ver en 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 Una entrada que no cabe cierra el lwb y abre el siguiente. La prueba larga deja que una entrada WR_NEED_COPY empiece en un bloque casi lleno solo cuando eso ahorra una división.
- L2348–L2357 Una entrada WR_COPIED debe caber entera: nunca se divide.
- L2359–L2369 Una entrada WR_NEED_COPY mayor que el espacio que queda se divide: un clon toma lo que cabe, el resto pasa al bloque siguiente.
Cada entrada recibe entonces el número de secuencia siguiente, lrc_seq, y el
lwb guarda el txg más alto de sus entradas. La cabecera de una entrada de escritura,
lr_write_t, ocupa 192 bytes; el final de un bloque de registro,
zil_chain_t, 184 bytes; un bloque de registro, como mucho
128 KiB. Así que un bloque de 128 KiB contiene 130 888 bytes de entradas, y una
escritura WR_NEED_COPY de 128 KiB ocupa 130 696 bytes en un lwb y 376 bytes en el
siguiente: la figura lo muestra con un SLOG.
La cadena, y dónde entra el SLOG. Cuando se emite un lwb, asigna el bloque del lwb siguiente:
zil_lwb_write_issue
Extracto de module/zfs/zil.c, líneas
2073–2092, OpenZFS zfs-2.4.4.
Ver en 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
El bloque siguiente se asigna ahora, y su puntero va al final de este bloque,
zc_next_blk: cada bloque apunta al siguiente, a partir dezh_logen la cabecera. - L2087 El bloque nuevo nace en un txg abierto.
- L2088–L2091 Hereda la semilla de la suma de comprobación de este bloque, con la palabra de secuencia incrementada: en la reproducción, así se distingue un bloque siguiente válido de datos antiguos.
El primer bloque de una cadena, de 4 KiB, lo da zil_create,
con el mismo asignador, zio_alloc_zil:
zio_alloc_zil
Extracto de module/zfs/zio.c, líneas
4561–4595, OpenZFS zfs-2.4.4.
Ver en 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
Primero la clase log, los dispositivos SLOG. Si tiene éxito, fija
slog, que se convierte enLWB_FLAG_SLOGen el lwb. - L4567–L4580 Después el espacio de registro reservado en los vdevs special, y la clase special.
- L4582–L4587 Después el registro integrado: el espacio que cada vdev ordinario de primer nivel reserva para el ZIL. Un pool sin SLOG, o con uno lleno, y sin vdevs special, registra ahí.
- L4589–L4595 Por último, la clase normal.
La primera confirmación espera a un txg. La confirmación llama a zil_create
cuando el dataset no tiene registro. No puede escribir ella misma el bloque nuevo en
zh_log: la cabecera solo cambia en contexto de sincronización. Así que asigna el bloque en una
transacción y espera a que ese txg se sincronice. Las escrituras de ese txg están
entonces en el árbol, de modo que la confirmación encuentra su txg sincronizado y
destruye sus itx en lugar de registrarlos; solo la espera de confirmación entra en el
primer lwb. La primera confirmación sobre un registro nuevo, un fsync() o una escritura
O_DSYNC, cuesta por tanto una sincronización de txg, y por eso un registro sin nada
que reproducir conserva su primer bloque.
En la figura, cada asignación enumera las clases que probó. El código dice adónde va un bloque de registro, no lo rápido que es el dispositivo: lo que aporta un SLOG depende del dispositivo.
Cuando el txg se sincroniza. En contexto de sincronización, para cada dataset cuyo registro cambió:
zil_sync
Extracto de module/zfs/zil.c, líneas
4206–4224, OpenZFS zfs-2.4.4.
Ver en 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
El
zh_logde la cabecera sigue a la cabeza de la lista. - L4208–L4210 Un lwb se libera una vez volcado, cuando cada entrada que contiene pertenece a un txg sincronizado y su propio bloque se asignó en este txg o antes. El primero que no cumple detiene el bucle.
- L4211–L4214 Su bloque vuelve al asignador.
Las entradas de un txg sincronizado que nunca se escribieron en el registro simplemente las descarta
zil_clean: el txg las hizo duraderas. En la figura, los bloques liberados
aparecen en gris y tachados.
Tras un fallo: la reclamación. Al importar, en el primer txg del pool, para cada dataset:
zil_claim
Extracto de module/zfs/zil.c, líneas
1232–1253, OpenZFS zfs-2.4.4.
Ver en 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_parserecorre la cadena desdezh_log, reclamando cada bloque para que el asignador no lo vuelva a entregar, y también los bloques a los que apuntan las entradas WR_INDIRECT. El recorrido termina en el bloque asignado pero nunca escrito: su suma de comprobación no coincide. - L1242 El txg de reclamación: el primer txg del pool.
-
L1245–L1246
ZIL_REPLAY_NEEDEDsi se encontró una entrada, o si se siguió más de un bloque. El recorrido cuenta el último bloque no escrito, así que un registro vacío cuenta uno. - L1253 Ese primer txg es el último txg sincronizado más uno.
La reproducción. Al montar, zil_replay pasa cada entrada por:
zil_replay_log_record
Extracto de module/zfs/zil.c, líneas
4696–4712, OpenZFS zfs-2.4.4.
Ver en 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 Una entrada ya reproducida por una reproducción anterior interrumpida se omite.
- L4711–L4712 Una entrada de un txg anterior al txg de reclamación se omite: ese txg se sincronizó, el cambio ya está en el árbol. Todas las demás entradas se aplican de nuevo.
Sin ZIL_REPLAY_NEEDED, zil_replay solo destruye el registro,
conservando su primer bloque: la cabecera vaciada vuelve a designarlo, con un nuevo GUID
de cadena, y la confirmación siguiente escribe ahí sin asignar nada. Si no, reproduce, y
luego destruye el registro. La cabecera que contiene todo esto,
zil_header_t, vive en el dataset: zh_claim_txg,
zh_replay_seq, zh_log, los números de secuencia reclamados y los indicadores. Así, un
fallo antes de la sincronización devuelve cada entrada del txg no sincronizado; un fallo después encuentra un
registro sin nada que reproducir, y una escritura a la que nunca siguió un fsync() y que nunca se
sincronizó se pierde, como su programa permitía. Tras una reproducción se destruye todo el registro, así que la
confirmación siguiente crea uno nuevo y vuelve a esperar a un txg.
Lo que la figura deja fuera
La figura ejecuta las funciones anteriores sobre un archivo de un dataset, con un solo escritor y un solo txg en
curso, sincronizado de una vez en una pasada. El archivo ya ocupa varios registros, así que sus bloques son de
128 KiB; un archivo de un solo bloque más pequeño que recordsize tiene un bloque más
pequeño, y la regla del medio bloque usa ese. El primer bloque de una cadena nueva es de 4 KiB, como en el
código; cada bloque de registro posterior es de 128 KiB, mientras que el código elige el tamaño de cada bloque según
la ráfaga de entradas (zil_lwb_plan). Así que en una cadena real las entradas se
dividen en otros puntos, y las cifras en bytes de la figura son las de bloques de 128 KiB. Cada write()
empieza al principio de un bloque del archivo. No hay errores de E/S, así que no hay
recurso a txg_wait_synced; el volcado es un solo paso para todo el lwb, sin el
árbol de volcados por vdev; el plazo de confirmación nunca vence, porque un solo escritor cierra su lwb de inmediato.
Las escrituras que superan zil_slog_bulk por confirmación, 64 MiB, van a un SLOG
con una prioridad más baja; el formato Slim ZIL y el cifrado no se dibujan. Nada de esto
cambia un estado de escritura, una clase, ni qué escrituras se reproducen. Para cuándo se sincroniza un txg, véase
Grupos de transacciones; para cómo se reparten las clases el espacio del pool,
Metaslabs y space maps.