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()

WR_NEED_COPY 4 KiB

  1. 4 KiB es inferior a zfs_immediate_write_sz, 32 KiB: no indirecta. código
  2. No confirmada de inmediato: WR_NEED_COPY, los datos se copian en el bloque de registro solo cuando se escribe la entrada. código

Este write() retorna de inmediato; fsync() lo confirma. código

El archivo de la aplicación

Aún no hay escrituras.

En memoria

itx a la espera de una confirmación código

Ninguno.

Bloques de escritura del registro (zl_lwb_list) código

Aún no hay registro: la primera confirmación lo crea.

En disco

zil_header_t, en el dataset código

zh_log
hueco
zh_claim_txg
0
zh_flags
0
zh_replay_seq
0

La cadena, desde zh_log código

Vacía.

vdevs del pool: espacio de registro integrado y datos

  • nada
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);
}
  1. 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: logbias decide adónde van los datos, no el registro.
  2. 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.
  3. L2230–L2232 Un pool con un SLOG nunca registra en indirecto. spa_has_slogs pregunta si la clase log tiene un dispositivo, no si tiene sitio: un SLOG lleno sigue contando.
  4. L2233–L2238 Vdevs special sin SLOG: indirecta solo cuando los bloques del archivo van a la clase special (o zil_special_is_slog vale 0). El pool de la figura no tiene vdev special.
  5. 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);
	}
  1. 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).
  2. L4100 Las entradas pendientes se empaquetan en lwb y los lwb se emiten.
  3. L4101 Quien llama duerme hasta que el lwb que contiene su espera está escrito y volcado (zil_commit_waiter_done).
  4. 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;
  1. L84–L85 Nuevo, y luego abierto con su primera entrada.
  2. L86 Cerrado cuando está lleno, o al final de una confirmación, que también crea el lwb siguiente, todavía vacío.
  3. L87–L88 Listo cuando está lleno con sus datos; emitido una vez asignado el bloque siguiente.
  4. 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;
  1. 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.
  2. L2348–L2357 Una entrada WR_COPIED debe caber entera: nunca se divide.
  3. 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]++;
	}
  1. 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 de zh_log en la cabecera.
  2. L2087 El bloque nuevo nace en un txg abierto.
  3. 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);
	}
  1. L4561–L4565 Primero la clase log, los dispositivos SLOG. Si tiene éxito, fija slog, que se convierte en LWB_FLAG_SLOG en el lwb.
  2. L4567–L4580 Después el espacio de registro reservado en los vdevs special, y la clase special.
  3. 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í.
  4. 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);
	}
  1. L4207 El zh_log de la cabecera sigue a la cabeza de la lista.
  2. 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.
  3. 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));
  1. L1239–L1241 zil_parse recorre la cadena desde zh_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.
  2. L1242 El txg de reclamación: el primer txg del pool.
  3. L1245–L1246 ZIL_REPLAY_NEEDED si 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.
  4. 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);
  1. L4708–L4709 Una entrada ya reproducida por una reproducción anterior interrumpida se omite.
  2. 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.